The Registry Modification API is a framework for adding future API which easily allows mutating or adding entries into both the built-in registries and the data-driven registries.
There are several definitions to cover first:
Built-in Registry: A registry that is unmodifiable by datapacks. They are created/loaded via static initializers
in classes like Blocks
, Items
, GameEvent
, and many more.
Data-driven Registry: A registry which is loaded entirely from .json
files within the vanilla jar. Code in the source
exists to create the objects, but it is only used for the data generator functionality
of the vanilla jars (used to create those .json
files).
Writable Registry: A writable registry is a registry which is in a state to be accepting new entries. This relates to the next definition.
Freeze: In the context of registries, freezing means the registry cannot change anymore. After the initial load of entries
via static initializers (for built-in registries) or .json
files (for data-driven registries), registries are frozen
preventing further changes.
All API surrounding registry modification takes place in a plugin bootstrap context. This means that much of the rest of the Bukkit/Paper API will not be available for use as it hasn't been initialized yet.
There will be a bunch of ...Keys
utility classes containing all fields of type TypedKey<T>
. These keys represent vanilla
game objects that are in a Registry
. These fields will always be an exact representation of what vanilla keys exist for that version of Minecraft. They may change on every minor version bump as Mojang frequently adds, renames, or removes these. Why do we need these if I can just get the key for a GameEvent
from GameEvent.BLOCK_OPEN.getKey()
? Well, when these registry events are ran, the vanilla Registry
has not been created yet. In order for the fields inside the API's GameEvent
to be filled when the class is statically initialized, the vanilla registry must have been fully created and frozen. So we cannot access the keys via those fields because those fields cannot exist yet. The fields in these ...Keys
utility classes are the solution to this problem.
Each field is of type TypedKey<T>
which the generic parameter being the API type, like GameEvent
. TypeKey<T>
will be used
to specify what object you want to modify or what the key of a newly created object will be.
There are 2 events used for either modification of an entry, or adding completely new entries. Support for modification or addition of entries is not universal, some registries may only support modification not addition, some both, some none at all. These events do not use the existing event system, but a separate Lifecycle Event System.
The registry addition event is handled right before an entry is about to be registered allowing modification of the entry. You check which object is being added to the registry by comparing the TypedKey<T>
s in order to modify only specific objects.
The registry pre-freeze event is handled right before a registry is frozen. It provides a WritableRegistryView
which allows adding of new entries (which calls the addition event). You can provide a new TypedKey<T>
in order to create a new object.
In both of these events, interaction with the value is done through a builder pattern as it is the best way to keep existing immutable API types, like GameEvent while allowing modification to an almost-built GameEvent.
Currently in the API you can access instances of Registry via static constants on org.bukkit.Registry
. Going forward, any data-driven registries that exist there will be deprecated and the new way of obtaining them is via RegistryAccess
to which you pass a RegistryKey
for the specific registry you want to obtain. All builtin or data-driven registry instances will be available through the RegistryAccess
going forward, but only new built-in registries will have constants added to org.bukkit.Registry
.
This is being done because data-driven registries do not exist when the org.bukkit.Registry
class is loaded so the fields cannot properly be filled.
This example is modifying a specific GameEvent
to have a larger radius and is adding a new GameEvent
.
static final TypedKey<GameEvent> NEW_EVENT = GameEventKeys.create(Key.key("machine_maker", "best_event"));
@Override
public void bootstrap(@NotNull BootstrapContext context) {
final LifecycleEventManager<BootstrapContext> lifecycles = context.getLifecycleManager();
// registers a new handler for the prefreeze event for the game event registry
lifecycles.registerEventHandler(RegistryEvents.GAME_EVENT.preFreeze().newHandler(event -> {
// the RegistryView provided here is writable so you can register new objects
event.registry().register(NEW_EVENT, builder -> {
builder.range(2);
});
}));
// registers a handler for the addition event
lifecycles.registerEventHandler(RegistryEvents.GAME_EVENT.newAdditionHandler(event -> {
// checks if the object being registered is the block open game event
if (event.key().equals(GameEventKeys.BLOCK_OPEN)) {
// multiplies the range by 2
event.builder().range(event.builder().range() * 2);
}
}));
}