🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21.4
This page is written for version:
1.21.4
Networking in Minecraft is used so the client and server can communicate with each other. Networking is a broad topic, so this page is split up into a few categories.
The importance of networking can be shown by a simple code example.
WARNING
Below code is for demonstration purposes only.
Say you had a Wand which highlights the block you're looking, which will be visible to all nearby players:
public class HighlightingWandItem extends Item {
public HighlightingWandItem(Item.Settings settings) {
super(settings);
}
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
BlockPos target = ...
// BAD CODE: DON'T EVER DO THIS!
ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
return super.use(world, user, hand);
}
}
Upon testing, you will see a lightning bolt being summoned and nothing crashes. Now you want to show the mod to your friend, you boot up a dedicated server and invite your friend on with the mod installed.
You use the item and the server crashes. You will probably notice in the crash log an error similar to this:
[Server thread/FATAL]: Error executing task on Server
java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER
The code calls logic only present on the client distribution of the Minecraft. The reason for Mojang distributing the game in this way is to cut down on the size of the Minecraft Server JAR file. There isn't really a reason to include an entire rendering engine when your own machine will render the world.
In a development environment, client-only classes are indicated by the @Environment(EnvType.CLIENT)
annotation.
To fix this issue, you need to understand how the game client and dedicated server communicate:
The diagram above shows that the game client and dedicated server are separate systems, bridged together using packets. Packets can contain data which we refer to as the payload.
This packet bridge does not only exist between a game client and dedicated server, but also between your client and another client connected over LAN. The packet bridge is also present even in singleplayer. This is because the game client will spin up a special integrated server instance to run the game on.
Connection to a server over LAN or singleplayer can also be treated like the server is a remote, dedicated server; so your game client can't directly access the server instance.
This can be done by creating a Java Record
with a BlockPos
parameter that implements CustomPayload
.
public record SummonLightningS2CPayload(BlockPos pos) implements CustomPayload {
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
At the same time, we've defined:
Identifier
used to identify our packet's payload. For this example our identifier will be fabric-docs-reference:summon_lightning
.public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
CustomPayload.Id
to uniquely identify this custom payload. We will be referencing this ID in both our common and client code.public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
PacketCodec
so that the game knows how to serialize/deserialize the contents of the packet.public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
We have also overridden getId
to return our payload ID.
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
Before we send a packet with our custom payload, we need to register it.
INFO
S2C
and C2S
are two common suffixes that mean Server-to-Client and Client-to-Server respectively.
This can be done in our common initializer by using PayloadTypeRegistry.playS2C().register
which takes in a CustomPayload.Id
and a PacketCodec
.
PayloadTypeRegistry.playS2C().register(SummonLightningS2CPayload.ID, SummonLightningS2CPayload.CODEC);
A similar method exists to register client-to-server payloads: PayloadTypeRegistry.playC2S().register
.
To send a packet with our custom payload, we can use ServerPlayNetworking.send
which takes in a ServerPlayerEntity
and a CustomPayload
.
Let's start by creating our Lightning Tater item. You can override use
to trigger an action when the item is used. In this case, let's send packets to the players in the server world.
public class LightningTaterItem extends Item {
public LightningTaterItem(Settings settings) {
super(settings);
}
@Override
public ActionResult use(World world, PlayerEntity user, Hand hand) {
if (world.isClient()) {
return ActionResult.PASS;
}
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
return ActionResult.SUCCESS;
}
}
Let's examine the code above.
We only send packets when the action is initiated on the server, by returning early with a isClient
check:
if (world.isClient()) {
return ActionResult.PASS;
}
We create an instance of the payload with the user's position:
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
Finally, we get the players in the server world through PlayerLookup
and send a packet to each player.
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
INFO
Fabric API provides PlayerLookup
, a collection of helper functions that will look up players in a server.
A term frequently used to describe the functionality of these methods is "tracking". It means that an entity or a chunk on the server is known to a player's client (within their view distance) and the entity or block entity should notify tracking clients of changes.
Tracking is an important concept for efficient networking, so that only the necessary players are notified of changes by sending packets.
To receive a packet sent from a server on the client, you need to specify how you will handle the incoming packet.
This can be done in the client initializer, by calling ClientPlayNetworking.registerGlobalReceiver
and passing a CustomPayload.Id
and a PlayPayloadHandler
, which is a Functional Interface.
In this case, we'll define the action to trigger within the implementation of PlayPayloadHandler
implementation (as a lambda expression).
ClientPlayNetworking.registerGlobalReceiver(SummonLightningS2CPayload.ID, (payload, context) -> {
ClientWorld world = context.client().world;
if (world == null) {
return;
}
BlockPos lightningPos = payload.pos();
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
});
Let's examine the code above.
We can access the data from our payload by calling the Record's getter methods. In this case payload.pos()
. Which then can be used to get the x
, y
and z
positions.
BlockPos lightningPos = payload.pos();
Finally, we create a LightningEntity
and add it to the world.
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
Now, if you add this mod to a server and when a player uses our Lightning Tater item, every player will see lightning striking at the user's position.
Just like sending a packet to the client, we start by creating a custom payload. This time, when a player uses a Poisonous Potato on a living entity, we request the server to apply the Glowing effect to it.
public record GiveGlowingEffectC2SPayload(int entityId) implements CustomPayload {
public static final Identifier GIVE_GLOWING_EFFECT_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "give_glowing_effect");
public static final CustomPayload.Id<GiveGlowingEffectC2SPayload> ID = new CustomPayload.Id<>(GIVE_GLOWING_EFFECT_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, GiveGlowingEffectC2SPayload> CODEC = PacketCodec.tuple(PacketCodecs.INTEGER, GiveGlowingEffectC2SPayload::entityId, GiveGlowingEffectC2SPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
We pass in the appropriate codec along with a method reference to get the value from the Record to build this codec.
Then we register our payload in our common initializer. However, this time as Client-to-Server payload by using PayloadTypeRegistry.playC2S().register
.
PayloadTypeRegistry.playC2S().register(GiveGlowingEffectC2SPayload.ID, GiveGlowingEffectC2SPayload.CODEC);
To send a packet, let's add an action when the player uses a Poisonous Potato. We'll be using the UseEntityCallback
event to keep things concise.
We register the event in our client initializer, and we use isClient()
to ensure that the action is only triggered on the logical client.
UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
if (!world.isClient()) {
return ActionResult.PASS;
}
ItemStack usedItemStack = player.getStackInHand(hand);
if (entity instanceof LivingEntity && usedItemStack.isOf(Items.POISONOUS_POTATO) && hand == Hand.MAIN_HAND) {
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
ClientPlayNetworking.send(payload);
return ActionResult.SUCCESS;
}
return ActionResult.PASS;
});
We create an instance of our GiveGlowingEffectC2SPayload
with the necessary arguments. In this case, the network ID of the targeted entity.
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
Finally, we send a packet to the server by calling ClientPlayNetworking.send
with the instance of our GiveGlowingEffectC2SPayload
.
ClientPlayNetworking.send(payload);
This can be done in the common initializer, by calling ServerPlayNetworking.registerGlobalReceiver
and passing a CustomPayload.Id
and a PlayPayloadHandler
.
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectC2SPayload.ID, (payload, context) -> {
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
livingEntity.addStatusEffect(new StatusEffectInstance(StatusEffects.GLOWING, 100));
}
});
INFO
It is important that you validate the content of the packet on the server side.
In this case, we validate if the entity exists based on its network ID.
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
Additionally, the targeted entity has to be a living entity, and we restrict the range of the target entity from the player to 5.
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
Now when any player tries to use a Poisonous Potato on a living entity, the glowing effect will be applied to it.