Client Payloads
GrieferGames sendet sogenannte Custom Plugin Messages (Client Payloads) über einen dedizierten Kanal an verbundene Clients. Diese Seite dokumentiert alle verfügbaren Payloads, ihr binäres Übertragungsformat und wie du sie in deinem Mod empfangen kannst.
#Übersicht
| Eigenschaft | Wert |
|---|---|
| Kanal | griefergames:main |
| Richtung | Server → Client (unidirektional) |
| Registrierung | Keine client-seitige Registrierung erforderlich |
Du musst kein Registrierungs- oder Handshake-Paket an den Server senden. Der Server schickt die Payloads eigenständig an jeden verbundenen Client.
#Verfügbarkeit
| Umgebung | Wo werden Payloads gesendet? |
|---|---|
| 1.8 | Auf allen Servern außer der Lobby und dem Portalraum |
| Cloud | Alle Server auf denen die Verwendung von Geld möglich ist |
#Übertragungsformat
Alle Payloads teilen dieselbe äußere Struktur auf dem griefergames:main-Channel:
┌──────────────────────────────────┐
│ id UTF (Java DataOutput) │ ← Payload-Identifikator
│ <body> payload-spezifische │ ← Felder je nach Payload (siehe unten)
│ Daten │
└──────────────────────────────────┘
Der UTF-String wird im Java-DataOutputStream.writeUTF-Format kodiert:
- 2 Bytes Big-Endian Unsigned Short → Länge der folgenden UTF-8-Byte-Sequenz
- N Bytes → String-Inhalt in modifiziertem UTF-8
Alle nachfolgenden numerischen Felder folgen der Standard-Big-Endian-Kodierung von DataOutputStream.
#Verfügbare Payloads
#accountbalance
Wird an den Client gesendet, sobald sich das Bargeld des Spielers ändert, sowie einmalig beim Betreten eines Servers.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "accountbalance" |
balance |
double (8 Bytes, Big-Endian IEEE 754) |
Aktuelles Guthaben des Spielers, auf 2 Dezimalstellen gerundet |
Beispiel-Hex-Aufschlüsselung (Guthaben = 1234.56):
00 0D 61 63 63 6F 75 6E 74 62 61 6C 61 6E 63 65 ← UTF: 2-Byte-Länge (13) + "accountbalance"
40 93 4A 3D 70 A3 D7 0A ← double: 1234.56
#bankbalance
Wird an den Client gesendet, sobald sich das Bankguthaben des Spielers ändert, sowie einmalig beim Betreten eines Servers.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "bankbalance" |
balance |
double (8 Bytes, Big-Endian IEEE 754) |
Aktuelles Bankguthaben des Spielers, auf 2 Dezimalstellen gerundet |
#blockoftheday
Wird an den Client gesendet, um Informationen über den Block oder die Entity des Tages zu übermitteln.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "blockoftheday" |
type |
UTF | "BLOCK", "MATERIAL" oder "ENTITY" |
blockMaterial |
UTF | Bukkit-Material (z.B. "PRISMARINE"); leer wenn type = "ENTITY" |
blockData |
int (4 Bytes) |
Block-Daten / Variante (z.B. 1 = Prismarinziegel bei PRISMARINE) |
entityType |
UTF | Bukkit-EntityType (z.B. "WITHER"); leer wenn type = "BLOCK" oder "MATERIAL" |
Feldlogik je nach Type:
type = "BLOCK"oder"MATERIAL":blockMaterialgefüllt,entityTypeleertype = "ENTITY":entityTypegefüllt,blockMaterialleer
#blockoftheday_progress
Wird an den Client gesendet, wenn der Spieler den Block oder die Entity des Tages abbaut/tötet und dabei einen Reward erhält. Dient als Bestätigung für die erfolgreiche Interaktion. Dieses Payload hat keine zusätzlichen Felder.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "blockoftheday_progress" |
#booster
Wird an den Client gesendet, um aktive Booster und deren Multiplikatoren zu übermitteln.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "booster" |
count |
int (4 Bytes) |
Anzahl der Booster in dieser Liste |
| Für jeden Booster: | ||
type |
UTF | Booster-Typ: "BREAK", "DROP", "FLY", "MOB", "XP" |
multiplier |
int (4 Bytes) |
Multiplikator des Boosters (z.B. 2 für 2x) |
tierCount |
int (4 Bytes) |
Anzahl der Einträge in remainingSeconds (= multiplier) |
remainingSeconds |
long (8 Bytes) × tierCount |
Verbleibende Sekunden bis zum Ablauf jeder einzelnen aktiven Stufe, aufsteigend sortiert |
Stapelbare Booster (z.B. Mob-Booster): Ein Booster kann mehrfach aktiviert werden und dadurch mehrere Stufen (Stack) besitzen, wobei jede Stufe zu einem eigenen Zeitpunkt abläuft (z.B. hat ein Stufe-5-Mob-Booster 5 unterschiedliche Endzeitpunkte). Deshalb wird remainingSeconds als Liste mit genau multiplier Einträgen übertragen, aufsteigend sortiert: Der erste Wert ist die Restzeit bis zum nächsten Ablauf einer Stufe (wodurch sich multiplier um 1 verringert), der letzte Wert die Restzeit der zuletzt aktivierten/am längsten laufenden Stufe.
#clearlag
Wird an den Client gesendet, um die verbleibenden Sekunden bis zum nächsten ClearLag-Event zu übermitteln.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "clearlag" |
remainingSeconds |
long (8 Bytes) |
Verbleibende Sekunden bis zum nächsten ClearLag |
#entityremover
Wird an den Client gesendet, um die verbleibenden Sekunden bis zum nächsten Entity Remover-Event zu übermitteln.
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "entityremover" |
remainingSeconds |
long (8 Bytes) |
Verbleibende Sekunden bis zum nächsten Entity Remover |
#plotchat_configuration
Wird an den Client gesendet, um die PlotChat-Konfiguration zu aktualisieren (ob PlotChat aktiviert ist oder nicht).
| Feld | Typ | Beschreibung |
|---|---|---|
id |
UTF | "plotchat_configuration" |
status |
boolean (1 Byte) |
true = PlotChat aktiviert, false = deaktiviert |
#Payloads empfangen — Code-Beispiele
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
public class MeinModClient implements ClientModInitializer {
public static final Identifier CHANNEL_ID = Identifier.of("griefergames", "main");
public record GrieferGamesPayload(byte[] data) implements CustomPayload {
public static final CustomPayload.Id<GrieferGamesPayload> ID = new CustomPayload.Id<>(CHANNEL_ID);
public static final PacketCodec<PacketByteBuf, GrieferGamesPayload> CODEC = PacketCodec.of(
(payload, buf) -> buf.writeBytes(payload.data()),
buf -> {
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
return new GrieferGamesPayload(bytes);
}
);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
@Override
public void onInitializeClient() {
PayloadTypeRegistry.playS2C().register(GrieferGamesPayload.ID, GrieferGamesPayload.CODEC);
ClientPlayNetworking.registerGlobalReceiver(GrieferGamesPayload.ID, (payload, context) -> {
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload.data()))) {
String id = in.readUTF();
switch (id) {
case "accountbalance": {
double balance = in.readDouble();
context.client().execute(() -> MeinMod.onMoneyUpdate(balance));
break;
}
case "bankbalance": {
double balance = in.readDouble();
context.client().execute(() -> MeinMod.onBankUpdate(balance));
break;
}
case "blockoftheday": {
String type = in.readUTF();
String blockMaterial = in.readUTF();
int blockData = in.readInt();
String entityType = in.readUTF();
// type ist "BLOCK"/"MATERIAL" → blockMaterial gefüllt, entityType leer
// type ist "ENTITY" → entityType gefüllt, blockMaterial leer
context.client().execute(() -> MeinMod.onBlockOfTheDay(type, blockMaterial, blockData, entityType));
break;
}
case "blockoftheday_progress": {
context.client().execute(() -> MeinMod.onBlockProgressUpdate());
break;
}
case "booster": {
int count = in.readInt();
for (int i = 0; i < count; i++) {
String type = in.readUTF();
int multiplier = in.readInt();
int tierCount = in.readInt();
long[] remainingSecondsPerTier = new long[tierCount];
for (int t = 0; t < tierCount; t++) {
remainingSecondsPerTier[t] = in.readLong();
}
context.client().execute(() -> MeinMod.onBooster(type, multiplier, remainingSecondsPerTier));
}
break;
}
case "clearlag": {
long remainingSeconds = in.readLong();
context.client().execute(() -> MeinMod.onClearLag(remainingSeconds));
break;
}
case "entityremover": {
long remainingSeconds = in.readLong();
context.client().execute(() -> MeinMod.onEntityRemover(remainingSeconds));
break;
}
case "plotchat_configuration": {
boolean status = in.readBoolean();
context.client().execute(() -> MeinMod.onPlotChatConfig(status));
break;
}
default:
// Unbekannte ID ignorieren
// Neue Payloads können jederzeit hinzukommen
break;
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.network.FMLEventChannel;
import net.minecraftforge.fml.common.network.FMLNetworkEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
@Mod(modid = "meinmod", name = "MeinMod", version = "1.0")
public class MeinMod {
private static final String CHANNEL = "griefergames:main";
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
// Kanal registrieren und diese Klasse als Listener anmelden
FMLEventChannel channel = NetworkRegistry.INSTANCE.newEventDrivenChannel(CHANNEL);
channel.register(this);
}
@SubscribeEvent
public void onCustomPacket(FMLNetworkEvent.ClientCustomPacketEvent event) {
ByteBuf buf = event.getPacket().payload();
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) {
String id = in.readUTF();
switch (id) {
case "accountbalance": {
double balance = in.readDouble();
Minecraft.getMinecraft().addScheduledTask(() -> onMoneyUpdate(balance));
break;
}
case "bankbalance": {
double balance = in.readDouble();
Minecraft.getMinecraft().addScheduledTask(() -> onBankUpdate(balance));
break;
}
case "blockoftheday": {
String type = in.readUTF();
String blockMaterial = in.readUTF();
int blockData = in.readInt();
String entityType = in.readUTF();
// type ist "BLOCK"/"MATERIAL" → blockMaterial gefüllt, entityType leer
// type ist "ENTITY" → entityType gefüllt, blockMaterial leer
Minecraft.getMinecraft().addScheduledTask(() -> onBlockOfTheDay(type, blockMaterial, blockData, entityType));
break;
}
case "blockoftheday_progress": {
Minecraft.getMinecraft().addScheduledTask(() -> onBlockProgressUpdate());
break;
}
case "booster": {
int count = in.readInt();
for (int i = 0; i < count; i++) {
String type = in.readUTF();
int multiplier = in.readInt();
int tierCount = in.readInt();
long[] remainingSecondsPerTier = new long[tierCount];
for (int t = 0; t < tierCount; t++) {
remainingSecondsPerTier[t] = in.readLong();
}
Minecraft.getMinecraft().addScheduledTask(() -> onBooster(type, multiplier, remainingSecondsPerTier));
}
break;
}
case "clearlag": {
long remainingSeconds = in.readLong();
Minecraft.getMinecraft().addScheduledTask(() -> onClearLag(remainingSeconds));
break;
}
case "entityremover": {
long remainingSeconds = in.readLong();
Minecraft.getMinecraft().addScheduledTask(() -> onEntityRemover(remainingSeconds));
break;
}
case "plotchat_configuration": {
boolean status = in.readBoolean();
Minecraft.getMinecraft().addScheduledTask(() -> onPlotChatConfig(status));
break;
}
default:
// Unbekannte ID ignorieren
// Neue Payloads können jederzeit hinzukommen
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
import net.labymod.api.client.component.event.PluginMessageEvent;
import net.labymod.api.event.Subscribe;
import java.io.DataInputStream;
import java.io.ByteArrayInputStream;
public class MeinPayloadListener {
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!"griefergames:main".equals(event.getIdentifier())) return;
byte[] data = event.getData();
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) {
String id = in.readUTF();
switch (id) {
case "accountbalance": {
double balance = in.readDouble();
MeinAddon.onMoneyUpdate(balance);
break;
}
case "bankbalance": {
double balance = in.readDouble();
MeinAddon.onBankUpdate(balance);
break;
}
case "blockoftheday": {
String type = in.readUTF();
String blockMaterial = in.readUTF();
int blockData = in.readInt();
String entityType = in.readUTF();
// type ist "BLOCK"/"MATERIAL" → blockMaterial gefüllt, entityType leer
// type ist "ENTITY" → entityType gefüllt, blockMaterial leer
MeinAddon.onBlockOfTheDay(type, blockMaterial, blockData, entityType);
break;
}
case "blockoftheday_progress": {
MeinAddon.onBlockProgressUpdate();
break;
}
case "booster": {
int count = in.readInt();
for (int i = 0; i < count; i++) {
String type = in.readUTF();
int multiplier = in.readInt();
int tierCount = in.readInt();
long[] remainingSecondsPerTier = new long[tierCount];
for (int t = 0; t < tierCount; t++) {
remainingSecondsPerTier[t] = in.readLong();
}
MeinAddon.onBooster(type, multiplier, remainingSecondsPerTier);
}
break;
}
case "clearlag": {
long remainingSeconds = in.readLong();
MeinAddon.onClearLag(remainingSeconds);
break;
}
case "entityremover": {
long remainingSeconds = in.readLong();
MeinAddon.onEntityRemover(remainingSeconds);
break;
}
case "plotchat_configuration": {
boolean status = in.readBoolean();
MeinAddon.onPlotChatConfig(status);
break;
}
default:
// Unbekannte ID ignorieren
// Neue Payloads können jederzeit hinzukommen
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Den Listener im AddonEntryPoint registrieren:
labyAPI().eventBus().registerListener(new MeinPayloadListener());
Das Payload kommt als clientbound Plugin Message-Packet auf dem Channel griefergames:main an. Den Datenbereich mit einem DataInputStream lesen, wie in den Beispielen oben gezeigt.
| Protokollversion | Packet-ID |
|---|---|
| 1.8 | 0x3F |
| 1.21+ | 0x02 (Configuration) / 0x18 (Play) |
#Zukunftssicherheit
Das Payload-System ist erweiterbar. Beim Empfang eines Payloads immer:
#Zuerst das id-Feld lesen.
#Per switch/if auf die ID reagieren und unbekannte IDs ignorieren.
Neue Payloads können jederzeit hinzukommen.
#Keine feste Gesamtlänge der Packetdaten annehmen.
#Kurzübersicht
Kanal: griefergames:main
Payloads:
┌──────────────────────────────┬─────────────────────────────────┬──────────────────────────────────────────────┐
│ ID │ Felder │ Wann gesendet │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ accountbalance │ double │ Beim Betreten eines Servers & bei jeder │
│ │ │ Guthabenänderung │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ bankbalance │ double │ Beim Betreten eines Servers & bei jeder │
│ │ │ Bankguthabenänderung │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ blockoftheday │ UTF, UTF, int, UTF │ Wenn der Block/die Entity des Tages │
│ │ │ aktualisiert wird │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ blockoftheday_progress │ (keine) │ Wenn der Spieler einen Reward vom Block/der │
│ │ │ Entity des Tages erhält │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ booster │ int, [UTF, int, int, long, ...] │ Beim Betreten eines Servers & bei Änderung │
│ │ │ der aktiven Booster │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ clearlag │ long │ Regelmäßig zur Anzeige der Zeit bis │
│ │ │ ClearLag │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ entityremover │ long │ Regelmäßig zur Anzeige der Zeit bis │
│ │ │ Entity Remover │
├──────────────────────────────┼─────────────────────────────────┼──────────────────────────────────────────────┤
│ plotchat_configuration │ boolean │ Beim Betreten eines Servers & bei Änderung │
│ │ │ der PlotChat-Einstellung │
└──────────────────────────────┴─────────────────────────────────┴──────────────────────────────────────────────┘