agentleFS
vvs1offvvs1off/fabric-skills/skills/fabric-networking/SKILL.md

fabric-networking

Skill4 starsChanged 13 days agoMITView on GitHub

Send data between Minecraft client and server in a Fabric mod using the current payload API - CustomPacketPayload records, StreamCodec, PayloadTypeRegistry, ServerPlayNetworking / ClientPlayNetworking, PlayerLookup - with server-side validation and strict client/server separation. Use when adding a custom packet, syncing state to clients, sending input to the server, or fixing "unknown payload", disconnects or desyncs.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Never trust ids, positions, counts, slot indexes, entity ids, strings or NBT from a client.

    line 134
  2. Very specific:

    Check the entity still exists (level().getEntity(id)), is the right type, and is within interaction range (closerThan, distanceToSqr).

    line 135
  3. Very specific:

    Check permissions for anything admin-like (the vanilla permission check changed shape in recent versions - look up how ServerPlayer exposes …

    line 137
  4. Distinctive:

    Check the chunk is loaded before reading blocks - loading chunks on request is a DoS vector.

    line 139
  5. Very specific:

    Prefer tracking/around over all: fewer packets, and players far away should not learn about things they cannot see.

    line 181
  6. Distinctive:

    Read the docs page for your version before using it - this area changed in several releases.

    line 198
  7. Distinctive:

    No server-side validation: trusting a client-sent position, entity id, amount, or "isCreative" flag.

    line 209

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Fabric networking

There is always a server, even in singleplayer (the integrated server). Anything the client wants to change in the world goes client -> server as a request; the server decides, applies, and tells the clients what happened.

Step 0 - Which API generation?

MinecraftAPINotes
26.1+CustomPacketPayload + StreamCodec; PayloadTypeRegistry.clientboundPlay() / serverboundPlay()Mojang names throughout
1.20.5 - 1.21.11Same payload/codec design. Registry methods playS2C() / playC2S(). Yarn names: CustomPayload, PacketCodec, PacketCodecs, RegistryByteBuf, CustomPayload.IdMojang-mapped 1.21.x uses Mojang names with the playS2C/playC2S methods
1.20.4 and olderChannel Identifier + PacketByteBuf (ServerPlayNetworking.send(player, id, buf))Legacy - do not use this shape on 1.20.5+

Detect it from minecraft_version in gradle.properties, then confirm the method names in Fabric API's sources (./gradlew genSources, or open PayloadTypeRegistry in the IDE). Version-matched docs: https://docs.fabricmc.net/develop/networking

Examples below use the 26.1+ names.

1. Define the payload (common code, src/main)

A payload is a record implementing CustomPacketPayload, with a TYPE and a StreamCodec.

package com.example.examplemod.network;

import net.minecraft.core.BlockPos;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import com.example.examplemod.ExampleMod;

/** Client -> server: "please ping this block". */
public record PingBlockPayload(BlockPos pos, int colorIndex) implements CustomPacketPayload {
    public static final Type<PingBlockPayload> TYPE = new Type<>(ExampleMod.id("ping_block"));

    public static final StreamCodec<RegistryFriendlyByteBuf, PingBlockPayload> CODEC = StreamCodec.composite(
        BlockPos.STREAM_CODEC, PingBlockPayload::pos,
        ByteBufCodecs.VAR_INT, PingBlockPayload::colorIndex,
        PingBlockPayload::new
    );

    @Override
    public Type<? extends CustomPacketPayload> type() {
        return TYPE;
    }
}
/** Server -> client: "show a ping at this position". */
public record ShowPingPayload(BlockPos pos, int colorIndex, UUID from) implements CustomPacketPayload {
    public static final Type<ShowPingPayload> TYPE = new Type<>(ExampleMod.id("show_ping"));
    public static final StreamCodec<RegistryFriendlyByteBuf, ShowPingPayload> CODEC = StreamCodec.composite(
        BlockPos.STREAM_CODEC, ShowPingPayload::pos,
        ByteBufCodecs.VAR_INT, ShowPingPayload::colorIndex,
        UUIDUtil.STREAM_CODEC, ShowPingPayload::from,
        ShowPingPayload::new
    );
    @Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}

Codec tips:

  • Primitives: ByteBufCodecs.BOOL, VAR_INT, INT, FLOAT, DOUBLE, STRING_UTF8 (prefer ByteBufCodecs.stringUtf8(maxLength) to cap untrusted strings).
  • Vanilla types ship their own: BlockPos.STREAM_CODEC, ItemStack.STREAM_CODEC, ComponentSerialization.STREAM_CODEC, UUIDUtil.STREAM_CODEC.
  • Collections: ByteBufCodecs.collection(ArrayList::new, elementCodec, maxSize).
  • StreamCodec.composite has overloads up to a fixed number of fields; beyond that, nest a record or write the codec with StreamCodec.of(encoder, decoder).
  • Payload ids live in your mod's namespace (examplemod:ping_block). Never reuse vanilla ids.

2. Register payload types (common initializer - both sides)

@Override
public void onInitialize() {
    PayloadTypeRegistry.serverboundPlay().register(PingBlockPayload.TYPE, PingBlockPayload.CODEC);
    PayloadTypeRegistry.clientboundPlay().register(ShowPingPayload.TYPE, ShowPingPayload.CODEC);

    ServerPlayNetworking.registerGlobalReceiver(PingBlockPayload.TYPE, ServerPings::handle);
}

Type registration must happen in the common ModInitializer, so both the client and the dedicated server know the payload. Registering only in the client initializer gives "unknown custom payload" / disconnects on servers.

3. Receive on the server - validate everything

(PingColors and RateLimiter stand for your own mod's classes.)

public final class ServerPings {
    private static final double MAX_DISTANCE = 64.0;

    public static void handle(PingBlockPayload payload, ServerPlayNetworking.Context context) {
        ServerPlayer player = context.player();
        BlockPos pos = payload.pos();

        // 1. Validate ranges and enums - the client can send ANY bytes.
        if (payload.colorIndex() < 0 || payload.colorIndex() >= PingColors.COUNT) return;

        // 2. Validate world state: loaded chunk, distance, permissions.
        if (!player.level().isLoaded(pos)) return;
        if (player.distanceToSqr(Vec3.atCenterOf(pos)) > MAX_DISTANCE * MAX_DISTANCE) return;

        // 3. Rate-limit anything a player can spam.
        if (!RateLimiter.tryAcquire(player.getUUID())) return;

        // 4. Act, then broadcast only to players who need it.
        ShowPingPayload out = new ShowPingPayload(pos, payload.colorIndex(), player.getUUID());
        for (ServerPlayer target : PlayerLookup.tracking(player.level(), pos)) {
            ServerPlayNetworking.send(target, out);
        }
    }
}

The handler already runs on the server thread (Fabric API documents this), so it may touch the world directly. Do not spawn your own threads to handle packets.

Server-side validation checklist:

  • Never trust ids, positions, counts, slot indexes, entity ids, strings or NBT from a client.
  • Check the entity still exists (level().getEntity(id)), is the right type, and is within interaction range (closerThan, distanceToSqr).
  • Check permissions for anything admin-like (the vanilla permission check changed shape in recent versions - look up how ServerPlayer exposes permissions in your sources).
  • Check the chunk is loaded before reading blocks - loading chunks on request is a DoS vector.
  • Clamp sizes of collections and strings; cap work per packet.
  • Silently ignore (or log at debug) bad packets; do not crash or kick on the first bad value unless it is clearly malicious.

4. Receive on the client (client initializer, src/client)

@Override
public void onInitializeClient() {
    ClientPlayNetworking.registerGlobalReceiver(ShowPingPayload.TYPE, (payload, context) -> {
        if (context.client().level == null) return;
        PingRenderer.add(payload.pos(), payload.colorIndex(), payload.from());
    });
}

Client handlers run on the client (render) thread. Keep them cheap: store the data, let rendering/tick code use it.

5. Send

// client -> server (client code only)
if (ClientPlayNetworking.canSend(PingBlockPayload.TYPE)) {
    ClientPlayNetworking.send(new PingBlockPayload(pos, color));
}

// server -> one client
ServerPlayNetworking.send(serverPlayer, payload);

// server -> many: pick the narrowest PlayerLookup
PlayerLookup.tracking(entity);                 // players that see this entity
PlayerLookup.tracking(serverLevel, blockPos);  // players with this chunk loaded
PlayerLookup.around(serverLevel, vec3, radius);
PlayerLookup.level(serverLevel);
PlayerLookup.all(server);
  • canSend tells you whether the other side has your mod (i.e. registered a receiver for that type). Use it so a client-side feature degrades gracefully on servers without your mod, and so the server does not send packets to vanilla clients.
  • Prefer tracking/around over all: fewer packets, and players far away should not learn about things they cannot see.

Client/server separation

  • ClientPlayNetworking, Minecraft, LocalPlayer, ClientLevel -> src/client only.
  • ServerPlayNetworking, PayloadTypeRegistry, payload records -> src/main (common).
  • The logical server also runs inside the client (singleplayer). Never use "am I on the client jar" checks to decide logic; use level.isClientSide() for logical side.
  • A client-only mod ("environment": "client") can still talk to a server that has a companion mod; it must check canSend and work fine when the server has nothing.

Configuration-phase payloads (advanced)

Data the client needs before joining the world (e.g. server-side config to sync) can use the configuration phase: PayloadTypeRegistry.clientboundConfiguration() / serverboundConfiguration() with ServerConfigurationNetworking and a configuration task. Read the docs page for your version before using it - this area changed in several releases.

Common mistakes

  • Using the pre-1.20.5 Identifier + PacketByteBuf channel API on modern versions, or the Yarn names (CustomPayload, PacketCodec) in a Mojang-named project.
  • Using playS2C()/playC2S() on 26.1+ (renamed to clientboundPlay()/serverboundPlay()), or the new names on 1.21.x.
  • Registering the payload type only on one side, or only in the client initializer.
  • Registering receivers in a static initializer that the dedicated server also loads, while referencing client classes from it.
  • No server-side validation: trusting a client-sent position, entity id, amount, or "isCreative" flag. This is how dupes and grief exploits happen.
  • Doing heavy work or blocking I/O inside a handler; or, conversely, wrapping a server handler in server.execute(...) out of habit (it already runs on the server thread).
  • Sending to PlayerLookup.all every tick instead of sending on change to tracking players.
  • Sending per-tick packets with full state instead of deltas or change events.
  • Missing canSend check, causing errors or kicks when joining vanilla/other servers.
  • Reusing one payload record for both directions with different meanings. Make one payload per direction and purpose; name them clearly (...ServerboundPayload / ...ClientboundPayload is the docs' convention).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.