Registries
Registration is the process of taking the objects of a mod (such as items, blocks, entities, etc.) and making them known to the game. Registering things is important, as without registration the game will simply not know about these objects, which will cause unexplainable behaviors and crashes.
注册是将模组中的对象(如物品 、 方块 、实体等)告知游戏的过程。注册至关重要,因为未经注册的游戏将无法识别这些对象,进而导致难以解释的行为和崩溃。
A registry is, simply put, a wrapper around a map that maps registry names (read on) to registered objects, often called registry entries. Registry names must be unique within the same registry, but the same registry name may be present in multiple registries. The most common example for this are blocks (in the BLOCKS
registry) that have an item form with the same registry name (in the ITEMS
registry).
简单来说,注册表是一个映射包装器,它将注册名称(见下文)与已注册对象(通常称为注册条目)关联起来。同一注册表中的注册名称必须唯一,但相同注册名称可存在于多个注册表中。最常见的例子是 BLOCKS
注册表中的方块,它们具有相同注册名称的物品形态(位于 ITEMS
注册表)。
Every registered object has a unique name, called its registry name. The name is represented as a ResourceLocation
. For example, the registry name of the dirt block is minecraft:dirt
, and the registry name of the zombie is minecraft:zombie
. Modded objects will of course not use the minecraft
namespace; their mod id will be used instead.
每个注册对象都有唯一的名称,称为注册名。该名称以 ResourceLocation
形式表示。例如,泥土方块的注册名为 minecraft:dirt
,僵尸的注册名为 minecraft:zombie
。模组对象当然不会使用 minecraft
命名空间,而是使用其模组 ID。
Vanilla vs. Modded 原版与模组对比
To understand some of the design decisions that were made in NeoForge's registry system, we will first look at how Minecraft does this. We will use the block registry as an example, as most other registries work the same way.
要理解 NeoForge 注册表系统中的某些设计决策,我们首先需要了解 Minecraft 的实现方式。我们将以方块注册为例进行说明,因为大多数其他注册表的工作方式与此相同。
Registries generally register singletons. This means that all registry entries exist exactly once. For example, all stone blocks you see throughout the game are actually the same stone block, displayed many times. If you need the stone block, you can get it by referencing the registered block instance.
注册表通常注册的是单例对象 。这意味着所有注册条目都严格唯一存在。例如,你在游戏中看到的所有石头方块实际上都是同一个石头方块的多次显示。如果需要获取石头方块,可以通过引用已注册的方块实例来获得。
Minecraft registers all blocks in the Blocks
class. Through the register
method, Registry#register()
is called, with the block registry at BuiltInRegistries.BLOCK
being the first parameter. After all blocks are registered, Minecraft performs various checks based on the list of blocks, for example the self check that verifies that all blocks have a model loaded.
Minecraft 将所有方块注册在 Blocks
类中。通过 register
方法调用 Registry#register()
,其中位于 BuiltInRegistries.BLOCK
的方块注册表作为第一个参数。在所有方块注册完成后,Minecraft 会根据方块列表执行各种检查,例如验证所有方块是否已加载模型的自我检查。
The main reason all of this works is that Blocks
is classloaded early enough by Minecraft. Mods are not automatically classloaded by Minecraft, and thus workarounds are needed.
这一切能够正常运作的主要原因是 Blocks
类被 Minecraft 足够早地加载。而模组不会被 Minecraft 自动加载,因此需要采用变通方案。
Methods for Registering 注册方法
NeoForge offers two ways to register objects: the DeferredRegister
class, and the RegisterEvent
. Note that the former is a wrapper around the latter, and is recommended in order to prevent mistakes.
NeoForge 提供了两种注册对象的方式:DeferredRegister
类和 RegisterEvent
事件。需要注意的是,前者是对后者的封装,推荐使用以避免错误。
DeferredRegister
We begin by creating our DeferredRegister
:
我们首先创建 DeferredRegister
:
public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(
// The registry we want to use.
// Minecraft's registries can be found in BuiltInRegistries, NeoForge's registries can be found in NeoForgeRegistries.
// Mods may also add their own registries, refer to the individual mod's documentation or source code for where to find them.
BuiltInRegistries.BLOCKS,
// Our mod id.
ExampleMod.MOD_ID
);
We can then add our registry entries as static final fields using one of the following methods (see the article on Blocks for what parameters to add in new Block()
):
随后可以通过以下任一方法将注册项添加为静态 final 字段(关于 new Block()
中应添加哪些参数,请参阅方块专题文章 ):
public static final DeferredHolder<Block, Block> EXAMPLE_BLOCK_1 = BLOCKS.register(
// Our registry name.
"example_block",
// A supplier of the object we want to register.
() -> new Block(...)
);
public static final DeferredHolder<Block, SlabBlock> EXAMPLE_BLOCK_2 = BLOCKS.register(
// Our registry name.
"example_block",
// A function creating the object we want to register
// given its registry name as a ResourceLocation.
registryName -> new SlabBlock(...)
);
The class DeferredHolder<R, T extends R>
holds our object. The type parameter R
is the type of the registry we are registering to (in our case Block
). The type parameter T
is the type of our supplier. Since we directly register a Block
in the first example, we provide Block
as the second parameter. If we were to register an object of a subclass of Block
, for example SlabBlock
(as shown in the second example), we would provide SlabBlock
here instead.
DeferredHolder<R, T extends R>
类用于持有我们的对象。类型参数 R
表示目标注册表的类型(本例中为 Block
)。类型参数 T
表示供应器的类型。由于第一个示例中直接注册的是 Block
,因此第二个参数指定为 Block
。如果要注册 Block
的子类对象(如第二个示例中的 SlabBlock
),则应在此处改为指定 SlabBlock
。
DeferredHolder<R, T extends R>
is a subclass of Supplier<T>
. To get our registered object when we need it, we can call DeferredHolder#get()
. The fact that DeferredHolder
extends Supplier
also allows us to use Supplier
as the type of our field. That way, the above code block becomes the following:
DeferredHolder<R, T extends R>
是 Supplier<T>
的子类。当我们需要获取已注册对象时,可以调用 DeferredHolder#get()
方法。由于 DeferredHolder
继承自 Supplier
,我们也可以直接使用 Supplier
作为字段类型。这样上述代码块可简化为:
public static final Supplier<Block> EXAMPLE_BLOCK_1 = BLOCKS.register(
// Our registry name.
"example_block",
// A supplier of the object we want to register.
() -> new Block(...)
);
public static final Supplier<SlabBlock> EXAMPLE_BLOCK_2 = BLOCKS.register(
// Our registry name.
"example_block",
// A function creating the object we want to register
// given its registry name as a ResourceLocation.
registryName -> new SlabBlock(...)
);
Be aware that a few places explicitly require a Holder
or DeferredHolder
and will not just accept any Supplier
. If you need either of those two, it is best to change the type of your Supplier
back to Holder
or DeferredHolder
as necessary.
请注意,某些场景会明确要求使用 Holder
或 DeferredHolder
类型,而不会接受任意 Supplier
实现。若遇到这种情况,请根据需要将字段的 Supplier
类型改回 Holder
或 DeferredHolder
。
Finally, since the entire system is a wrapper around registry events, we need to tell the DeferredRegister
to attach itself to the registry events as needed:
最后,由于整个系统是基于注册事件构建的包装器,我们需要告知 DeferredRegister
在适当时机将自己挂接到注册事件上:
//This is our mod constructor
public ExampleMod(IEventBus modBus) {
ExampleBlocksClass.BLOCKS.register(modBus);
//Other stuff here
}
There are specialized variants of DeferredRegister
s for blocks, items, and data components that provide helper methods: DeferredRegister.Blocks
, DeferredRegister.Items
, DeferredRegister.DataComponents
, and DeferredRegister.Entities
respectively.
针对方块、物品和数据组件存在专门的 DeferredRegister
变体,它们提供了辅助方法:分别是 DeferredRegister.Blocks
、DeferredRegister.Items
、 DeferredRegister.DataComponents
以及 DeferredRegister.Entities
。
RegisterEvent
RegisterEvent
is the second way to register objects. This event is fired for each registry, after the mod constructors (since those are where DeferredRegister
s register their internal event handlers) and before the loading of configs. RegisterEvent
is fired on the mod event bus.RegisterEvent
是第二种注册对象的方式。这个事件会在每个注册表上触发,触发时机位于模组构造函数之后(因为 DeferredRegister
会在构造函数中注册其内部事件处理器)且在配置加载之前。RegisterEvent
事件会在模组事件总线上触发。
@SubscribeEvent // on the mod event bus
public static void register(RegisterEvent event) {
event.register(
// This is the registry key of the registry.
// Get these from BuiltInRegistries for vanilla registries,
// or from NeoForgeRegistries.Keys for NeoForge registries.
BuiltInRegistries.BLOCKS,
// Register your objects here.
registry -> {
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_1"), new Block(...));
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_2"), new Block(...));
registry.register(ResourceLocation.fromNamespaceAndPath(MODID, "example_block_3"), new Block(...));
}
);
}
Querying Registries 查询注册表
Sometimes, you will find yourself in situations where you want to get a registered object by a given id. Or, you want to get the id of a certain registered object. Since registries are basically maps of ids (ResourceLocation
s) to distinct objects, i.e. a reversible map, both of these operations work:
有时,您会遇到需要通过给定 ID 获取已注册对象的情况。或者,您可能想获取某个已注册对象的 ID。由于注册表本质上是将 ID(ResourceLocation
)映射到不同对象的可逆映射表,因此这两种操作都能实现:
BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // returns the dirt block
BuiltInRegistries.BLOCKS.getKey(Blocks.DIRT); // returns the resource location "minecraft:dirt"
// Assume that ExampleBlocksClass.EXAMPLE_BLOCK.get() is a Supplier<Block> with the id "yourmodid:example_block"
BuiltInRegistries.BLOCKS.getValue(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_block")); // returns the example block
BuiltInRegistries.BLOCKS.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block"
If you just want to check for the presence of an object, this is also possible, though only with keys:
若仅需检查对象是否存在,也可实现此功能,但仅限于通过键名进行检测
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCKS.containsKey(ResourceLocation.fromNamespaceAndPath("create", "brass_ingot")); // true only if Create is installed
As the last example shows, this is possible with any mod id, and thus a perfect way to check if a certain item from another mod exists.
如最后一个示例所示,这适用于任何模组 ID,因此是检查其他模组中是否存在特定物品的绝佳方法。
Finally, we can also iterate over all entries in a registry, either over the keys or over the entries (entries use the Java Map.Entry
type):
最后,我们还可以遍历注册表中的所有条目,既可以遍历键,也可以遍历条目(条目使用 Java 的 Map.Entry
类型):
for (ResourceLocation id : BuiltInRegistries.BLOCKS.keySet()) {
// ...
}
for (Map.Entry<ResourceKey<Block>, Block> entry : BuiltInRegistries.BLOCKS.entrySet()) {
// ...
}
Query operations always use vanilla Registry
s, not DeferredRegister
s. This is because DeferredRegister
s are merely registration utilities.
查询操作始终使用原版 Registry
,而非 DeferredRegister
。这是因为 DeferredRegister
仅是注册工具类。
Query operations are only safe to use after registration has finished. DO NOT QUERY REGISTRIES WHILE REGISTRATION IS STILL ONGOING!
查询操作仅在注册完成后使用是安全的。 注册流程尚未完成时,切勿查询注册表!
Custom Registries 自定义注册表
Custom registries allow you to specify additional systems that addon mods for your mod may want to plug into. For example, if your mod were to add spells, you could make the spells a registry and thus allow other mods to add spells to your mod, without you having to do anything else. It also allows you to do some things, such as syncing the entries, automatically.
自定义注册表允许您为模组扩展指定额外的系统接口。例如,若您的模组需要添加法术系统,您可以将法术设为注册表项,这样其他模组就能直接向您的模组添加新法术,而无需您进行额外操作。该机制还能自动实现某些功能,比如条目同步。
Let's start by creating the registry key and the registry itself:
让我们从创建注册表键和注册表本身开始:
// We use spells as an example for the registry here, without any details about what a spell actually is (as it doesn't matter).
// Of course, all mentions of spells can and should be replaced with whatever your registry actually is.
public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells"));
public static final Registry<YourRegistryContents> SPELL_REGISTRY = new RegistryBuilder<>(SPELL_REGISTRY_KEY)
// If you want to enable integer id syncing, for networking.
// These should only be used in networking contexts, for example in packets or purely networking-related NBT data.
.sync(true)
// The default key. Similar to minecraft:air for blocks. This is optional.
.defaultKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "empty"))
// Effectively limits the max count. Generally discouraged, but may make sense in settings such as networking.
.maxId(256)
// Build the registry.
.create();
Then, tell the game that the registry exists by registering them to the root registry in NewRegistryEvent
:
然后,通过将注册表注册到 NewRegistryEvent
中的根注册表来告知游戏该注册表的存在:
@SubscribeEvent // on the mod event bus
public static void registerRegistries(NewRegistryEvent event) {
event.register(SPELL_REGISTRY);
}
You can now register new registry contents like with any other registry, through both DeferredRegister
and RegisterEvent
:
现在你可以像注册其他注册表内容一样,通过 DeferredRegister
和 RegisterEvent
来注册新的注册表内容:
public static final DeferredRegister<Spell> SPELLS = DeferredRegister.create(SPELL_REGISTRY, "yourmodid");
public static final Supplier<Spell> EXAMPLE_SPELL = SPELLS.register("example_spell", () -> new Spell(...));
// Alternatively:
@SubscribeEvent // on the mod event bus
public static void register(RegisterEvent event) {
event.register(SPELL_REGISTRY_KEY, registry -> {
registry.register(ResourceLocation.fromNamespaceAndPath("yourmodid", "example_spell"), () -> new Spell(...));
});
}
Datapack Registries 数据包注册表
A datapack registry (also known as a dynamic registry or, after its main use case, worldgen registry) is a special kind of registry that loads data from datapack JSONs (hence the name) at world load, instead of loading them when the game starts. Default datapack registries most notably include most worldgen registries, among a few others.
数据包注册表(又称动态注册表,或根据其主要用途称为世界生成注册表)是一种特殊类型的注册表,它会在世界加载时从数据包的 JSON 文件中加载数据(因此得名),而非在游戏启动时加载。默认的数据包注册表主要包括大多数世界生成注册表以及其他少数几种。
Datapack registries allow their contents to be specified in JSON files. This means that no code (other than datagen if you don't want to write the JSON files yourself) is necessary. Every datapack registry has a Codec
associated with it, which is used for serialization, and each registry's id determines its datapack path:
数据包注册表允许通过 JSON 文件指定其内容。这意味着无需编写代码(除非你不想手动编写 JSON 文件,才需要使用数据生成器 )。每个数据包注册表都关联一个编解码器
用于序列化,且每个注册表的 ID 决定了其在数据包中的路径:
- Minecraft's datapack registries use the format
data/yourmodid/registrypath
(for exampledata/yourmodid/worldgen/biome
, whereworldgen/biome
is the registry path).
Minecraft 的数据包注册表采用data/你的模组 ID/注册表路径
格式(例如data/你的模组 ID/worldgen/biome
,其中worldgen/biome
就是注册表路径)。 - All other datapack registries (NeoForge or modded) use the format
data/yourmodid/registrynamespace/registrypath
(for exampledata/yourmodid/neoforge/biome_modifier
, whereneoforge
is the registry namespace andbiome_modifier
is the registry path).
其他所有数据包注册表(NeoForge 或模组添加的)均采用data/yourmodid/registrynamespace/registrypath
格式(例如data/yourmodid/neoforge/biome_modifier
,其中neoforge
是注册表命名空间,biome_modifier
是注册表路径)。
Datapack registries can be obtained from a RegistryAccess
. This RegistryAccess
can be retrieved by calling ServerLevel#registryAccess()
if on the server, or Minecraft.getInstance().getConnection()#registryAccess()
if on the client (the latter only works if you are actually connected to a world, as otherwise the connection will be null). The result of these calls can then be used like any other registry to get specific elements, or to iterate over the contents.
数据包注册表可以通过 RegistryAccess
获取。在服务端可通过调用 ServerLevel#registryAccess()
获取该 RegistryAccess
,在客户端则通过 Minecraft.getInstance().getConnection()#registryAccess()
获取(后者仅在已连接世界时有效,否则连接将为 null)。获取的注册表可像其他注册表一样用于查询特定元素或遍历内容。
Custom Datapack Registries
自定义数据包注册表
Custom datapack registries do not require a Registry
to be constructed. Instead, they just need a registry key and at least one Codec
to (de-)serialize its contents. Reiterating on the spells example from before, registering our spell registry as a datapack registry looks something like this:
自定义数据包注册表无需构建 Registry
对象,仅需提供注册表键和至少一个 Codec
编解码器来实现内容(反)序列化。以前文的法术系统为例,将法术注册表声明为数据包注册表的实现方式如下:
public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath("yourmodid", "spells"));
@SubscribeEvent // on the mod event bus
public static void registerDatapackRegistries(DataPackRegistryEvent.NewRegistry event) {
event.dataPackRegistry(
// The registry key.
SPELL_REGISTRY_KEY,
// The codec of the registry contents.
Spell.CODEC,
// The network codec of the registry contents. Often identical to the normal codec.
// May be a reduced variant of the normal codec that omits data that is not needed on the client.
// May be null. If null, registry entries will not be synced to the client at all.
// May be omitted, which is functionally identical to passing null (a method overload
// with two parameters is called that passes null to the normal three parameter method).
Spell.CODEC,
// A consumer which configures the constructed registry via the RegistryBuilder.
// May be omitted, which is functionally identical to passing builder -> {}.
builder -> builder.maxId(256)
);
}
Data Generation for Datapack Registries
数据包注册表的数据生成
Since writing all the JSON files by hand is both tedious and error-prone, NeoForge provides a data provider to generate the JSON files for you. This works for both built-in and your own datapack registries.
由于手动编写所有 JSON 文件既繁琐又容易出错,NeoForge 提供了数据提供器来为您生成这些 JSON 文件。这既适用于内置注册表,也适用于您自定义的数据包注册表。
First, we create a RegistrySetBuilder
and add our entries to it (one RegistrySetBuilder
can hold entries for multiple registries):
首先,我们创建一个 RegistrySetBuilder
并将条目添加其中(一个 RegistrySetBuilder
可容纳多个注册表的条目):
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
// Register configured features through the bootstrap context (see below)
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// Register placed features through the bootstrap context (see below)
});
The bootstrap
lambda parameter is what we actually use to register our objects. It has the type BootstrapContext
. To register an object, we call #register
on it, like so:bootstrap
lambda 参数是我们实际用于注册对象的接口。其类型为 BootstrapContext
。注册对象时,我们调用其 #register
方法,如下所示:
// The resource key of our object.
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
bootstrap.register(
// The resource key of our configured feature.
EXAMPLE_CONFIGURED_FEATURE,
// The actual configured feature.
new ConfiguredFeature<>(Feature.ORE, new OreConfiguration(...))
);
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
// ...
});
The BootstrapContext
can also be used to lookup entries from another registry if needed:BootstrapContext
也可用于在需要时从其他注册表中查找条目:
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
Registries.CONFIGURED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
public static final ResourceKey<PlacedFeature> EXAMPLE_PLACED_FEATURE = ResourceKey.create(
Registries.PLACED_FEATURE,
ResourceLocation.fromNamespaceAndPath(MOD_ID, "example_placed_feature")
);
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, bootstrap -> {
bootstrap.register(EXAMPLE_CONFIGURED_FEATURE, ...);
})
.add(Registries.PLACED_FEATURE, bootstrap -> {
HolderGetter<ConfiguredFeature<?, ?>> otherRegistry = bootstrap.lookup(Registries.CONFIGURED_FEATURE);
bootstrap.register(EXAMPLE_PLACED_FEATURE, new PlacedFeature(
otherRegistry.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // Get the configured feature
List.of() // No-op when placement happens - replace with whatever your placement parameters are
));
});
Finally, we use our RegistrySetBuilder
in an actual data provider, and register that data provider to the event:
最后,我们在实际的数据提供者中使用 RegistrySetBuilder
,并将该数据提供者注册到事件中:
@SubscribeEvent // on the mod event bus
public static void onGatherData(GatherDataEvent.Client event) {
// Adds the generated registry objects to the current lookup provider for use
// in other datagen.
this.createDatapackRegistryObjects(
// Our registry set builder to generate the data from.
new RegistrySetBuilder().add(...),
// (Optional) A biconsumer that takes in any conditions to load the object
// associated with the resource key
conditions -> {
conditions.accept(resourceKey, condition);
},
// (Optional) A set of mod ids we are generating the entries for
// By default, supplies the mod id of the current mod container.
Set.of("yourmodid")
);
// You can use the lookup provider with your generated entries by either calling one
// of the `#create*` methods or grabbing the actual lookup via `#getLookupProvider`
// ...
}