Skip to content

Instantly share code, notes, and snippets.

@gigaherz
Last active March 28, 2017 12:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gigaherz/2dbaaff11e32940cc9500e863071b69b to your computer and use it in GitHub Desktop.
Save gigaherz/2dbaaff11e32940cc9500e863071b69b to your computer and use it in GitHub Desktop.

I want to show you why the capabilities work the way they work, and not just how they work.

The following explanation is not meant to be accurate with reality, only a way to form the necessary connections and be able to understand one possible reasoning for the design.

I will try my best.

Backstory

Once upon a time, every API would define a bunch of interfaces that people would implement in their TileEntities and such. Because the interfaces are fixed, every API would have its own isActive or canConnect or similar, and they'd have facing parameters on each function, because often, a machine may expose different APIs through different sides. Add it all together, and they'd clutter your code to death.

Some mod authors wanted more flexibility: a way to enable and disable features, maybe a block only wants to provide RF energy if the Rf upgrade is present, maybe they only have an inventory if an inventory upgrade is present.

A clear exmaple are the MultiPart mods (Forge MultiPart / MCMultiPart), which generally speaking, are "wrappers" on a block. They simply couldn't do that before, because it would mean they'd have to implement ALL the APIs ever created. So rather than that, each block that wanted to support multiparts would have to implement them into their own blocks, adding to the overall mess and meaning implementations were inconsistent. What if two separate blocks needed to be the master?

Yeah, that bad.

Providers

So the idea occurred to someone at some point, to have a generic system for dynamic features. There would be an API with a method to get the requested feature. Since getting the feature only to see if it exists may be slow and wasteful, it was paired with another method used to quickly check if it's present or not.

That ended up as hasCapability, and getCapability. This API then would be implemented by Entities and TileEntities.

That, however, only solves half of the problem. It works if it's your own object, but what about adding capabilities to others' objects, or even to ItemStacks, that don’t have custom classes?

Well, how about we make the hasCapability and getCapability methods an interface? Let's call it ICapabilityProvider. Then, anyone who's a capability provider will be able to ... provide capabilities. Which capabilities? It doesn't matter.

In order to support extra providers, Forge adds to those basic objects, including ItemStacks, an internal list of attached providers, alongside the Item#initCapabilities method, and the AttachCapabilitiesEvent, with sub-events for Entities, TileEntities, and ItemStacks.

The Capability class

The next problem to solve, then, is how to identify the capabilities. There needs to be a 'key', which could be a number, or a string, or even a ResourceLocation. As you will see in a bit, the final solution was neither of those.

There was yet another issue with capabilities: the capability is registered normally for an interface, as a way to avoid dealing with explicit implementations. However, if you have to then use an explicit class, the point becomes moot. So why not also add a way to get instances of a capability without requiring the user of the capability to know which class it is? From that thought, came the default instance factory.

The question then would be, how to you GET that instance? You'd need some way to obtain it, some helper class, maybe. Of course, it would have to be tied to the capability, and it would have to be easy to discover, probably even passed along to the providers for easy access.

The solution seems obvious: if you give each capability a special object, both as identifier, and helper, you kill both birds with one stone. Let’s call this the Capability, with capital c. Then, the methods end up as boolean hasCapability(Capability<T> cap, EnumFacing side) and T hasCapability(Capability<T> cap, EnumFacing side), and the default instance factory is accessed through a method we will call getDefaultInstance.

Storage

At this point, you have a way to expose capabilities from objects, but we haven't talked about how to save and load yet. For the basic objects, that's easy: we already have writeToNBT and readFromNBT on the objects, so just use those. Problem solved, right?

Well, no, of course not. What about the attached providers? They also need a way to save their data into the object! So let’s give the providers the choice to have an optional interface for saving an loading. For that, INBTSerializable is a perfect choice. But in order to allow people to use anonymous classes, we need to have a single interface in it, so we'll make a new interface, ICapabilitySerializable, which builds upon ICapabilityProvider and adds INBTSerializable to it.

Then, when the base object loads, Forge will iterate through the capability providers and call deserializeNBT on each one, and when it saves, it will iterate and call serializeNBT on them.

What did you say? Forge needs a way to know which data corresponds to which provider? Well… let's add an identifier to the provider, maybe?

A new method… getKey? No that wouldn't work, because it would add the method also to the basic objects (Entity and such), since they all implement ICapabilityProvider. So rather than that, we will add this key to the registration methods on the attach event, so that the code attaching the providers is the one responsible for assigning them a key.

Okay, now our providers can save and load, both internal ones, and attached ones. What remains is how to actually tell the "features" to save and load. Remember, some of them may be explicit, but others may be unknown, obtained through getDefaultInstance.

How do you save and load, a class that you don't know how to save or load? Cue the IStorage interface.

The idea is simple: give the Capability an instance of this interface, and it will allow any user of the capability to call readNBT/writeNBT methods for it, without ever worrying about what they do.

Injection

We have seen how people will be able to query capabilities. We have seen how people can attach capabilities to other people's objects. We have seen how providers can load and save themselves. But there is still one loose thread.

How do we actually obtain the Capability object itself? For the original creator, this would be easy, since they can just assign it to a field somewhere, but what about users who want weak dependencies?

The provided solution is the CapabilityInject annotation.

You tell it which feature’s Capability object to inject, and it will assign the corresponding instance into the variable. This has one benefit over just a global static registry: if the class for the capability is missing, the field will just remain null, and the code will still load (the only hard references are in annotations, which can be discarded).

However, you may also want to run code only if a Capability exists, and as early as possible. This is solved by making the annotation also be applicable to methods. Specifically, static methods with a single parameter of type Capability.

This closes the circle. Thanks to CapabilityInject, default instances, and IStorage, you can provide capabilities purely through weak binding, without ever once having a hard reference to any class or interface that would prevent loading your code.

Registration

In closing, all that is left is to tell Forge about our shiny new capability, so that it can notify other mods about it.

For this, we will need to use the CapabilityManager class, which is a singleton, and contains a method to register the capabilities. It handles both constructing the Capability instance, and injecting it everywhere.

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