Skip to content

Instantly share code, notes, and snippets.

@williewillus
Last active June 15, 2023 03:33
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save williewillus/c8dc2a1e7963b57ef436c699f25a710d to your computer and use it in GitHub Desktop.
Save williewillus/c8dc2a1e7963b57ef436c699f25a710d to your computer and use it in GitHub Desktop.
Capabilities: A Primer (tm)

Capabilities

Another award-winning primer by williewillus

Capabilities...a wondrous new system. That you've probably been forced into using. But let's not talk about that and get straight into the learning!

Terms and definitions

  • Capability System - This entire system; what this primer is about. This system is named very literally for what it does.
    • Capability - the quality of being capable; capacity; ability
    • Capable - having power and ability
  • Capability Interface - Some class (doesn't need to be an interface but highly recommended) that represents the functionality you want to expose. Example: Look up IItemHandler in your IDE, another example
  • Capability Implementation - Any implementation of a capability interface. Unique instances of this are attached to Entities/ItemStacks/etc.
  • Capability Object - An object that is a subclass of Capability<T> where T is a Capability Interface. It serves as a "blueprint" of sorts for operations pertaining to your capability interface. It's sort of like Block, which doesn't represent a block in the world, rather a blueprint for all blocks of that type in the world.
  • INBTSerializable<T extends NBTBase> - Marker interface for anything that can be written and read from NBT.
  • ICapabilityProvider - Interface that states that the implementing object can hold and provide capability implementations to those who request them. THIS IS NOT YOUR CAPABILITY INTERFACE.
    • Provides methods hasCapability and getCapability
      • These both take a Capability Object and a EnumFacing side
      • The first returns boolean
      • The second returns an object with the type of the Capability Interface T (the same T as the one in the passed Capability Object Capability<T>)
      • Notable: In vanilla TileEntity, ItemStack, Entity, World, and Chunk implement this interface.
  • ICapabilitySerializable<T> - A convenience combination of the above two interfaces

How it works

CapabilityDispatcher : The top-dog ICapabilityProvider implementation

  • Entities, TileEntities, and ItemStacks all delegate their hasCapability and getCapability methods to an instance of this class.
  • CapabilityDispatcher, simply put, is just an ICapabilityProvider that holds other ICapabilityProviders. These "child" providers are identified by a ResourceLocation name.
  • When asked for a Capability Implementation, the dispatcher simply asks all of its children, in order, until it finds one.
  • CapabilityDispatcher implements INBTSerializable. When written or read, the dispatcher simply calls the corresponding read and write methods on all of its children that implement INBTSerializable themselves.
  • CapabilityDispatchers can and DO hold OTHER CapabilityDispatchers. When reading or writing from NBT or when asking for Capability Implementations, a dispatcher always queries its parent first, if one exists.

Registering your Capability

Note that this entire section is only applicable if you are creating your own new Capabilities.

How do we create a Capability Object?

Simply call CapabilityManager.INSTANCE.register() It wants three things:

  • The class of your Capability Interface
  • An instance of IStorage (wha??? hang in there, I'll explain)
  • The class of one Capability Implementation (or a Callable factory) that you want to be the default implementation of this Capability Interface. For most intents and purposes, this will be the * only * implementation of the interface. Then, the internal manager will generate a Capability Object representing the Capability we just registered.

How do we get our Capability Object?

  • Whenever a Capability Object is created, Forge will look for all fields with the annotation @CapabilityInject(SomeClass.class). If SomeClass matches the class of your Capability Interface, Forge will then insert the Capability Object into that field. The field must be static and have type Capability<SomeClass>.
  • Forge will also look for all methods containing the same annotation. When the Capability Object is created, that method will be called, passing the Capability Object as a parameters. The method must be static, and take one parameter of type Capability<?>
  • You can do this with the Capability Interfaces of other APIs, thus allowing a powerful and safe way for testing if some Capability Object is present within your game environment, or enabling features whenever a Capability is registered.
  • Because of how the JVM processes annotations you can mention interfaces inside @CapabilityInject that could potentially be absent at runtime! The annotation just gets ignored.

What the heck is an IStorage?

  • An IStorage is a public-facing utility object that is able to read and write your Capability Interface to and from NBT.
  • "Wait a minute!" you say, confused that we already discussed NBT above and now here comes something that seems completely redundant.
  • An IStorage is simply a general purpose object that knows, for sure, how to read and write the default Capability Implementation. It may also be above to read and write other implementations, depending on the API author.
  • This is mainly useful for Capability Interfaces that are meant to be exposed and used publicly, such as energy or inventory APIs. In fact, IItemHandler (the Forge-provided Capability Interface for inventories) gives you a fully functional IStorage that is able to write and read. Thus, no one should ever need to write their own serialization/deserialization code for an IItemHandler.
  • If your Capability Interface is merely for internal usage, feel free to return an IStorage that does nothing - returns null in the write method and does nothing in the read method. Or, you could implement it but only use it internally. Your personal choice.

Making things have a Capability

If the Item / Entity / TileEntity / World / Chunk is a "foreign" one (vanilla counts as foreign!)

  • If you ever want to attach a capability to a "foreign" object (i.e. you do not have that object's code where you could sit down and edit it freely), then you must use AttachCapabilitiesEvent
  • This event is like any other Forge event. I assume you have knowledge about that system.
  • This event has many specializations: for TileEntity, Entity, ItemStack, World, Chunk, etc.
  • Add a capability by calling event.addCapability. It wants two things:
    • A unique ResourceLocation name for your capability
    • An ICapabilityProvider
  • This might sound familiar and you'd be right if you think so! It turns out, after the event is complete, all foreign ICapabilityProviders gathered are neatly tucked into the CapabilityDispatcher at the top.
  • Remember that if your ICapabilityProvider implements INBTSerializable, then CapabilityDispatcher will automatically call its read and write functions to load and save it from disk for you!
  • If you don't want your cap to saved (e.g. your information is volatile and recreated each time the world is loaded), then don't implement INBTSerializable.
  • See below for information about writing ICapabilityProviders

If the Item / Entity / TileEntity is from your mod

Items

  • Item capabilities work much in a similar way as foreign capability attachment.
  • Override Item.initCapabilities.
  • You are passed the ItemStack and the capability NBT subtag of the old stack, and are expected to return an ICapabilityProvider as before.
  • Your ICapabilityProvider becomes the top level capability provider of the ItemStack, the CapabilityDispatcher mentioned far above becomes its child.
  • See below for information about writing ICapabilityProviders

Entities and TileEntities

  • You simply need to override hasCapability and getCapability, as the two classes mentioned all implement ICapabilityProvider. You do NOT need to supply your own ICapabilityProvider like in the above cases.
  • As you are in control, you need to save your Capability Implementations yourself. Use the provided IStorage if applicable.
  • WARNING: You must fall back to super whenever you do this. Intuitively, from what we've learned so far, this makes sense. "Foreign" Capability Implementations are attached as children inside the CapabilityDispatcher wayyyy up in the class hierarchy in Entity and TileEntity. If you do not call super if you don't find your own Capability Object then you basically hide all other Capabilities further up in the inheritance chain and all other Capabilities attached by other people.
  • WARNING 2: Make these methods FAST. They may potentially get called every tick and capabilities need to react quickly. In most cases you can use direct if-statements like below. Try not to use data structures unless ABSOLUTELY needed.
  • An example (names use the terminology we've been using so far):
private final MyCapInterface myImpl = new MyCapImplementation();

@CapabilityInject(MyCapInterface.class)
public static Capability<MyCapInterface> MY_CAP_OBJECT = null; // This would probably be somewhere else

@Override
public static boolean hasCapability (Capability<?> capObject, EnumFacing side) {
    if (capObject == MY_CAP_OBJECT && side == EnumFacing.UP) {
      return true;
    } else {
      return super.hasCapability(capObject, side);
    }
}

@Override
public static <T> T getCapability(Capability<T> capObject, EnumFacing side) {
    if (capObject == MY_CAP_OBJECT && side == EnumFacing.UP) {
      return MY_CAP_OBJECT.cast(myImpl);
    } else {
      return super.getCapability(capObject, side);
    }
}

So..what do I even write in an ICapabilityProvider?

  • It might be a bit confusing what goes in here at first, but it gets more intuitive after an example
  • The Provider simply holds an instance of the Capability Implementation to give to whoever wants it. That should be it.
  • Although the behaviour shown in the example is simple, if you need complex behaviour you can pass the Entity, TileEntity, or Item directly into the Provider to operate on it directly. Be mindful of memory leaks.
  • Remember that making it INBTSerializable lets it get saved automatically. In this case, I simply defer to the Capability Interface, which I made extend INBTSerializable as well.
  • Remember to make the two methods fast!

Using other Capabilities

  • Obtain the Capability Object using @CapabilityInject(<CapabilityInterface>.class)
    • In some cases, a public field is already available in the API for usage.
    • For example a copy of IItemHandler's Capability Object lives at CapabilityItemHandler.ITEM_HANDLER_CAPABILITY
  • Simply call getCapability and pass the Capability Object you want and the side you want to query. Null is a VALID side to ask about! You will receive back either an instance of the Capability Interface or null if nothing was found.

IItemHandler

  • IItemHandler is the Forge-provided Capability Interface for all inventories. It is meant to replace usages of IInventory.
  • ItemStackHandler is the default Capability Implementation
  • All vanilla TE's are retrofitted to expose this Capability Interface
  • Several entities also expose this Capability Interface (Horses, Mobs, and Players)
  • ItemHandlerHelper is a helper class provided by Forge to deal with IItemHandlers. It is SUPER useful, and with it you can get rid of all of your potentially buggy inventory insertion and extraction boilerplate
  • You can use IItemHandler in Containers as well, just use SlotItemHandler which takes an IItemHandler instead of IInventory
    • Important note: If you have item handlers that restrict automated insertion and extraction by guarding insertItem/extractItem, it's easy to accidentally break your slots since SlotItemHandler uses insertItem and extractItem to put things in the slot. I advise, in your internal code, pass around a Capability Implementation that has less restriction so that GUIs work properly. Then, in getCapability return a wrapped Implementation that restricts insertion and extraction as appropriate

Capability case study: ProjectE Alchemical Bags

@CrispyChips6660
Copy link

Is this updated for 1.15?

@williewillus
Copy link
Author

No. Most of it should still work, but the major thing that's changed in modern versions is a LazyOptional is now returned instead of having separate has/getCapability methods

@gamerYToff
Copy link

you are incredible! I didn't know about project bags. I'm going to study their code to try to help me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment