The Capability rework

Introduction

Our initial 20.3 release comes with a fundamental redesign of the capability system, with the goal of fixing all the issues that were found in the previous iteration after years of usage.

Most importantly, there are now two different systems to replace what was previously known as “capabilities”:

  • Data attachments allow adding arbitrary data to block entities, chunks, entities, and item stacks.
  • Capabilities allow querying behavior instances from blocks, entities, and item stacks.

Data attachments

The attachment system allows mods to attach arbitrary data objects to block entities, chunks, entities, and stacks.

To use the system, you need to register an AttachmentType. The attachment type contains:

  • a default value supplier to create the instance when the data is first accessed, or to compare stacks that have the data and stacks that don’t have it;
  • an optional serializer if the attachment should be persisted;
  • additional configuration options for the attachment, for example the copyOnDeath flag.

There are a few ways to provide an attachment serializer: directly implementing IAttachmentSerializer, implementing INBTSerializable and using the static AttachmentSerializer.serializable() method to create the builder, or providing a codec to the builder. (This latter option is not recommended for item stacks due to relatively slowness).

In any case, we recommend using a DeferredRegister for registration:

// Create the DeferredRegister for attachment types
private static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, MOD_ID);

// Serialization via INBTSerializable
private static final Supplier<AttachmentType<ItemStackHandler>> HANDLER = ATTACHMENT_TYPES.register(
        "handler", () -> AttachmentType.serializable(() -> new ItemStackHandler(1)).build());
// Serialization via codec
private static final Supplier<AttachmentType<Integer>> MANA = ATTACHMENT_TYPES.register(
        "mana", () -> AttachmentType.builder(() -> 0).serialize(Codec.INT).build());
// No serialization
private static final Supplier<AttachmentType<SomeCache>> SOME_CACHE = ATTACHMENT_TYPES.register(
        "some_cache", () -> AttachmentType.builder(() -> new SomeCache()).build()
);

// Don't forget to register the DeferredRegister to your mod bus:
ATTACHMENT_TYPES.register(modBus);

Once the attachment type is registered, it can be used on any holder object. Calling getData if no data is present will attach a new default instance.

// Get the ItemStackHandler if it already exists, else attach a new one:
ItemStackHandler stackHandler = stack.getData(HANDLER);
// Get the current player mana if it is available, else attach 0:
int playerMana = player.getData(MANA);
// And so on...

If attaching a default instance is not desired, a hasData check can be added:

// Check if the stack has the HANDLER attachment before doing anything.
if (stack.hasData(HANDLER)) {
    ItemStackHandler stackHandler = stack.getData(HANDLER);
    // Do something with stack.getData(HANDLER).
}

The data can also be updated with setData:

// Increment mana by 10.
player.setData(MANA, player.getData(MANA) + 10);

Usually, block entities and chunks need to be marked as dirty when they are modified (with setChanged and setUnsaved(true)). This is done automatically for calls to setData:

chunk.setData(MANA, chunk.getData(MANA) + 10); // will call setUnsaved automatically

but if you modify some data that you obtained from getData then you must mark block entities and chunks as dirty explicitly:

var mana = chunk.getData(MUTABLE_MANA);
mana.set(10);
chunk.setUnsaved(true); // must be done manually because we did not use setData

Before we move on to capabilities, here are a few points to take note of with respect to the data attachment system:

  • Level attachments were removed: please use SavedData instead.
  • Serializable item stack attachments are always synced with the client now.
  • Entity attachments are copied when a player is teleported back from the end. (Previously this was not the case).
  • Entity attachments that have copyOnDeath set in their builder will automatically be copied on player death (and on mob conversion).

Future work for attachments

We have plans to work on the following improvements to the attachment system over the coming weeks:

  • Attachments in recipe JSONs: Just like we add support for count and NBT to recipe results, we will add support to specify data attachments in recipe result JSONs.
  • Syncable data attachments: Currently, all serializable item stack attachments are synced automatically from the logical server to the logical client. We will look into opt-in syncing for block entity, chunk, and entity attachments in the future.
  • Custom copy handler: Currently, all data attachments are copied by serializing to NBT and then deserializing a new copy. This is a good default, but we want to allow modders to provide their own copy implementation for better performance.

We are open to other suggestions as well, don’t hesitate to get in touch!

Capabilities

Capabilities are designed to separate what a block, entity or item stack can do from how it does it. If you are wondering whether capabilities are the right tool for a job, ask yourself the following questions:

  1. Do I only care about what a block, entity or item stack can do, but not about how it does it?
  2. Is the what, the behavior, only available for some blocks, entities, or item stacks, but not all of them?
  3. Is the how, the implementation of that behavior, dependent on the specific block, entity or item stack?

Here are a few examples of good capability usage:

  • “I want to count how many items are in some entity, but I do not know how the entity might store them.” - Yes, use the IItemHandler capability.
  • “I want to fill some item stack with power, but I do not know how the item stack might store it.” - Yes, use the IEnergyStorage capability.
  • “I want to apply some color to whatever block a player is currently targeting, but I do not know how the block will be transformed”. - Yes. NeoForge does not provide a capability to color blocks, but you can implement one yourself.

Here is an example of discouraged capability usage:

  • “I want to check if an entity is within the range of my machine.” - No, use a helper method instead.

NeoForge supports capabilities for blocks, entities, and item stacks.

Capabilities allow looking up implementations of some APIs with some dispatching logic. The following kinds of capabilities are implemented in NeoForge:

  • BlockCapability: capabilities for blocks and block entities; behavior depends on the specific Block.
  • EntityCapability: capabilities for entities: behavior dependends on the specific EntityType.
  • ItemCapability: capabilities for item stacks: behavior depends on the specific Item.

Creating capabilities

NeoForge already defines common capabilities, which we recommend for compatibility with other mods. For example:

// Standard item handler BlockCapability
Capabilities.ItemHandler.BLOCK
// Standard item handler ItemCapability
Capabilities.ItemHandler.ITEM

// See the `Capabilities` class for the full list.

If these are not sufficient, you can create your own capabilities. Creating a capability is a single function call, and the resulting object should be stored in a static final field. The following parameters must be provided:

  • The name of the capability. Creating a capability with the same name multiple times will always return the same object. Capabilities with different names are completely independent, and can be used for different purposes.
  • The behavior type that is being queried. This is the T type parameter.
  • The type for additional context in the query. This is the C type parameter.

For example, here is how a capability for side-aware block IItemHandlers might be declared:

public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
    BlockCapability.create(
        // Provide a name to uniquely identify the capability.
        new ResourceLocation("mymod", "item_handler"),
        // Provide the queried type. Here, we want to look up `IItemHandler` instances.
        IItemHandler.class,
        // Provide the context type. We will allow the query to receive an extra `Direction side` parameter.
        Direction.class);

A @Nullable Direction is so common for blocks that there is a dedicated helper:

public static final BlockCapability<IItemHandler, @Nullable Direction> ITEM_HANDLER_BLOCK =
    BlockCapability.createSided(
        // Provide a name to uniquely identify the capability.
        new ResourceLocation("mymod", "item_handler"),
        // Provide the queried type. Here, we want to look up `IItemHandler` instances.
        IItemHandler.class);

If no context is required, Void should be used. There is also a dedicated helper for context-less capabilities:

public static final BlockCapability<IItemHandler, Void> ITEM_HANDLER_NO_CONTEXT =
    BlockCapability.createVoid(
        // Provide a name to uniquely identify the capability.
        new ResourceLocation("mymod", "item_handler_no_context"),
        // Provide the queried type. Here, we want to look up `IItemHandler` instances.
        IItemHandler.class);

For entities and item stacks, similar methods exist in EntityCapability and ItemCapability respectively.

Querying capabilities

Once we have our BlockCapability, EntityCapability, or ItemCapability object in a static field, we can query a capability.

Entities and item stacks have essentially the same API as before, but with a @Nullable T return type instead of LazyOptional<T>. Simply call getCapability with the capability object and the context:

var object = entity.getCapability(CAP, context);
if (object != null) {
    // Use object
}
var object = stack.getCapability(CAP, context);
if (object != null) {
    // Use object
}

Block capabilities are used differently, to accommodate for capabilities provided by blocks without block entities. The query is performed on a level:

var object = level.getCapability(CAP, pos, context);
if (object != null) {
    // Use object
}

If the block entity and/or the block state is known, they can be passed to save on query time:

var object = level.getCapability(CAP, pos, blockState, blockEntity, context);
if (object != null) {
    // Use object
}

To give a more concrete example, here is how one might query an IItemHandler capability for a block, from the Direction.NORTH side:

IItemHandler handler = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, Direction.NORTH);
if (handler != null) {
    // Use the handler for some item-related operation.
}

Block capability caching

For efficient queries and automatic caching, use BlockCapabilityCache instead of directly calling level.getCapability. This is a more powerful replacement for the old LazyOptional invalidation system.

When a capability is looked up, the system will perform the following steps under the hood:

  1. Fetch block entity and block state if they were not supplied.
  2. Fetch registered capability providers. (More on this below).
  3. Iterate the providers and ask them if they can provide the capability.
  4. One of the providers will return a capability instance, potentially allocating a new object.

The implementation is rather efficient, but for queries that are performed frequently, for example every game tick, these steps can take a significant amount of server time. The BlockCapabilityCache system provides a dramatic speedup for capabilities that are frequently queried at a given position.

Generally, a BlockCapabilityCache will be created once and then stored in a field of the object performing frequent capability queries. When exactly you store the cache is up to you. The cache must be provided with the capability to query, the level, the position, and the query context.

// Declare the field:
private BlockCapabilityCache<IItemHandler, @Nullable Direction> capCache;

// Later, for example in `onLoad` for a block entity:
this.capCache = BlockCapabilityCache.create(
        Capabilities.ItemHandler.BLOCK, // capability to cache
        level, // level
        pos, // target position
        Direction.NORTH // context
);

Querying the cache is then done with getCapability():

IItemHandler handler = this.capCache.getCapability();
if (handler != null) {
    // Use the handler for some item-related operation.
}

The cache is automatically cleared by the garbage collector, there is no need to unregister it.

It is also possible to receive notifications when the capability object changes! This includes capabilities changing (oldHandler != newHandler), becoming unavailable (null) or becoming available again (not null anymore).

The cache then needs to be created with two additional parameters:

  • A validity check, that is used to determine if the cache is still valid. In the simplest usage as a block entity field, () -> !this.isRemoved() will do.
  • An invalidation listener, that is called when the capability changes. This is where you can react to capability changes, removals, or appearances.
// With optional invalidation listener:
this.capCache = BlockCapabilityCache.create(
        Capabilities.ItemHandler.BLOCK, // capability to cache
        level, // level
        pos, // target position
        Direction.NORTH, // context
        () -> !this.isRemoved(), // validity check (because the cache might outlive the object it belongs to)
        () -> onCapInvalidate() // invalidation listener
);

For this system to work, modders must call level.invalidateCapabilities(pos) whenever a capability changes, appears, or disappears.

// whenever a capability changes, appears, or disappears:
level.invalidateCapabilities(pos);

NeoForge already handles common cases such as chunk load/unloads and block entity creation/removal, but other cases need to be handled explicitly by modders. For example, modders must invalidate capabilities in the following cases:

  • If the configuration of a capability-providing block entity changes.
  • If a capability-providing block (without a block entity) is placed or changes state, by overriding onPlace.
  • If a capability-providing block (without a block entity) is removed, by overriding onRemove.

For a plain block example, refer to the ComposterBlock.java file.

For more information, refer to the javadoc of IBlockCapabilityProvider.

Registering capabilities

A capability provider is what ultimately supplies a capability. A capability provider is function that can either return a capability instance, or null if it cannot provide the capability. Providers are specific to:

  • the given capability that they are providing for, and
  • the block instance, block entity type, entity type, or item instance that they are providing for.

They need to be registered in the RegisterCapabilitiesEvent.

Block providers are registered with registerBlock. For example:

private static void registerCapabilities(RegisterCapabilitiesEvent event) {
    event.registerBlock(
        Capabilities.ItemHandler.BLOCK, // capability to register for
        (level, pos, state, be, side) -> <return the IItemHandler>,
        // blocks to register for
        MY_ITEM_HANDLER_BLOCK,
        MY_OTHER_ITEM_HANDLER_BLOCK);
}

In general, registration will be specific to some block entity types, so the registerBlockEntity helper method is provided as well:

    event.registerBlockEntity(
        Capabilities.ItemHandler.BLOCK, // capability to register for
        MY_BLOCK_ENTITY_TYPE, // block entity type to register for
        (myBlockEntity, side) -> <return the IItemHandler for myBlockEntity and side>);

Entity registration is similar, using registerEntity:

event.registerEntity(
    Capabilities.ItemHandler.ENTITY, // capability to register for
    MY_ENTITY_TYPE, // entity type to register for
    (myEntity, context) -> <return the IItemHandler for myEntity>);

Item registration is similar too. Note that the provider receives the stack:

event.registerItem(
    Capabilities.ItemHandler.ITEM, // capability to register for
    (itemStack, context) -> <return the IItemHandler for the itemStack>,
    // items to register for
    MY_ITEM,
    MY_OTHER_ITEM);

If for some reason you need to register a provider for all blocks, entities, or items, you will need to iterate the corresponding registry and register the provider for each object.

For example, NeoForge uses this system to register a fluid handler capability for all buckets:

// For reference, you can find this code in the `CapabilityHooks` class.
for (Item item : BuiltInRegistries.ITEM) {
    if (item.getClass() == BucketItem.class) {
        event.registerItem(Capabilities.FluidHandler.ITEM, (stack, ctx) -> new FluidBucketWrapper(stack), item);
    }
}

Providers are asked for a capability in the order that they are registered. Should you want to run before a provider that NeoForge already registers for one of your objects, register your RegisterCapabilitiesEvent handler with a higher priority. For example:

modBus.addListener(RegisterCapabilitiesEvent.class, event -> {
    event.registerItem(
        Capabilities.FluidHandler.ITEM,
        (stack, ctx) -> new MyCustomFluidBucketWrapper(stack),
        // blocks to register for
        MY_CUSTOM_BUCKET);
}, EventPriority.HIGH); // use HIGH priority to register before NeoForge!

See CapabilityHooks for a list of the providers registered by NeoForge itself.

Entities, IItemHandler and Direction

You can skip this section if you don’t use the item handler entity capability.

There are now two capabilities for item handlers on entities:

  • Capabilities.ItemHandler.ENTITY: exposes the full inventory of some entity.
  • Capabilities.ItemHandler.ENTITY_AUTOMATION: exposes the automation-accessible inventory. Hoppers and droppers are patched to support that capability.

Here is a migration guide from the old system that used a single capability, and distinguished using the Direction parameter:

Minecart and chest inventories

If you want to support automation-aware inventories:

Old Syntax New Syntax
entity.getCapability(...) entity.getCapability(Capabilities.ItemHandler.ENTITY_AUTOMATION)

Otherwise:

Old Syntax New Syntax
entity.getCapability(...) entity.getCapability(Capabilities.ItemHandler.ENTITY)

Horse inventory

Old Syntax New Syntax
horse.getCapability(..., ...) horse.getCapability(Capabilities.ItemHandler.ENTITY)

Living entities

Old Syntax New Syntax
entity.getCapability(..., any vertical direction) new EntityHandsInvWrapper(livingEntity)
entity.getCapability(..., any horizontal direction) new EntityArmorInvWrapper(livingEntity)
entity.getCapability(..., null) livingEntity.getCapability(Capabilities.ItemHandler.ENTITY)

Players

Old Syntax New Syntax
player.getCapability(..., any vertical direction) new PlayerMainInvWrapper(player.getInventory())
player.getCapability(..., any horizontal direction) new CombinedInvWrapper(new PlayerArmorInvWrapper(player.getInventory()), new PlayerOffhandInvWrapper(player.getInventory()))
player.getCapability(..., null) player.getCapability(Capabilities.ItemHandler.ENTITY)

Future plans for capabilities

Composters now support the item handler capability. However, cauldrons still do not support the fluid handler capability. This will be addressed in the coming weeks, and mods using the block fluid handler capability will work with cauldrons out of the box.

We have reviewed and tested this capability overhaul extensively. Nonetheless, we expect that issues will be discovered after the release. Please don’t hesitate to get in touch with us, be it on Discord or GitHub!

That’s all for now, happy porting!