Skip to main content
Version: Minecraft 26.2

Networking and sync

Payload channels​

PayloadChannel<String> greetings = mod.network().channel("greetings", PayloadCodec.string());
greetings.onServerReceive((message, sender) -> { /* server thread */ });
greetings.onClientReceive(message -> { /* client thread */ });
greetings.sendToPlayer(player, "hello");
greetings.sendToServer("thanks"); // from the client
  • Declare channels in the common entrypoint. A channel ID is <mod id>:<path>.
  • Received messages are handled on the matching game thread (server thread or client thread).
  • Size limits: 32 KiB toward the server, 1 MiB toward the client. If you go over, you get POLY-NETWORK-003.
  • Invalid payload: Poly Mod Loader drops it and logs POLY-NETWORK-004.
  • Sending before the connection is ready gives POLY-NETWORK-002.
Trust boundary

The server must always validate values sent by the client. A client can send any value.

Write your own codec​

For messages other than strings, implement encode and decode of PayloadCodec<T>.

record Ping(int id, String text) {}

PayloadCodec<Ping> PING_CODEC = new PayloadCodec<>() {
@Override
public void encode(Ping value, PayloadWriter out) {
out.writeInt(value.id()).writeString(value.text());
}

@Override
public Ping decode(PayloadReader in) {
return new Ping(in.readInt(), in.readString());
}
};

PayloadWriter and PayloadReader handle boolean, int, long, double, String, and byte arrays. Codecs run on the network thread, so do not access game state in them.

Exchange in the configuration stage​

Before a player enters the world, the server asks the client a question and receives an answer in the game's configuration stage. Use it for version checks, matching settings, and data the client must receive first. The player joins only after the exchange finishes, and the server can refuse the connection.

// common entrypoint: declare on both sides
handshake = mod.network().configurationExchange("handshake", PayloadCodec.string())
.serverRequest((id, name) -> Optional.of("version?")) // what to send this player; empty to skip
.serverCheck((id, name, reply) -> reply.filter(v -> v.startsWith("my_mod "))
.isPresent() ? Optional.empty() // let the player in
: Optional.of("Install My Mod on your client to join.")); // refusal message

// client entrypoint
handshake.clientAnswer(request -> "my_mod 1.2.0");
  1. The server's serverRequest decides the message to send to each player, or skips the player (with Optional.empty()).
  2. The client's clientAnswer replies.
  3. The server's serverCheck looks at the reply. If there is no reply within timeoutTicks (default 100 ticks, 5 seconds), it receives an empty value. A client without the mod does not reply. If you return an empty value, the player is let in. If you return a message, the connection is closed with that message.
  • If there is no serverCheck, a player who does not reply is refused ("This server needs the mod … on your client.").
  • The server side is called on the server thread, and the client side on the client thread.
  • After the game syncs registries and before the player is placed in the world, exchanges run one at a time in declaration order.
  • If mod code throws an exception, Poly Mod Loader logs POLY-NETWORK-005. If a request fails, the exchange is skipped. If an answer fails, no answer is sent. If a check fails, that player is refused.

Entity data​

You attach mod data to any entity, including players.

EntityDataKey MANA = mod.content().entityData("mana", true); // true: sync to the player's own client
StoredData mana = player.data(MANA);
mana.data().putInt("value", 10);
mana.markChanged();
  • It is stored in poly_entity_data in the entity's saved data. For players, it persists across respawns.
  • It is read-only on the client. Change it on the server.

Sync options​

EntityDataKey MOOD = mod.content().entityData("golem_mood", EntityDataOptions.DEFAULT
.syncedToTrackers() // to every client that sees the entity
.withVersion(2, (data, fromVersion) -> { /* … */ })); // converts old saved data when loading
OptionSent toSent when
syncedToOwner() (entityData(path, true))The player's own clientWhen it changes and when the player joins
syncedToTrackers()Every client that sees the entityOnce when a client starts seeing the entity, and every time it changes
(default)Not sent—

Read it on the client with entity.data(key). Data of other players or mobs that you did not sync does not go to the client.

World storage sync​

StorageOptions options = StorageOptions.DEFAULT.syncedToClients();
world.storage(ResourceId.of("my_mod", "info"), options); // read-only copy on the client

Opening the same storage ID with different options throws IllegalArgumentException. Open it with the same options on the client too.

Block entity sync​

If you turn on BlockEntityDefinition.builder().syncToClients(), block entity data goes to clients on markChanged(). See Block entities and menus.

Error codes​

CodeMeaning
POLY-NETWORK-001Channel ID error
POLY-NETWORK-002Sent before the connection was ready, or declared after registration closed
POLY-NETWORK-003Size exceeded
POLY-NETWORK-004Decoding failed (dropped)
POLY-NETWORK-005Mod code in a configuration-stage exchange threw an exception, or an invalid timeout
Verification scope

Only the network round trip on the single-player integrated server is verified automatically. Connections between a dedicated server and a separate client require accepting the EULA, so they are not verified automatically.

Buy me a coffee