User Tools

Site Tools


tutorial:networking

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
tutorial:networking [2019/12/26 22:11] – Added Fudge's networking modmuss50tutorial:networking [2024/04/15 02:17] (current) – [Advanced Networking topics] solidblock
Line 1: Line 1:
-====== Communicating between the clients and the server ======+**Note: **This page has replaced the old networking page. 
 +It is recommended to use the new networking API described on this page. 
 +The old page can be [[tutorial:legacy:networking-v0|found here]].
  
-Say you wanted to emit an explosion particle whenever your block is destroyed. Emitting particles requires access to the ''%%ParticleManager%%'', which only exists on the ''%%MinecraftClient%%'' instance. Let's try doing that:+====== Networking ======
  
-<code java> +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. 
-public class MyBlock extends Block + 
-    @Override +===== Example: Why is networking important? ===== 
-    public void onBlockRemoved(BlockState before, World world, BlockPos posBlockState after, boolean bool) { + 
-        if (player !null) { +The importance of networking can be shown by a simple code example. This code should **NOT** be used and is here to explain why networking is important. 
-            // WARNING: BAD CODEDON'T EVER DO THIS! + 
-            MinecraftClient.getInstance().particleManager.addParticle( +Say you had a wand which highlights the block you are looking at to all nearby players. 
-                    ParticleTypes.EXPLOSIONpos.getX(), pos.getY()pos.getZ(), +<code java [enable_line_numbers="true"]
-                    0.0D, 0.0D, 0.0D +public class HighlightingWandItem extends Item 
-            ); +    public HighlightingWand(Item.Settings settings) { 
-        }+        super(settings) 
 +    } 
 + 
 +    public TypedActionResult<ItemStack> use(World world, PlayerEntity userHand hand) { 
 +        // Raycast and find the block the user is facing at 
 +        BlockPos target ... 
 + 
 +        // BAD CODEDON'T EVER DO THIS! 
 +        ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target)
 +        return super.use(worlduserhand);
     }     }
 } }
 </code> </code>
-If you go into try to break your block, you will see your particle works perfectly fine. Job's doneright? Well, try to [[testing-on-a-dedicated-server.md|run it on a dedicated server]]. You break your block, and...+ 
 +Upon testing, you will see the block you are facing at is highlighted and nothing crashes. 
 +Now you want to show the mod to your friendyou 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:
  
 <code> <code>
Line 23: Line 37:
 java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER
 </code> </code>
-==== What went wrong? ==== 
  
-There are 2 types of Minecraft jars. The //server// jar, which is run by the server that hosts a multiplayer game. And there is the //client// jar, which is run by every player who joins the server. Some classes, such as the ''%%MinecraftClient%%'', only exist in the client jar. And for good reason - the server can't play particles! It doesn't even have a screen to display anything on.+==== Why does the server crash? ====
  
-''%%onBlockedRemoved%%'' gets called by the server, which cannot do the client-only operations that ''%%MinecraftClient%%'' provides+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.
  
-  * in this case, playing particles. Since ''%%onBlockRemoved%%'' gets called by the server **only**,+==== How do I fix the crash? ====
  
-we will need to somehow tell the clients they need to play the particles.+In order to fix this issue, you need to understand how Minecraft communicates between the game client and dedicated server.
  
-In a single player settings this is easy, the client runs in the same process as the server,+[[Minecraft's Logical Sides|{{tutorial:sides.png?700}}]]
  
-so we can just access the ''%%MinecraftClient%%'' and be done with it.+The diagram above shows that the game client and dedicated server are separate systems bridged together using packetsThis 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. The key difference between the three types of connections that are shown in the table below:
  
-In multiplayer we will need to communicate between entirely different computers. This is where packets come in.+^ Connection Type               ^ Access to game client       ^ 
 +| Connected to Dedicated Server | None -> Server Crash        | 
 +| Connected over LAN            | Yes -> Not host game client | 
 +| Singleplayer (or LAN host)    | Yes -> Full access          |
  
-===== Server To Client (S2C) Packets =====+It may seem complicated to have communication with the server in three different ways. However, you don't need to communicate in three different ways with the game client. Since all three connection types communicate with the game client using packets, you only need to communicate with the game client like you are always running on a dedicated server. Connection to a server over LAN or Singleplayer can be also be treated like the server is a remote dedicated server; so your game client cannot directly access the server instance.
  
-Server To Client, or S2C packets for short, allow the server to tell the clients to execute predefined code with some data. The clients will first need to know what to execute, so we will tell them just that during [**client** mod initialization]($$$ LINK TO CLIENT INIT DOC+===== An introduction to networking =====
  
-(Note that the identifier we will put in the class of the common initializer so it can be accessed from both sides:)+To begin, we need to fix an issue with the example code shown above. Since we are using packets to communicate with the client, we want to make sure the packets are only sent when an action is initiated on the server.
  
-<code java> +==== Sending a packet to the game client ==== 
-public class ExampleMod implements ModInitializer + 
-    // Save the id of the packet so we can reference it later +<code java [enable_line_numbers="true", highlight_lines_extra="2,3"]
-    public static final Identifier PLAY_PARTICLE_PACKET_ID new Identifier("example""particle"); +    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) 
-}+        // Verify we are processing the use action on the logical server 
 +        if (world.isClient()) return super.use(world, user, hand); 
 + 
 +        // Raycast and find the block the user is facing at 
 +        BlockPos target ... 
 + 
 +        // BAD CODE: DON'T EVER DO THIS! 
 +        ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance()target); 
 +        return TypedActionResult.success(user.getStackInHand(hand)); 
 +    }
 </code> </code>
 +
 +Next, we need to send the packet to the game client. First, you need to define an ''Identifier'' used to identify your packet. For this example our Identifier will be ''wiki_example:highlight_block''. In order to send the packet to the game client, you need to specify which player's game client you want the packet to be received by. Since the action is occurring on the logical server, we may upcast the ''player'' to a ''ServerPlayerEntity''.
 <code java> <code java>
-public class ExampleClientInit implements ClientModInitializer +public class TutorialNetworkingConstants 
-    @Override +    // Save the id of the packet so we can reference it later 
-    public void onInitializeClient() { +    public static final Identifier HIGHLIGHT_PACKET_ID = new Identifier("wiki_example""highlight_block");
-        // We associate PLAY_PARTICLE_PACKET_ID with this callback, so the server can then use that id to execute the callback. +
-        ClientSidePacketRegistry.INSTANCE.register(ExampleMod.PLAY_PARTICLE_PACKET_ID, +
-                (packetContext, attachedData) -> { +
-                    // Client sided code +
-                    // Caution: don't execute code here just yet +
-                }); +
-    }+
 } }
 </code> </code>
-One thing we must make sure is that **we execute the code in the correct thread**. The packet callback is executed on the //networking thread//while interaction with the most things, such as the particle manager, must be done in the main thread. The ''%%packetContext%%'' that is passed to the callback contains an easy way to do that by calling ''%%getTaskQueue().execute%%'':+ 
 +To send the packet to the playerwe will use some of the methods inside of ''ServerPlayNetworking''. We will use the following method inside of that class:
  
 <code java> <code java>
-// [...] +public static void send(ServerPlayerEntity playerIdentifier channelNamePacketByteBuf buf) { 
-ClientSidePacketRegistry.INSTANCE.register(ExampleMod.PLAY_PARTICLE_PACKET_ID, +    ...
-        (packetContextattachedData-> packetContext.getTaskQueue().execute(() -> +
-            // For now we will use the player's position since we don't know (yet) how to use the BlockPos +
-            // in the onBlockRemoved callbackThis is explained in "passing information to packets". +
-            Vec3d pos = packetContext.getPlayer().getPos(); +
-            MinecraftClient.getInstance().particleManager.addParticle( +
-                    ParticleTypes.EXPLOSION, pos.getX(), pos.getY(), pos.getZ(), +
-                    0.0D, 0.0D, 0.0D +
-            ); +
-        }));+
 </code> </code>
-The only thing that is left is for the server to actually send the packet to clients. 
  
-<code java> +The player in this method is the player the packet will be sent to. The channel name is the ''Identifier'' you decided on earlier to identify your packetThe ''PacketByteBuf'' is what stores the data for the packet. We will return later to writing data to the packet's payload via the buf.
-public class MyBlock extends Block { +
-    @Override +
-    public void onBlockRemoved(BlockState before, World world, BlockPos pos, BlockState after, boolean bool) { +
-        // First we need to actually get hold of the players that we want to send the packets to. +
-        // A simple way is to obtain all players watching this position: +
-        Stream<PlayerEntity> watchingPlayers = PlayerStream.watching(world,pos); +
-        // Look at the other methods of `PlayerStream` to capture different groups of players.+
  
-        // We'll get to this later +Since we are not writing any data to the packet, for now, we will send the packet with an empty payloadA buf with an empty payload may be created using ''PacketByteBufs.empty()''.
-        PacketByteBuf passedData = new PacketByteBuf(Unpooled.buffer()); +
- +
-        // Then we'll send the packet to all the players +
-        watchingPlayers.forEach(player -> +
-                ServerSidePacketRegistry.INSTANCE.sendToPlayer(player,ExampleMod.PLAY_PARTICLE_PACKET_ID,passedData)); +
-        // This will work in both multiplayer and singleplayer! +
-    }+
  
-    // [...]+<code java [enable_line_numbers="true", highlight_lines_extra="2"]> 
 +    ...
 +    ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, PacketByteBufs.empty()); 
 +    return TypedActionResult.success(user.getHandStack(hand));
 } }
 </code> </code>
-If you join a dedicated server, go into first person and break the block, you will see an explosion particle was indeed emitted in your location. 
  
-===== Attaching data to packets =====+Though you have sent a packet to the game client, the game client cannot do anything with the packet since the client does not know how to receive the packet. Information on receiving a packet on the game client is shown below:
  
-But wait, we wanted the explosion to occur at the block's position, not the player's! For that we will need to send the block position to the packet. It's fairly simple. Write the data to the ''%%PacketByteBuf%%'':+==== Receiving a packet on the game client ====
  
-<code java> +To receive a packet from a server on the game client, your mod needs to specify how it will handle the incoming packet. 
-Stream<PlayerEntity> watchingPlayers = /*...*/+In your client entrypoint, you will register the receiver for your packet using ''ClientPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler)''
  
-// Pass the `BlockPos` information +The ''Identifier'' should match the same Identifier you use to send the packet to the clientThe ''ChannelHandler'' is the functional interface you will use to implement how the packet is handled**Note the ''ChannelHandler'' should be the one that is a nested interface of ''ClientPlayNetworking''**
-PacketByteBuf passedData = new PacketByteBuf(Unpooled.buffer()); +
-passedData.writeBlockPos(pos);  // <---+
  
-watchingPlayers.forEach(player -> /*...*/);+The example below implements the play channel handler as a lambda: 
 + 
 +<code java [enable_line_numbers="true"]> 
 +ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> 
 +    ... 
 +});
 </code> </code>
-And then retrieve it in the packet callback, **but make sure you do it in the network thread**. 
  
-<code java> +However, you cannot draw the highlight box immediately. This is because the receiver is called on the netty event loop. The event loop runs on another thread, and you must draw the highlight box on the render thread. 
-ClientSidePacketRegistry.INSTANCE.register(ExampleMod.PLAY_PARTICLE_PACKET_ID, + 
-        (packetContextattachedData) -> { +In order to draw the highlight box, you need to schedule the task on the game client. This may be done with the ''client'' field that is provided in the channel handler. Typically you will run the task on the client by using the ''execute'' method: 
-            // Get the BlockPos we put earlier, in the networking thread + 
-            BlockPos pos = attachedData.readBlockPos(); +<code java [enable_line_numbers="true"]
-            packetContext.getTaskQueue().execute(() -> { +ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, bufresponseSender) -> { 
-                // Use the pos in the main thread +    client.execute(() -> { 
-                MinecraftClient.getInstance().particleManager.addParticle( +        // Everything in this lambda is run on the render thread 
-                        ParticleTypes.EXPLOSIONpos.getX(), pos.getY(), pos.getZ(), +        ClientBlockHighlighting.highlightBlock(clienttarget); 
-                        0.0D, 0.0D, 0.0D +    }); 
-                ); +});
-            }); +
-        });+
 </code> </code>
-Try it out in a dedicated server environment. 
  
-If you want to send additional data, just call additional ''%%read%%'' and ''%%write%%'' methodsbut make sure you do them in the correct order:+You may have noticed you are not told where the block to highlight is. You can write this data to the packet byte buf. Instead of sending ''PacketByteBufs.empty()'' to the game client in your item'''use'' method, instead, you will create a new packet byte buf and send that instead.
  
-<code java> +<code java [enable_line_numbers="true"]
-// Write +PacketByteBuf buf = PacketByteBufs.create();
-passedData.writeBlockPos(pos);   +
-passedData.writeInt(123); +
-passedData.writeString("hello");+
 </code> </code>
-<code java> + 
-// Read +Next, you need to write the data to the packet byte buf. It should be noted that you must read data in the same order you write it. 
-(packetContextattachedData) -> { + 
-    // Read in the correct, networking thread +<code java [enable_line_numbers="true", highlight_lines_extra="3"]
-    BlockPos pos attachedData.readBlockPos(); +PacketByteBuf buf = PacketByteBufs.create(); 
-    int num = attachedData.readInt(); + 
-    String str = attachedData.readString(); +buf.writeBlockPos(target); 
-    packetContext.getTaskQueue().execute(() -> { +</code> 
-        /+ 
-         * Use num and str in the correct, main thread +Afterwards, you will send the ''buf'' field through the ''send'' method. 
-         */ + 
-        System.out.println(num); +To read this block position on the game client, you can use ''PacketByteBuf.readBlockPos()''
-        System.out.println(str);+ 
 +You should read all data from the packet on the network thread before scheduling a task to occur on the client thread. You will get errors related to the ref count if you try to read data on the client thread. If you must read data on the client threadyou need to ''retain()'' the data and then read it on the client thread. If you do ''retain()'' the data, make sure you ''release()'' the data when you no longer need it. 
 + 
 +In the end, the client's handler would look like this: 
 +<code java [enable_line_numbers="true"]> 
 +ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> { 
 +    // Read packet data on the event loop 
 +    BlockPos target buf.readBlockPos(); 
 + 
 +    client.execute(() -> { 
 +        // Everything in this lambda is run on the render thread 
 +        ClientBlockHighlighting.highlightBlock(client, target);
     });     });
 }); });
 </code> </code>
-===== Client To Server (C2S) Packets ===== 
  
-Client to server packets follow the same principles. Some things can only be done from the server, such as changing the world in a way that affects other players, but you want them to be triggered by a client-only action, such as holding a keybinding. One key difference is that **you must validate what you receive in the ''%%PacketByteBuf%%''**.+==== Sending packets to the server and receiving packets on the server ====
  
-In this example we will replace block with diamond when it is right clicked when keybinding is heldusing C2S packetIf you want to know how to use hotkeys specifically, refer to the [hotkeys tutorial]($$$ LINK TO KEYBINDINGS TUTORIAL+Sending packets to server and receiving packet on the server is very similar to how you would on the client. Howeverthere are few key differences.
  
-As before we'll define an identifier for our packet:+Firstly sending a packet to the server is done through ''ClientPlayNetworking.send''. Receiving a packet on the server is similar to receiving a packet on the client, using the ''ServerPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler)'' method. The ''ChannelHandler'' for the server networking also passes the ''ServerPlayerEntity'' (player) who sent the packet through the ''player'' parameter.
  
-<code java> +===== The concept of tracking and why only you see the highlighted block ===== 
-public class ExampleMod implements ModInitializer + 
-    public static final Identifier TURN_TO_DIAMOND_PACKET_ID new Identifier("example""diamond"); +Now that the highlighting wand properly uses networking so the dedicated server does not crash, you invite your friend back on the server to show off the highlighting wand. You use the wand and the block is highlighted on your client and the server does not crash. However, your friend does not see the highlighted block. This is intentional with the code that you already have here. To solve this issue let us take a look at the item's use code: 
-}+ 
 +<code java [enable_line_numbers="true"]
 +    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) 
 +        // Verify we are processing the use action on the logical server 
 +        if (world.isClient()) return super.use(world, user, hand); 
 + 
 +        // Raycast and find the block the user is facing at 
 +        BlockPos target ... 
 +        PacketByteBuf buf = PacketByteBufs.create(); 
 +        buf.writeBlockPos(target); 
 + 
 +        ServerPlayNetworking.send((ServerPlayerEntity) userTutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf); 
 +        return TypedActionResult.success(user.getHandStack(hand)); 
 +    }
 </code> </code>
-Now we will send the packet when the block is right-clicked and the keybinding is held. We can only check the keybinding on the client, so we must only execute the code here on the client: 
  
-<code java> +You may notice the item will only send the packet to the player who used the item. To fix this, we can use the utility methods in ''PlayerLookup'' to get all the players who can see the highlighted block. 
-public class MyBlock extends Block { + 
-    @Override +Since we know where the highlight will occur, we can use ''PlayerLookup.tracking(ServerWorld world, BlockPos pos)'' to get a collection of all players who can see that position in the world. Then you would simply iterate through all players in the returned collection and send the packet to each player: 
-    public boolean activate(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult result) { + 
-        if (world.isClient()) +<code java [enable_line_numbers="true", highlight_lines_extra="10,11,12,13"]
-            // We are in the client nowcan't do anything server-sided+    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) { 
 +        // Verify we are processing the use action on the logical server 
 +        if (world.isClient()) return super.use(worlduser, hand);
  
-            // See the keybindings tutorial for information about this if statement (the keybindings tutorial calls this variable "keyBinding") +        // Raycast and find the block the user is facing at 
-            if(ExampleClientInit.EXAMPLE_KEYBINDING.isPressed()){ +        BlockPos target = ... 
-                // Pass the `BlockPos` information +        PacketByteBuf buf PacketByteBufs.create(); 
-                PacketByteBuf passedData new PacketByteBuf(Unpooled.buffer()); +        buf.writeBlockPos(target);
-                passedData.writeBlockPos(pos); +
-                // Send packet to server to change the block for us +
-                ClientSidePacketRegistry.INSTANCE.sendToServer(ExampleMod.TURN_TO_DIAMOND_PACKET_ID, passedData); +
-            }+
  
 +        // Iterate over all players tracking a position in the world and send the packet to each player
 +        for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
 +            ServerPlayNetworking.send(player, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
         }         }
-        return true;+ 
 +        return TypedActionResult.success(user.getHandStack(hand));
     }     }
 +</code>
 +
 +After this change, when you use the wand, your friend should also see the highlighted block on their own client.
 +
 +
 +===== Networking in 1.20.5 =====
 +Since 1.20.5, the logic of networking is totally rewrited. In 1.20.5, you have to define a custom ''Payload''. First, define the payload that includes a ''BlockPos'':
 +
 +<code java>
 +public record BlockHighlightPayload(BlockPos blockPos) implements CustomPayload {
 +  public static final Id<BlockHighlightPayload> ID = CustomPayload.id("tutorial:block_highlight");
 +  public static final PacketCodec<PacketByteBuf, BlockHighlightPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, BlockHighlightPayload::blockPos, BlockHighlightPayload::new);
 +  // or you can also write like this:
 +  // public static final PacketCodec<PacketByteBuf, BlockHighlightPayload> CODEC = PacketCodec.of((value, buf) -> buf.writeBlockPos(value.blockPos), buf -> new BlockHighlightPayload(buf.readBlockPos()));
 +
 +  @Override
 +  public Id<? extends CustomPayload> getId() {
 +    return ID;
 +  }
 } }
 </code> </code>
-And then we receive the packet on the server side by registering it in the common mod initializer. Make sure to **take data in the IO thread and use it in the main thread**, and to **validate the received data**: 
  
 +And then, register the receiver like this:
 <code java> <code java>
-public class ExampleMod implements ModInitializer +PayloadTypeRegistry.playS2C().register(BlockHighlightPayload.ID, BlockHighlightPayload.CODEC); 
-    public static final Identifier TURN_TO_DIAMOND_PACKET_ID = new Identifier("example""diamond");+ClientPlayNetworking.registerGlobalReceiver(BlockHighlightPayload.ID, (payload, context) -> { 
 +  context.client().execute(() -> 
 +    ClientBlockHighlighting.highlightBlock(clienttarget); 
 +  }); 
 +}); 
 +</code>
  
-    @Override +Now, on the server side, you can send the packet to players like this: 
-    public void onInitialize() { +<code java> 
-        ServerSidePacketRegistry.INSTANCE.register(TURN_TO_DIAMOND_PACKET_ID(packetContextattachedData-> +    public TypedActionResult<ItemStack> use(World worldPlayerEntity userHand hand) { 
-            // Get the BlockPos we put earlier in the IO thread +        if (world.isClient()) return super.use(world, user, hand);
-            BlockPos pos = attachedData.readBlockPos(); +
-            packetContext.getTaskQueue().execute(() -> { +
-                // Execute on the main thread+
  
-                // ALWAYS validate that the information received is valid in a C2S packet! +        // ...
-                if(packetContext.getPlayer().world.isHeightValidAndBlockLoaded(pos)){ +
-                    // Turn to diamond +
-                    packetContext.getPlayer().world.setBlockState(pos, Blocks.DIAMOND_BLOCK.getDefaultState()); +
-                }+
  
-            }); +        for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) { 
-        });+            ServerPlayNetworking.send(player, new BlockHighlightPayload(blockPos)); 
 +        } 
 + 
 +        return TypedActionResult.success(user.getHandStack(hand));
     }     }
-} 
 </code> </code>
tutorial/networking.1577398318.txt.gz · Last modified: 2019/12/26 22:11 by modmuss50