Advanced Modding - Entity Properties

Goal

I want to show you how to add additional properties to vanilla (or modded) entities, for instance to the player(s).

Difficulty


5/10 - Mediocre Difficult

Prerequisites

Forge Version

This Tutorial was created with Forge 11.14.3.1450 for Minecraft 1.8. If anything doesn't work with other versions, please contact me!

Version

The Packet Handler that is needed for this tutorial requires Miner's Basic. Check out the corresponding tutorial to see which version you need!


What are Entity Properties?

What I refer to as Entity Properties or Entity Data is basically a system by MinecraftForge that allows modders to add new variables to existing entities, mostly to the player. This can be used for new values for the player, like Mana, or for a new achievement system.

Entity Properties are extremely useful and probably more often needed than you think you'd need them.

How to create Entity Properties?

For out Entity Properties we need, of course, a new class.

This time, it needs to implement IExtendedEntityProperties. I called mine PlayerData here, because I'll use it only for players, however you can create properties for any kind of entity. You then just need to change the class for the variables and parameters.

PlayerData.java:
package com.bedrockminer.tutorial.entitydata;

import net.minecraft.entity.Entity;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import net.minecraftforge.common.IExtendedEntityProperties;

public class PlayerData implements IExtendedEntityProperties {

    @Override
    public void saveNBTData(NBTTagCompound nbt) {
    }

    @Override
    public void loadNBTData(NBTTagCompound nbt) {
    }

    @Override
    public void init(Entity entity, World world) {
    }
}

Now, we add a variable to store a reference to the player to which we register the extended properties.

This variable will be final and it will be set in the constructor.

PlayerData.java:
public class PlayerData implements IExtendedEntityProperties {

    // PROPERTIES =============================================================

    private final EntityPlayer player;

    // CONSTRUCTOR, GETTER, REGISTER ==========================================

    public PlayerData(EntityPlayer player) {
        this.player = player;
    }
[...]

As you can see I've added some comments to separate different parts of the class. You don't need to do this, but it makes the file easier to read.

I usually add some utility methods, one to register the extended properties, one to get it and one to check if we are on server or client side.

PlayerData.java:
// CONSTRUCTOR, GETTER, REGISTER ==========================================
[...]

public static PlayerData get(EntityPlayer player) {
    return (PlayerData) player.getExtendedProperties(identifier);
}

public static void register(EntityPlayer player) {
    player.registerExtendedProperties(identifier, new PlayerData(player));
}

public boolean isServerSide() {
    return this.player instanceof EntityPlayerMP;
}
[...]

You'll get an error under "identifier", because this is a constant we've not created yet. However, I'll just explain what those methods do first.

The first one uses the identifier to get an instance of the extended properties for that player and instantly casts it to PlayerData. This shortens the Code for getting an instance from

((PlayerData)player.getExtendedProperties(PlayerData.identifier))

to

PlayerData.get(player)

The second method registers the properties to the player. I'll show you in a minute where we need to use this.

The last method checks whether we are on server or client side. Here, this is done by checking if the player instance if an instance of the EntityPlayerMP (MultiPlayer) class, which is only the case if we are on server side.

If you want to create extended properties for other entities than players, you need to use this method:

PlayerData.java:
public boolean isServerSide() {
    return this.entity != null && this.entiy.worldObj != null && !this.entity.worldObj.isRemote;
}

Now, lets go on to the identifier. This will be a constant String.

PlayerData.java:
public class PlayerData implements IExtendedEntityProperties {

    private static final String identifier = "tutorialPlayerData";
[...]

The String needs to be unique, so it's a convention to use the modid (here tutorial) as a prefix.


This is how our class looks like now:

PlayerData.java - Show

Adding properties to the entity properties

To add a property, we first need to add a variable. Here, I'll add a "mana" value for the player, but you can do anything. It doesn't even need to be an integer, it can be really anything as long as you can serialize it to NBT data.

PlayerData.java:
// PROPERTIES =============================================================
[...]
private int mana;

That's it, we've just added a property.

We just need to add load/save and sync methods.


But first, let's add getters and setters.

PlayerData.java:
// GETTER, SETTER, SYNCER =================================================

public void setMana(int mana) {
    this.mana = mana;
}

public int getMana() {
    return this.mana;
}

We also can create a default value that is applied the first time the player enters a world and everytime the player respawns.

This value should be set in the constructor:

PlayerData.java:
// CONSTRUCTOR, GETTER, REGISTER ==========================================
public PlayerData(EntityPlayer player) {
    this.player = player;
    this.mana = 100;
}

To load and save the properties, we can use the methods saveNBTData and loadNBTData.

PlayerData.java:
// LOAD, SAVE =============================================================
@Override
public void saveNBTData(NBTTagCompound nbt) {
    nbt.setInteger("mana", this.getMana());
}

@Override
public void loadNBTData(NBTTagCompound nbt) {
    if (nbt.hasKey("mana", 3))
        this.setMana(nbt.getInteger("mana"));
}

In the save method, the mana value is added to the NBTTagCompound as an integer value named "mana".

The load method first checks whether the mana tag is available and of the type integer (ID 3). Only if the tag is available, the value is loaded and stored in the mana variable. The check is very useful, because otherwise our default values would be lost when the player creates a new world: They would be overridden with the default return value 0.

The last thing we need to create are synchronizer methods to sync data that is relevant for the client. If you have data that the client doesn't need to know, don't sync it.

We need a sync method for each property individually and a sync method for the PlayerData at all. Why? To reduce traffic. When a player joins the server, everything must be synchronized. Thus, we need a method to sync everything at all, without producing tons of packets. When only a single value is changed, it would be a waste of resources if we changed everything, thus we need methods and packets to synchronize single properties as well.

I assume you have a packet handler set up and know how to use it. If not, read this tutorial.


First, we add the functionality to synchronize the mana property only. Therefore, we need this packet class: (I won't explain this any more as I assume you know how to use packets)

PacketSyncMana.java:
package com.bedrockminer.tutorial.network.packets;

import com.bedrockminer.tutorial.entitydata.PlayerData;

import io.netty.buffer.ByteBuf;
import minersbasic.api.network.MessageHandler;
import minersbasic.api.utils.ClientUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;

public class PacketSyncMana implements IMessage {

    private int mana;

    public PacketSyncMana() {}

    public PacketSyncMana(int mana) {
        this.mana = mana;
    }

    @Override
    public void fromBytes(ByteBuf buf) {
        this.mana = buf.readInt();
    }

    @Override
    public void toBytes(ByteBuf buf) {
        buf.writeInt(this.mana);
    }

    // ========================================================================

    public static class Handler extends MessageHandler.Client<PacketSyncMana> {

        @Override
        public IMessage handleClientMessage(final EntityPlayer player, final PacketSyncMana msg, MessageContext ctx) {
            ClientUtils.addScheduledTask(new Runnable() {
                @Override
                public void run() {
                    PlayerData.get(player).setMana(msg.mana);
                }
            });
            return null;
        }
    }
}

The content of the run method is the most important part. Here, the mana value that was sent is set to the client side entity properties.

Register this packet to the packet handler.

We now can add a method to the PlayerData class that will sync the mana value.

PlayerData.java:
// GETTER, SETTER, SYNCER =================================================

public void setMana(int mana) {
    this.mana = mana;
    this.syncMana();
}

public int getMana() {
    return this.mana;
}

public void syncMana() {
    if (this.isServerSide())
        Main.packetHandler.sendTo(new PacketSyncMana(this.getMana()), (EntityPlayerMP) this.player);
}

The sync method needs to check whether it is called from server side and, if this is true, send the sync packet to the client.

The sync method also should be called from the setter method to immediately sync every change.


There are a few cases where the synchronization needs to be handled a bit different. Either if the property should be available for other players as well or if you are creating extended properties for non-player entities which should be available to client side players anyway.

If you want to do this, you need to change the packet a little bit:

You need to add another integer variable that stores the entity ID and use this to get the real entity.

PacketSyncMana.java - Show

Now we can change the sync method accordingly to this one:

The last value for sendToAllAround is the range. Depending on what your property does, you can change this value.

PlayerData.java:
public void syncMana() {
    if (this.isServerSide())
        Main.packetHandler.sendToAllAround(new PacketSyncMana(this.player, this.getMana()), this.player, 128);
}

Now, we can add Packet and method to sync everything. This is only necessary if you apply your data to a player who needs to receive every information when he connects to the server.


I'll show you a very simple method here that syncs every property with very few code.

Also, I'm going to use a request-response way, for two reasons.

First, if I only send a synchronization packet from the server once, it could get lost because the client has not started completely (Happend to me often enough).

Second, the client should be able to request a complete synchronization if needed, for instance after changing dimensions or similar.


To achieve this behaviour, I'll use a bidirectional packet here:

PlayerSyncPlayerData.java:
package com.bedrockminer.tutorial.network.packets;

import io.netty.buffer.ByteBuf;
import minersbasic.api.network.MessageHandler;
import minersbasic.api.utils.ClientUtils;
import minersbasic.api.utils.ServerUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraftforge.fml.common.network.ByteBufUtils;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;

import com.bedrockminer.tutorial.entitydata.PlayerData;

public class PacketSyncPlayerData implements IMessage {

    private NBTTagCompound data;

    public PacketSyncPlayerData () {}

    public PacketSyncPlayerData(PlayerData playerData) {
        this.data = new NBTTagCompound();
        playerData.saveNBTData(this.data);
    }

    @Override
    public void fromBytes(ByteBuf buf) {
        this.data = ByteBufUtils.readTag(buf);
    }

    @Override
    public void toBytes(ByteBuf buf) {
        ByteBufUtils.writeTag(buf, this.data);
    }

    // ========================================================================

    public static class Handler extends MessageHandler.Bidirectional<PacketSyncPlayerData> {

        @Override
        public IMessage handleClientMessage(final EntityPlayer player, final PacketSyncPlayerData msg, MessageContext ctx) {
            ClientUtils.addScheduledTask(new Runnable() {
                @Override
                public void run() {
                    PlayerData.get(player).loadNBTData(msg.data);
                }
            });
            return null;
        }

        @Override
        public IMessage handleServerMessage(final EntityPlayer player, PacketSyncPlayerData msg, MessageContext ctx) {
            ServerUtils.addScheduledTask(new Runnable() {
                @Override
                public void run() {
                    PlayerData.get(player).syncAll();
                }
            });
            return null;
        }
    }
}

The method syncAll, which currently still shows an error, is the method which actually sends the synchronization back. When the method requestSyncAll() is called from client side, it sends this packet to the server, where the method syncAll is called (see above). Then a new packet of this type is sent back, this time packed with the data that the client needs to read.

Here are those methods:

PlayerData.java:
public void syncAll() {
    if (this.isServerSide())
        Main.packetHandler.sendTo(new PacketSyncPlayerData(this), (EntityPlayerMP) this.player);
}

public void requestSyncAll() {
    if (!this.isServerSide())
        Main.packetHandler.sendToServer(new PacketSyncPlayerData());
}

Registering extended properties

Our PlayerData class is finished so far, so our next step is to register the extended properties.

Therefore, we need an Event Handler. If you don't know how to create one, read this tutorial and for god's sake, check the prerequisites before you start reading a tutorial! (Sorry, but some people havent got it yet).

We need an event handler that is registered with the Minecraft Forge event bus on both server and client side.

The events we're looking for are EntityConstructing and EntityJoinWorldEvent.

In the handler method for the EntityConstructing Event we register our extended properties, in the handler for the EntityJoinWorldEvent we request the synchronization.

EventHandlerCommon.java:
@SubscribeEvent
public void onEntityConstructing(EntityConstructing e) {
    if (e.entity instanceof EntityPlayer) {
        PlayerData.register((EntityPlayer) e.entity);
    }
}

@SubscribeEvent
public void onEntityJoinWorld(EntityJoinWorldEvent e) {
    if (e.entity instanceof EntityPlayer) {
        PlayerData.get((EntityPlayer) e.entity).requestSyncAll();
    }
}

Now, our entity properties are basically finished. There is only one problem left:

When the player dies or comes back from the end, the properties are resetted. This is wanted when the player dies, but most likely not when he returns from the end. Therefore, there is an event called PlayerEvent.Clone which is fired whenever the proerties are resetted. Here, we can add some code to keep the properties when returning from the end, or even keep some properties at all (for instance achievements).

EventHandlerCommon.java:
@SubscribeEvent
public void onPlayerCloned(PlayerEvent.Clone e) {
    NBTTagCompound nbt = new NBTTagCompound();
    PlayerData.get(e.original).saveReviveRelevantNBTData(nbt, e.wasDeath);
    PlayerData.get(e.entityPlayer).loadNBTData(nbt);
}

Here, a new NBTTagCompound is created to which the data of the original player is saved. The method saveReviveRelevantNBTData will be created in a second, so don't worry about the error.

After that, the new player loads the data.


This is how the method saveReviveRelevantNBTData looks like if no data should be copied when the player has died:

PlayerData.java:
public void saveReviveRelevantNBTData(NBTTagCompound nbt, boolean wasDeath) {
    if (!wasDeath)
        this.saveNBTData(nbt);
}

Now, our entity properties are finally finished!

Using entity properties

As an example on how you can use entity properties, I've just created three items, one to increase the mana amount, one to decrease it and one to print the current value out in chat. The first two execute their functionality on server side, the third one on client side to show that the synchronization works.

ModItems.java:
GameRegistry.registerItem(new Item() {
    @Override
    public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) {
        if (!world.isRemote) {
            PlayerData.get(player).setMana(PlayerData.get(player).getMana() + 1);
        }
        return stack;
    }
}.setUnlocalizedName("increase_item").setCreativeTab(CreativeTabs.tabMisc), "increase_item");

GameRegistry.registerItem(new Item() {
    @Override
    public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) {
        if (!world.isRemote) {
            PlayerData.get(player).setMana(PlayerData.get(player).getMana() - 1);
        }
        return stack;
    }
}.setUnlocalizedName("decrease_item").setCreativeTab(CreativeTabs.tabMisc), "decrease_item");

GameRegistry.registerItem(new Item() {
    @Override
    public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) {
        if (world.isRemote) {
            player.addChatMessage(new ChatComponentText("Mana: " + PlayerData.get(player).getMana()));
        }
        return stack;
    }
}.setUnlocalizedName("output_item").setCreativeTab(CreativeTabs.tabMisc), "output_item");

With those Items you can test the properties and try out what happens if you change dimensions, relog or die.


Maybe it would be useful to add bounds to the mana value (0 to 100 or something).

You can add those yourself, if you want to. If you want to use this in your mod, you should add bounds.


You can download the code used in this tutorial as a .zip file from here.


Recommended tutorials to continue with

Take a look at the tutorial overview and decide what you want to do next.


Comments and Questions:

If you want to report modding problems, please make sure to include the code in a pastebin link or something else! Don't just write "It doesn't work", otherwise your post will be deleted. For more complicated problems, please use the troubleshooter form.