Plugins

Eigene Paper- und Velocity-Plugins gegen die Bridge-API von MuteCloud bauen.

Jeder Server und jeder Proxy bekommt die Bridge von der Cloud. Sie bringt eine kleine API mit, gegen die deine eigenen Plugins kompilieren. Die Bridge selbst gehört nicht in dein Plugin, sie ist zur Laufzeit ohnehin auf dem Server vorhanden.

Einbinden

Installiere die Bridge einmal lokal, danach steht sie jedem Projekt als Abhängigkeit zur Verfügung. Zum Bauen brauchst du Java 25, weil die Velocity-API, gegen die sie kompiliert, das verlangt:

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>

Für Velocity heißt das Artefakt mutecloud-bridge-velocity. In Gradle geht es genauso mit compileOnly. Damit die Bridge vor deinem Plugin geladen wird, trägst du sie als Abhängigkeit ein: bei Paper depend: [MuteCloudBridge] in der plugin.yml, bei Velocity dependencies = {@Dependency(id = "mutecloudbridge")} in der @Plugin-Annotation.

Alles läuft über die statische Klasse MuteCloud, bei Paper aus de.mutebefehl.cloud.bridge.paper, bei Velocity aus de.mutebefehl.cloud.bridge.velocity.

Server im Netzwerk

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

Eine ServiceInfo enthält Name, Gruppe, Node, Adresse, Phase, Spielerzahl, Version und die Eigenschaften, die der Server selbst gesetzt hat. send gibt false zurück, wenn es weder einen solchen Server noch einen Server einer solchen Gruppe gibt. networkPlayers zählt die Spieler auf den Proxys, so wird niemand doppelt gezählt, während er zugleich auf einem Spielserver ist.

Eigenschaften

Ein Server kann Informationen über sich veröffentlichen, etwa den Stand einer Runde oder die laufende Map. Die Werte erscheinen auf jedem anderen Server, in service info und in der 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

Ein Server darf bis zu 64 Eigenschaften haben, ein Name höchstens 64 Zeichen, ein Wert höchstens 1024. Alles darüber wirft direkt beim Aufruf eine IllegalArgumentException und wird nicht gespeichert. Verliert die Bridge kurz ihre Verbindung, schickt sie beim Wiederverbinden alles erneut.

Auf Velocity stehen setProperty, setProperties, removeProperty und withProperty zur Verfügung. setState und das Zurücklesen der eigenen Werte gibt es nur auf Spielservern.

Kanäle

Über Kanäle können Plugins auf verschiedenen Servern miteinander sprechen, ohne Redis oder eine Datenbank. Eine Nachricht ist Text. Ein Objekt wird mit Gson zu JSON und kommt auf der anderen Seite mit as wieder heraus.

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();

Auf Paper laufen Empfänger im Hauptthread, sie dürfen also Spieler und Welten anfassen. Auf Velocity laufen sie in einem Hintergrundthread. Der Absender hört seine eigenen Nachrichten nicht. Eine Nachricht darf bis zu 64 KB groß sein. Eine größere wird nicht zugestellt, und die Node nennt den Grund in der Konsole des Absenders.

Nachrichten, die aus der API oder von einem WebSocket-Client kommen, haben keinen sendenden Server: from() ist null, und reply() wirft eine Ausnahme. Antworte darauf stattdessen auf einem Kanal, auf dem die Website oder der Bot mithört.

Ein Kanalname besteht aus Buchstaben, Ziffern, -, _ und .. Wer * abonniert, bekommt jeden Kanal.

Events

Wenn dir Listener lieber sind, gibt es dieselben Vorgänge als Plattform-Events. Auf Paper sind es Bukkit-Events aus de.mutebefehl.cloud.bridge.paper.event, ausgelöst im Hauptthread. Auf Velocity sind es Events aus de.mutebefehl.cloud.bridge.velocity.event für @Subscribe.

EventWann
CloudServiceUpdateEventein Server hat seinen Zustand, seine Spielerzahl oder seine Eigenschaften geändert. previous() liefert den Stand davor, becameReady() und propertiesChanged() ersparen den Vergleich
CloudServiceGoneEventein Server ist weg, last() ist sein letzter bekannter Stand und nie null
CloudMessageEventeine Nachricht auf einem beliebigen Kanal, den dieser Server empfängt, registriert mit MuteCloud.listen(channel) oder subscribe
CloudConnectionEventdie Verbindung zur Node steht (connected()) oder ist abgerissen
@EventHandler
public void onUpdate(CloudServiceUpdateEvent event) {
    if (event.becameReady() && event.service().group().equals("BedWars")) {
        Bukkit.broadcast(Component.text(event.service().name() + " is ready"));
    }
}

Gemeinsamer Speicher

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

Der Speicher liegt auf der Node, im Verbund auf dem Leiter. Er ist für kleine Werte gedacht, die jeder Server sehen soll, nicht als Ersatz für eine Datenbank.

Die Futures von data und players werden im Netzwerkthread der Bridge abgeschlossen. Wechsle mit dem Scheduler zurück in den Hauptthread, bevor du Spieler oder Welten anfasst, und warte in einem solchen Callback nie auf eine weitere Antwort: getOr(key, fallback, millis) blockiert, bis die Antwort da ist, und in einem Callback wartet es auf sich selbst und liefert nach Ablauf der Wartezeit den Fallback. Ruf es aus dem Hauptthread oder aus einem eigenen Thread auf.

Von außen

Kanäle sind auch über die API erreichbar, so kann eine Website oder ein Discord-Bot Plugins direkt ansprechen:

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

Das Token braucht das Recht channels.publish. Solche Nachrichten haben keinen sendenden Server, siehe Kanäle. In der Konsole geht dasselbe mit publish shop @Lobby {"player":"Steve","coins":100}, das ist praktisch zum Ausprobieren.

Über den WebSocket hört ein Programm mit, indem es nach seinem hello Kanäle registriert:

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

Nachrichten kommen dann als message mit channel, message, from und target an.

Server bis 1.20.4

Server bis 1.20.4 und 1.8-Server bekommen, auch wenn sie auf Java 21 laufen, die Legacy-Bridge, deren API kleiner ist. Sie liegt unter de.mutebefehl.cloud.bridge.legacy.MuteCloud im Artefakt mutecloud-bridge-legacy und bietet serviceName, group, connected, services, own, best, networkPlayers, players, node, send, requestStart, stop, drain und data. Dort liefern own und best null statt eines Optional.

Eigenschaften funktionieren dort ebenfalls, über setState, setProperty, setProperties, removeProperty und property zum Zurücklesen eines eigenen Werts des Servers. Das reicht für die NPC-Aktion auffuellen in der Serverauswahl. Kanäle und Events gibt es auf diesen Servern noch nicht.

Auf dieser Seite