MuteBefehl Wiki

Plugins

Build your own plugins against the 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:

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.players();              // who is online across the network, as a future

A ServiceInfo contains name, group, node, address, phase, player count, version and the properties the server has set itself.

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. If the bridge briefly loses its connection, it sends everything again on reconnect.

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 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
CloudMessageEventa message on a channel the plugin registered with MuteCloud.listen(channel)
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.

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. 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 running on Java 8 or 17 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, channels and events are not yet available on these servers.

On this page