Plugins

Build your own Paper and Velocity plugins against the MuteCloud bridge API.

Every server and every proxy receives the bridge from the cloud. It ships a small API that your own plugins compile against. The bridge itself does not belong inside your plugin; it is present on the server at runtime anyway.

Adding it

Install the bridge locally once, after that it is available to every project as a dependency. Building it needs Java 25, because the Velocity API it compiles against does:

cd bridge && mvn install
pom.xml
<dependency>
  <groupId>de.mutebefehl.cloud</groupId>
  <artifactId>mutecloud-bridge-paper</artifactId>
  <version>0.1.0</version>
  <scope>provided</scope>
</dependency>

For Velocity the artifact is called mutecloud-bridge-velocity. In Gradle it is the same with compileOnly. To make sure the bridge loads before your plugin, add it to the dependencies: for Paper depend: [MuteCloudBridge] in plugin.yml, for Velocity dependencies = {@Dependency(id = "mutecloudbridge")} in the @Plugin annotation.

Everything goes through the static class MuteCloud, on Paper from de.mutebefehl.cloud.bridge.paper, on Velocity from de.mutebefehl.cloud.bridge.velocity.

Servers in the network

MuteCloud.serviceName();          // own name, e.g. Lobby-1
MuteCloud.services();             // every server in the network
MuteCloud.services("BedWars");    // one group
MuteCloud.best("Lobby");          // the emptiest one currently accepting players
MuteCloud.send(player, "Lobby");  // send a player to a server or a group
MuteCloud.requestStart("BedWars", 2);
MuteCloud.networkPlayers();       // players in the whole network
MuteCloud.players();              // who is online and where, as a future

A ServiceInfo contains name, group, node, address, phase, player count, version and the properties the server has set itself. send returns false if there is no such server and no server of such a group. networkPlayers counts the players on the proxies, so nobody is counted twice while they are also on a game server.

Properties

A server can publish information about itself, such as the state of a round or the map that is running. The values appear on every other server, in service info and in the API.

MuteCloud.setState("WAITING");                       // short for setProperty("state", ...)
MuteCloud.setProperties(Map.of("map", "Desert", "mode", "4x2"));
MuteCloud.removeProperty("mode");

MuteCloud.withProperty("BedWars", "state", "WAITING");  // servers of a group with this value
service.property("map");                                // read a value of another server

A server may have up to 64 properties, a name at most 64 characters, a value at most 1024. Anything beyond that throws an IllegalArgumentException right at the call and is not stored. If the bridge briefly loses its connection, it sends everything again on reconnect.

On Velocity, setProperty, setProperties, removeProperty and withProperty are available; setState and reading back your own values exist only on game servers.

Channels

Channels let plugins on different servers talk to each other without Redis or a database. A message is text; an object is turned into JSON with Gson and comes back out on the other side with as.

record Invite(String from, String to) {}

CloudChannel party = MuteCloud.channel("party");

party.publish(new Invite("Steve", "Alex"));          // to everyone listening on the channel
party.send("Lobby", new Invite("Steve", "Alex"));    // only to the group Lobby
party.send("Lobby-2", "{\"ping\":1}");               // only to one server

CloudChannel.Subscription sub = party.subscribe(message -> {
    Invite invite = message.as(Invite.class);
    message.reply(Map.of("accepted", true));         // back to the sender
});
sub.close();

On Paper, receivers run on the main thread, so they may touch players and worlds. On Velocity they run on a background thread. The sender does not hear its own messages. A message may be up to 64 KB; a larger one is not delivered, and the node reports the reason in the sender's console.

Messages that come from the API or a WebSocket client have no sending server: from() is null and reply() throws. Answer those on a channel the website or bot listens to instead.

A channel name consists of letters, digits, -, _ and .. Subscribing to * receives every channel.

Events

If you prefer listeners, the same happenings are available as platform events. On Paper they are Bukkit events from de.mutebefehl.cloud.bridge.paper.event, fired on the main thread; on Velocity they are events from de.mutebefehl.cloud.bridge.velocity.event for @Subscribe.

EventWhen
CloudServiceUpdateEventa server changed its state, player count or properties. previous() returns the state before, becameReady() and propertiesChanged() save the comparison
CloudServiceGoneEventa server is gone, last() is its last known state and never null
CloudMessageEventa message on any channel this server receives, registered with MuteCloud.listen(channel) or subscribe
CloudConnectionEventthe connection to the node is up (connected()) or has dropped
@EventHandler
public void onUpdate(CloudServiceUpdateEvent event) {
    if (event.becameReady() && event.service().group().equals("BedWars")) {
        Bukkit.broadcast(Component.text(event.service().name() + " is ready"));
    }
}

Shared storage

CloudData stats = MuteCloud.data("bedwars");
stats.set("wins:Steve", "12");
stats.get("wins:Steve").thenAccept(value -> ...);
stats.list("wins:").thenAccept(all -> ...);

The storage lives on the node, in a cluster on the leader. It is meant for small values every server should see, not as a replacement for a database.

The futures of data and players complete on the bridge's network thread. Go back to the main thread with the scheduler before touching players or worlds, and never wait for another answer inside such a callback: getOr(key, fallback, millis) blocks until the answer arrives, and inside a callback it waits for itself and returns the fallback after the timeout. Call it from the main thread or from your own thread.

From outside

Channels are also reachable through the API, so a website or a Discord bot can address plugins directly:

curl -H "$A" -X POST -H 'Content-Type: application/json' \
     -d '{"message":{"player":"Steve","coins":100},"target":"Lobby"}' \
     $H/api/channels/shop

The token needs the right channels.publish. Such messages have no sending server, see channels. In the console the same works with publish shop @Lobby {"player":"Steve","coins":100}, which is handy for trying things out.

Over the WebSocket a program listens by registering channels after its hello:

{"t":"listen","d":{"channels":["shop","party"]}}

Messages then arrive as message with channel, message, from and target.

Servers up to 1.20.4

Servers up to 1.20.4 and 1.8 servers, even when they run on Java 21, receive the legacy bridge, whose API is smaller. It lives at de.mutebefehl.cloud.bridge.legacy.MuteCloud in the artifact mutecloud-bridge-legacy and offers serviceName, group, connected, services, own, best, networkPlayers, players, node, send, requestStart, stop, drain and data. There, own and best return null instead of an Optional.

Properties work there as well, through setState, setProperty, setProperties, removeProperty and property for reading back one of the server's own values. That is enough for the NPC action auffuellen in the server selection. Channels and events are not yet available on these servers.

On this page