Packet Handling

From Minecraft Forge
Jump to: navigation, search

How-To Icon.png

This is a How-To guide or Tutorial detailing a practice or process for Minecraft Forge or related software.

Generic Mod

Havvy Generic Mod Banner Final.png

March 27, 2014 Updated Basic Items to 1.7.x --Jwosty

February 13, 2014 1.7 update happen and I fix up some of the stuff so far:

But it has one part not working yet

--Chibill

July 5, 2013

1.6.1 came out so I have fixed up some of the basic tutorials to go with it.

Ones that are updated include:

-- Mew

March 13, 2013

1.5 came out, so I fixed the methods to account for this (mostly the Icons/Textures tutorial).

--Squawkers13

December 13th, 2012

Alright, Forge made their installation process really simple and also made proper packages, removing the common/src distinction. I rewrote most of Basic Modding to account for this update. Overall though, this simplifies the explanations, but imports changed in all tutorials. Next up, I need to finish the world gen tutorial.

With the Basic Modding tutorial being mostly rewritten, if anything is confusing about it now, please tell me ASAP!

December 3rd, 2012

I finally finished the tutorial on Plants I started a month ago. I also fixed a minor compilation bug in Packet Handling. I reorganized the Generic Ore section by adding a Dropping Ore section. I'm starting to use a General Knowledge template where the background is grey. The knowledge in those boxes are general facts that are not specific to whatever thing we are implementing. I updated the Textures and Icons section to say that the image can be any scalar of 16x16 pixels where 256x256 will have each icon be 16x16 in size.


Contents

Goals

  • To understand packet handlers.
  • To send data from the server to all players.

Prerequisites

  • Basic modding
  • Basic blocks — optional, but we are going to use a block.
  • A reason for using packets. There are many reasons one would use packets, and the information here is not going to be useful unless you actually want to send packets.

Why Use Packets?

Minecraft is built using the standard client/server model. The client displays graphical updates to the player, and takes input from the player and passes it to the server. The server maintains the state of the world, and updates with each tick. Without some form of communication, the client has no clue what is happening on the server, and the client goes out of sync. While the client tries to imitate the server, running a lot of the same code, when actual decisions are made, say via randomness or another player, the server must tell each client. Furthermore, for security reasons, actions that change the world, like crafting or damage entities should be checked on the server. Packets are the communication channel of the Internet, and Minecraft uses its own NBT Packets for communication.

One good place for sending packets is in TileEntities and their respective GUIs, but that topic is complex and the examples here will instead be trivial.

What are Packets?

Packets are the transport layer between the client and server. If the world wants to change the worldview of the client, it has to send a packet. If the player wants to change the world model of the server, the player's client has to send a packet. It does not matter whether the client is on the same computer of the server or if the server is on the other side of the globe, packets still have to be created and sent.

In the case that the player is playing single player, then the packet will just be sent from the server world to the client world without going through the network, but if the player is playing multiplayer, then the packet has to travel across the network, which takes time.

Packets are sent across named channels, which are Strings with a maximal length of 16 characters. From how they are sent, I do not believe they are unique per mod, so try to use unique names for the channels. Which channels you listen to has to be registered.

Packets are handled by an IPacketHandler, which also has to be registered.

A Note on Security

The information stored in a packet from the player should not be trusted on blind faith. It is possible for clients to purposely send incorrect data as a way of cheating. The only thing you do know from a packet coming from a player is that the packet came from that player. Because of this, it may be necessary to validate your data. If you do not, and hackers start abusing your mod, you are the one who let it happen.

On the other side, modified clients do not necessarily have the features you expect. The only thing the client does is display the world, and if the client is modified to remove one way of displaying the world, then the modification won't make sense. For example, if the experience packet is sent to the player, but the experience bar has been modded to not show, then the player won't see the change in experience unless there is another handler of the experience packet. Ultimately, there's very little you can do about this, and in most cases, this will not give an undue advantage to the player. Just know that it can happen.

Scaffolding

Now that we know why we want to send packets, and what they are, let us set up the scaffolding for sending packets in a mod.

First up, is the class implementing IPacketHandler. Since only one handler needs to be made for all packets you'll listen to, we will call this class PacketHandler.

package tutorial.generic;

import net.minecraft.network.INetworkManager;
import net.minecraft.network.packet.Packet250CustomPayload;
import cpw.mods.fml.common.network.IPacketHandler;
import cpw.mods.fml.common.network.Player;

public class PacketHandler implements IPacketHandler {

        @Override
        public void onPacketData(INetworkManager manager,
                        Packet250CustomPayload packet, Player playerEntity) {
                // TODO Auto-generated method stub
        }

}

Next up is registration of the PacketHandler and the channels we want to use.

For the generic mod, we'll prefix our channels with "Generic".

The packet we'll be using will be to send random ints from the server to all the players. The channel we'll send this over will be called "GenericRandom".

Registration of channels and PacketHandlers happens in the @NetworkMod annotation. So, in the base mod, change the annotation to the following.

@NetworkMod(clientSideRequired=true, serverSideRequired=false,
        channels={"GenericRandom"}, packetHandler = PacketHandler.class)

The channels property takes a String[] of the channels to listen on.

The packetHandler property takes a class reference to a class that implements IPacketHandler.

GenericRandom

With the scaffolding out of the way, let's find a place to send the packet.

How about when a player right clicks on a block? We'll have the server send randomness to all the players and the player send randomness to the server. When the randomness is obtained, it'll print it out in the console.

RandomBlock

The only part of this block we care about is, is that onBlockActivated() is called when a player right clicks on it. In fact, it is called twice. It is called on both the client side and server side. This actually makes it a terrible location to send packets, but a good place to demonstrate how to use them.

package tutorial.generic;

import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.block.material.Material;
import net.minecraft.world.World;

public class RandomBlock extends Block {
        public RandomBlock (int id, int texture) {
                super(id, texture, Material.ground);
                setBlockName("random");
                setCreativeTab(CreativeTabs.tabBlock);
        }
       
        @Override
        public boolean onBlockActivated(World world, int bx, int by, int bz,
                        EntityPlayer player, int side, float px, float py, float pz) {
                return false;
        }

        @Override
        public String getTextureFile () {
                return CommonProxy.BLOCK_PNG;
        }
}

Register this block in the Generic base mod file. Add this field to the class, and the next block of code to the load method.

public static final Block randomBlock = new RandomBlock(503, 2);
GameRegistry.registerBlock(randomBlock);
LanguageRegistry.addName(randomBlock, "Random Block");

In the onBlockActivated method, we are going to build the packet, determine which side of the client/server divide we are on, and send the packet.

Building the Packet

A packet has three fields: channel, data, and length.

The channel is a String and is the channel the packet is sent under. Make sure it's one you are listening to, or else it'll get dropped from the perspective of your mod.

The length is the integer number of bytes the packet is. A char is one byte. An int and float is four bytes. Double and long are 8 bytes. Strings have a length dependent upon the number of characters in the String. We are passing two random ints, so our length will be 8 bytes.

The data field is a ByteArrayOutputStream. This is a standard Java class, and extends OutputStream. The building of these streams is a complicated mess of names, and Java tutorials can explain how it is done. The following code can be used though.

The packet is an instance of Packet250CustomPayload. This packet is the packet that doesn't have a defined shape, size, or usage.

Here's how it is built.

Random random = new Random();
int randomInt1 = random.nextInt();
int randomInt2 = random.nextInt();

ByteArrayOutputStream bos = new ByteArrayOutputStream(8);
DataOutputStream outputStream = new DataOutputStream(bos);
try {
        outputStream.writeInt(randomInt1);
        outputStream.writeInt(randomInt2);
} catch (Exception ex) {
        ex.printStackTrace();
}

Packet250CustomPayload packet = new Packet250CustomPayload();
packet.channel = "GenericRandom";
packet.data = bos.toByteArray();
packet.length = bos.size();

Since we are sending the same data on both sides, we do not have to build a packet for each one.

Detecting Side

There are many ways of detecting which side the player is on. Use whichever is most convenient for you, but for this block, we'll use the getEffectiveSide method. This method, and two others are described here.

With the World

If you have access to a world object, the world has an isRemote method which is true if it is on the client side and false if it is on the server side.

With the Player

If the player is an instance of EntityClientPlayerMP, then the code is being ran on the client side. If the instance of the player is an instance of EntityPlayerMP, then the code is being ran on the server side.

getEffectiveSide

Side.BUKKIT
The Side enum has a BUKKIT value. Forge and Bukkit do not work together, and this side will never be returned by Forge code.

In the latest version of Forge (4.1.x.x), we got a utility method for detecting the side called FMLCommonHandler.getEffectiveSide(). It's not static, so we'll need to get the instance first. The method returns a Side, which is either Side.SERVER or Side.CLIENT.

Side side = FMLCommonHandler.instance().getEffectiveSide();
if (side == Side.SERVER) {
        // We are on the server side.
        EntityPlayerMP player = (EntityPlayerMP) playerEntity;
} else if (side == Side.CLIENT) {
        EntityClientPlayerMP player = (EntityClientPlayerMP) playerEntity;
        // We are on the client side.
} else {
        // We have an errornous state!
}
Player Class
How do I know which class the player is at those points? I just System.out.println(playerEntity) anywhere I want to know what class it is. I generally do it with any method that gives me a player, to see how many times its getting called and on which sides.

Sending the Packet

Player -> Server

With our instance of EntityClientPlayerMP, we have a field sendQueue which is a NetClientHandler. The NetClientHandler has a method.

netClientHandler.addToSendQueue(Packet250CustomPayload packet)

This method sends the packet to the server at some point in the near future. In the client section of the onBlockActivated() method, add the following line.

player.sendQueue.addToSendQueue(packet);

Now, when the player right clicks the block, it'll send the randomness to the server.

Alternative:

We can use the PacketDispatcher:

PacketDispatcher.sendPacketToServer(Packet packet);

Server -> Player

If you want to send a packet from the server to the client you can use the following line of code.

PacketDispatcher.sendPacketToPlayer(packet, (Player)player);

This is a static method so you don't need to make an instance of PacketDispatcher.

The variable player has to be an instance of Player so in most cases you have to typecast the variable to Player.

Handling the Packet

All of our packets get called through onPacketData, so instead of having any specific logic in that method, let's just check what the channel is, and pass the packet and player to its own handling method. We only have one channel, so this is pretty simple. If you have more than one channel, just chain else-ifs.

String Switches
In Java 7, switches can use Strings. If you want to require Java 7, you can use them instead of chaining else-ifs.
@Override
public void onPacketData(INetworkManager manager,
                Packet250CustomPayload packet, Player player) {
       
        if (packet.channel.equals("GenericRandom")) {
                handleRandom(packet);
        }
}

Since the thing we are doing, printing out the randomness to the console, will already go to the correct console based on where the code is running, we don't need the player. We just need the packet. So we just send the packet.

Here's the handleRandom() method.

private void handleRandom(Packet250CustomPayload packet) {
        DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(packet.data));
       
        int randomInt1;
        int randomInt2;
       
        try {
                randomInt1 = inputStream.readInt();
                randomInt2 = inputStream.readInt();
        } catch (IOException e) {
                e.printStackTrace();
                return;
        }
       
        System.out.println(randomInt1 + " " + randomInt2);
}

Reading from an inputStream is, just like writing to an outputStream, a Java concept, and tutorials for it exist out on the web. Still, this is the general pattern.

Finishing Up

This is hard to test without setting up a server, and connecting to it. If you do it on single player, you'll get two sets of random ints in your console, since both the server and client share a console.

As always, here's a full code listing.

Generic base mod Class

package tutorial.generic;

// This Import list will grow longer with each additional tutorial.
// It's not pruned between full class postings, unlike other tutorial code.
import net.minecraft.block.Block;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.block.material.Material;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.MinecraftForge;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.Init;
import cpw.mods.fml.common.Mod.Instance;
import cpw.mods.fml.common.Mod.PostInit;
import cpw.mods.fml.common.Mod.PreInit;
import cpw.mods.fml.common.SidedProxy;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.network.NetworkMod;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.common.registry.LanguageRegistry;

@Mod(modid="Generic", name="Generic", version="0.0.0")
@NetworkMod(clientSideRequired=true, serverSideRequired=false,
        channels={"GenericRandom"}, packetHandler = PacketHandler.class)
public class Generic {
        public static final Block randomBlock = new RandomBlock(503, 2);
       
        @Instance("Generic")
        public static Generic instance;
       
        @SidedProxy(clientSide="tutorial.generic.client.ClientProxy",
                        serverSide="tutorial.generic.CommonProxy")
        public static CommonProxy proxy;
       
        @PreInit
        public void preInit(FMLPreInitializationEvent event) {
                // Stub Method
        }
       
        @Init
        public void load(FMLInitializationEvent event) {
                proxy.registerRenderers();
               
                GameRegistry.registerBlock(randomBlock);
                LanguageRegistry.addName(randomBlock, "Random Block");
        }
       
        @PostInit
        public void postInit(FMLPostInitializationEvent event) {
                // Stub Method
        }
}

PacketHandler

package tutorial.generic;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;

import net.minecraft.network.INetworkManager;
import net.minecraft.network.packet.Packet250CustomPayload;

import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Side;
import cpw.mods.fml.common.network.IPacketHandler;
import cpw.mods.fml.common.network.Player;

public class PacketHandler implements IPacketHandler {

        @Override
        public void onPacketData(INetworkManager manager,
                        Packet250CustomPayload packet, Player player) {
               
                if (packet.channel.equals("GenericRandom")) {
                        handleRandom(packet);
                }
        }
       
        private void handleRandom(Packet250CustomPayload packet) {
                DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(packet.data));
               
                int randomInt1;
                int randomInt2;
               
                try {
                        randomInt1 = inputStream.readInt();
                        randomInt2 = inputStream.readInt();
                } catch (IOException e) {
                        e.printStackTrace();
                        return;
                }
               
                System.out.println(randomInt1 + " " + randomInt2);
        }

}

RandomBlock

package tutorial.generic;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.util.Random;

import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Side;
import net.minecraft.block.Block;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.client.entity.EntityClientPlayerMP;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.block.material.Material;
import net.minecraft.network.packet.Packet250CustomPayload;
import net.minecraft.world.World;

public class RandomBlock extends Block {
        public RandomBlock (int id, int texture) {
                super(id, texture, Material.ground);
                setBlockName("random");
                setCreativeTab(CreativeTabs.tabBlock);
        }
       
        @Override
        public boolean onBlockActivated(World world, int bx, int by, int bz,
                        EntityPlayer playerEntity, int unknown, float px, float py, float pz) {
                Random random = new Random();
                int randomInt1 = random.nextInt();
                int randomInt2 = random.nextInt();
               
                ByteArrayOutputStream bos = new ByteArrayOutputStream(8);
                DataOutputStream outputStream = new DataOutputStream(bos);
                try {
                        outputStream.writeInt(randomInt1);
                        outputStream.writeInt(randomInt2);
                } catch (Exception ex) {
                        ex.printStackTrace();
                }
               
                Packet250CustomPayload packet = new Packet250CustomPayload();
                packet.channel = "GenericRandom";
                packet.data = bos.toByteArray();
                packet.length = bos.size();
               
                Side side = FMLCommonHandler.instance().getEffectiveSide();
                if (side == Side.SERVER) {
                        // We are on the server side.
                        EntityPlayerMP player = (EntityPlayerMP) playerEntity;
                } else if (side == Side.CLIENT) {
                        // We are on the client side.
                        EntityClientPlayerMP player = (EntityClientPlayerMP) playerEntity;
                        player.sendQueue.addToSendQueue(packet);
                } else {
                        // We are on the Bukkit server.
                }
               
                return false;
        }

        @Override
        public String getTextureFile () {
                return CommonProxy.BLOCK_PNG;
        }
}

What's Next

If you have a lot of channels, you can use a HashMap.

Currently this is the end of this path. Go back to the table of contents for more tutorials.

Personal tools
Namespaces
Variants
Actions
Navigation
tutorials
Toolbox