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<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 futureEine 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 serverEin 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.
| Event | Wann |
|---|---|
CloudServiceUpdateEvent | ein Server hat seinen Zustand, seine Spielerzahl oder seine Eigenschaften geändert. previous() liefert den Stand davor, becameReady() und propertiesChanged() ersparen den Vergleich |
CloudServiceGoneEvent | ein Server ist weg, last() ist sein letzter bekannter Stand und nie null |
CloudMessageEvent | eine Nachricht auf einem beliebigen Kanal, den dieser Server empfängt, registriert mit MuteCloud.listen(channel) oder subscribe |
CloudConnectionEvent | die 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/shopDas 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.