Minecraft Blogs / Tutorial

Forge tutorial: Capability System

  • 28,577 views, 1 today
  • 13
  • 7
  • 42
McHorse's Avatar McHorse
Level 71 : Legendary Unicorn
264
Background: I was working on multiplayer support for my mod, and I needed to store some information in the player. Forge provided some mechanism called Capabilities (this system replaced mechanism that was previously known as IEEP). I spent few days figuring out how this mechanism works. The official documentation didn't really helped to understand how to use Capabilities, so I decided to write a tutorial of how to use this mechanism. I hope it will help you :)

This tutorial requires an understanding of basic concept of Forge modding (proxies).



From official documentation, Capabilities are:

... allow exposing features in a dynamic and flexible way, without having to resort to directly implementing many interfaces.

While it a valid explanation, but it's really wordy, filled with complicated terms, and not really clear. In my words:

Capabilities is a mechanism to store your custom interface implementations (functions and data) in Entities, TEs, ItemStacks and World with separate storage class that would be responsible for saving and loading data for your implementation. It also allows substituting default implementations in another mods.

Capabilities are actually pretty easy to use (if you'll find an code example in internet). I will only walk over about how to create your own capability.

Creating Capability

Let's say we want to store some information about player, like mana. Let's create capability for storing mana. So first we start out with creating and Java interface for your capability:

/**
* Mana capability
*/
public interface IMana
{
public void consume(float points);
public void fill(float points);
public void set(float points);

public float getMana();
}

Our mana capability would have methods for consuming, filling, setting and getting mana methods. That's our capability. Now that we have our interface, we need to create a default implementation for our capability. Our default implementation would be some basic class that implements IMana interface:

/**
* Default implementation of IMana
*/
public class Mana implements IMana
{
private float mana = 250.0F;

public void consume(float points)
{
this.mana -= points;

if (this.mana < 0.0F) this.mana = 0.0F;
}

public void fill(float points)
{
this.mana += points;
}

public void set(float points)
{
this.mana = points;
}

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

This is a simple implementation of our mana storage interface. Now that we have our capability interface and default implementation, we need to create a storage class that will be responsible for persisting our mana to world.

Creating Storage Class

Storage class (implements Capability.IStorage) is responsible for saving capability's data to NBT (as most of stuff in minecraft). For our mana capability, we'll have a pretty simple storage class:

/**
* This class is responsible for saving and reading mana data from or to server
*/
public class ManaStorage implements IStorage<IMana>
{
@Override
public NBTBase writeNBT(Capability<IMana> capability, IMana instance, EnumFacing side)
{
return new NBTTagFloat(instance.getMana());
}

@Override
public void readNBT(Capability<IMana> capability, IMana instance, EnumFacing side, NBTBase nbt)
{
instance.set(((NBTPrimitive) nbt).getFloat());
}
}

Nothing complicated, just an implementation of IStorage interface that responsible for reading data from and writing data to NBT. You can write or read any type of NBT class like starting from NBTPrimitive instances to NBTTagCompound. All you need to do is to stay consistent with your NBT read and return types.

Now that we have our capability and its storage, it's time to register our capability.

Registering our Capability

Registering a capability is actually a breeze. All you have to do is to place this one-liner to your common proxy:

CapabilityManager.INSTANCE.register(IMana.class, new ManaStorage(), Mana.class);
This line of code will make our capability available for injection via @CapabilityInject annotation. The final part of this Capabilities tutorial is attaching the capability to designated target (i.e. Entity, TE, ItemStack or World).

Attaching our Mana Capability

So, to make everything work, we need to attach our capability to the designated target (Entity, TE, ItemStack, or/and World). To do that, we must create an event handler that listens to AttachCabapilityEvent. In this tutorial, I'm going to attach my capability to player, however, this may be done to anything listed above.

Before attaching capability, we need another class that will provide capability:

/**
* Mana provider
*
* This class is responsible for providing a capability. Other modders may
* attach their own provider with implementation that returns another
* implementation of IMana to the target's (Entity, TE, ItemStack, etc.) disposal.
*/
public class ManaProvider implements ICapabilitySerializable<NBTBase>
{
@CapabilityInject(IMana.class)
public static final Capability<IMana> MANA_CAP = null;

private IMana instance = MANA_CAP.getDefaultInstance();

@Override
public boolean hasCapability(Capability<?> capability, EnumFacing facing)
{
return capability == MANA_CAP;
}

@Override
public <T> T getCapability(Capability<T> capability, EnumFacing facing)
{
return capability == MANA_CAP ? MANA_CAP.<T> cast(this.instance) : null;
}

@Override
public NBTBase serializeNBT()
{
return MANA_CAP.getStorage().writeNBT(MANA_CAP, this.instance, null);
}

@Override
public void deserializeNBT(NBTBase nbt)
{
MANA_CAP.getStorage().readNBT(MANA_CAP, this.instance, null, nbt);
}
}


So what this class does? This class is responsible for storing registered capability that will be injected via @CapabilityInject annotation (you can see how this implemented in CapabilityManager class), providing a capability based on the interface and saving/loading capability data. If you don't need to persist the data, you replace ICapabilitySerializable interface with ICapabilityProvider and remove serializeNBT and deserializeNBT methods).

The beauty of ICapabilityProvider (underlying interface of ICapabilitySerializable) is that other modders can substitute your capability provider and provide other implementation of your interface.

So now, finally, we can attach our capability to players:

/**
* Capability handler
*
* This class is responsible for attaching our capabilities
*/
public class CapabilityHandler
{
public static final ResourceLocation MANA_CAP = new ResourceLocation(ExampleMod.MODID, "mana");

@SubscribeEvent
public void attachCapability(AttachCapabilitiesEvent.Entity event)
{
if (!(event.getEntity() instanceof EntityPlayer)) return;

event.addCapability(MANA_CAP, new ManaProvider());
}
}

You need to register this event handler to MinecraftForge.EVENT_BUS in your common proxy.

The code is pretty simple here, we attach our capability to EntityPlayer (both on client and server). Now, next time you want to use your capability from the player, for example, consume or refill mana, you can use: Entity#getCapability(Capability<?> cap, EnumFacing face) method to retrieve IMana implementation. Let's make something that will consume mana or fill it.

Let's add another event handler that would be responsible for consuming and filling mana. This would be pretty simple, when player falls, it absorbs mana and shows in chat the amount of mana that was consumed and how much mana left. When players uses the bed, it will refill 50 mana points. Also when the player enters the game, it shows how much mana he has:

public class EventHandler
{
@SubscribeEvent
public void onPlayerLogsIn(PlayerLoggedInEvent event)
{
EntityPlayer player = event.player;
IMana mana = player.getCapability(ManaProvider.MANA_CAP, null);

String message = String.format("Hello there, you have §7%d§r mana left.", (int) mana.getMana());
player.addChatMessage(new TextComponentString(message));
}

@SubscribeEvent
public void onPlayerSleep(PlayerSleepInBedEvent event)
{
EntityPlayer player = event.getEntityPlayer();

if (player.worldObj.isRemote) return;

IMana mana = player.getCapability(ManaProvider.MANA_CAP, null);

mana.fill(50);

String message = String.format("You refreshed yourself in the bed. You received 50 mana, you have §7%d§r mana left.", (int) mana.getMana());
player.addChatMessage(new TextComponentString(message));
}

@SubscribeEvent
public void onPlayerFalls(LivingFallEvent event)
{
Entity entity = event.getEntity();

if (entity.worldObj.isRemote || !(entity instanceof EntityPlayerMP) || event.getDistance() < 3) return;

EntityPlayer player = (EntityPlayer) entity;
IMana mana = player.getCapability(ManaProvider.MANA_CAP, null);

float points = mana.getMana();
float cost = event.getDistance() * 2;

if (points > cost)
{
mana.consume(cost);

String message = String.format("You absorbed fall damage. It costed §7%d§r mana, you have §7%d§r mana left.", (int) cost, (int) mana.getMana());
player.addChatMessage(new TextComponentString(message));

event.setCanceled(true);
}
}
}


Attach this event handler in your common proxy, and you're good to go. Known bugs: you can spam the bed during the day light to gain a lot of mana points. It's an issue, but I created this event handler for demonstration how to use created capabilities.

If you want your values to be accessible on client side, you need to manually send packets to the client with values you want to change. See official documentation link for more information.

Recovering capability data on death

One problem (or rather flexible choice, in case if you don't want to keep the data) with capabilities, is that you need to copy data between dimensions and on death from died/teleported player to new player. It's done in a quite simple manner. We just need to add an event listener on player's clone (when player dies or gets teleported to another dimension), and simply copy the data from old player's capability to a new player's capability (added method to EventListener):

/**
* Copy data from dead player to the new player
*/
@SubscribeEvent
public void onPlayerClone(PlayerEvent.Clone event)
{
EntityPlayer player = event.getEntityPlayer();
IMana mana = player.getCapability(ManaProvider.MANA_CAP, null);
IMana oldMana = event.getOriginal().getCapability(ManaProvider.MANA_CAP, null);

mana.set(oldMana.getMana());
}

That's it! Now when a player gets killed or trasnferred to another dimension, the data from previous player will be recovered to a new player. For more complex capabilities, I recommend adding another method to capability interface named "copy" which will accept a capability as first argument from which it can copy the data. Take a look for example in Metamorph's source.

If you want to recover data only between dimensions (not on the death, though), take a look at PlayerEvent.Clone's method called isWasDeath(). It checks whether player died or was transferred to another dimension.

I also published a mod on github with working code of this tutorial. Feel free to check it out.



I hope you enjoyed the tutorial, and it was pretty clear and easy to understand. If you have any questions or problems, feel free to leave a comment.

Don't PM me with problems related to this tutorial, please, if you leave the comment with problem, and I provide a solution, it would benefit everyone, thanks! :)

For more information, see those resources:
P.S.: My definition of Forge's Capabilities mechanism is subjective, I put it as I understand Capabilities mechanism
P.S.S.: Sorry for formatting of the code, apparently PMC's formatter trims space indentation
Tags

1 Update Logs

Added a section about recovering data on death : by McHorse 03/13/2017 4:09:06 amMar 13th, 2017

Just a short paragraph about how to transfer data from old player (which died or teleported to another dimension) to a new one.

Create an account or sign in to comment.

1
05/11/2021 6:42 pm
Level 1 : New Miner
DivinityEX12
DivinityEX12's Avatar
Hi. I've followed pretty much this entire guide. However, I am getting an error. When I try to create the capability object in my event handler, then setMana, the line where I setMana returns a null pointer exception error. Any ideas what it could be?

Here is the part that errors:

@SubscribeEvent
public void onPlayerSleep(PlayerSleepInBedEvent event)
{
EntityPlayer player = event.getEntityPlayer();

if (player.world.isRemote) return;

IPlayerAttack mana = player.getCapability(AttackProvider.ATTACK_CAP, null);

mana.setAttack(2.0f);
String message = String.format("You refreshed yourself in the bed. You received 50 mana, you have §7%d§r mana left.", (int) mana.getAttack());
player.sendMessage(new TextComponentString(message));
}
1
09/14/2020 8:09 am
Level 1 : New Miner
SOTAGummy
SOTAGummy's Avatar
Hello. I’m the one making a mod based on this tutorial.  When I log out of the game, my Mana values are not saved. Could you please tell me what I’m missing? Thankyou
1
09/14/2020 3:08 pm
Level 71 : Legendary Unicorn
McHorse
McHorse's Avatar
Did you implemented the Storage class?
1
09/24/2020 11:26 am
Level 1 : New Miner
SOTAGummy
SOTAGummy's Avatar
Upon examination, I found that it was due to the client and server not being in sync. Can you tell me how to synchronize Capability with packets?
3
06/25/2020 12:22 pm
Level 43 : Master Miner
GnRSlashSP
GnRSlashSP's Avatar
Very nice tutorial and very good explanation about Forge Documentation (too simple, no useful examples and too complex....)

I already made a mod 1.14.4, where an entity (mob), has interactable inventory, with container, gui and network messages to sync it. It is working very well but I really want to upgrade the code to use Capabilities with IItemHandler and other recomended methods.

I have no idea where to start....
I use Inventory class, IInventory interface, etc.. How can I change all of this to new capability system?
Any links that help me about this?

thank you for your tutorial, very good!
1
06/25/2020 3:36 pm
Level 71 : Legendary Unicorn
McHorse
McHorse's Avatar
Sadly, I'm stuck on 1.12.2, so I don't know how it's done in 1.14.4.
2
07/25/2020 6:13 pm
Level 1 : New Miner
SolarAlpha
SolarAlpha's Avatar
Stuck on 1.12.2 as well, for spongeforge. I don't believe the system itself changed very much in later versions. What broke in 1.13 was more objects and not systems, though the register method does already seem to be deprecated and almost removed in favor of another documented (more messy forge documentation) system.
2
10/13/2019 10:01 pmhistory
Level 89 : Elite Senpai
sekwah41
sekwah41's Avatar
You might wanna update
CapabilityManager.INSTANCE.register(IMana.class, new ManaStorage(), Mana.class);
to
CapabilityManager.INSTANCE.register(IMana.class, new ManaStorage(), Mana::new);

Due to the other one being deprecated
1
08/04/2019 10:23 am
Level 1 : New Explorer
V972
V972's Avatar
This tutorial is gonna be hella usefull for my first mod, thank you! Aven tho I'll be attaching capabilities to swords.
1
08/05/2019 3:38 am
Level 71 : Legendary Unicorn
McHorse
McHorse's Avatar
You're welcome! 🙂
Planet Minecraft

Website

© 2010 - 2024
www.planetminecraft.com

Welcome