注册表

注册表

注册是让游戏认识 Mod 对象(例如物品、方块、实体等)的过程。没有注册,游戏就不知道这些对象,可能出现难以解释的行为或崩溃。

简单来说,注册表是对映射的封装:将注册名映射到已注册对象,这些对象通常称为注册表条目。同一注册表中的注册名必须唯一,但不同注册表可以使用相同的注册名。最常见的例子是方块与其物品形式:它们分别位于 BLOCKS 和 ITEMS 注册表中,却可以使用相同名称。

每个注册对象都有唯一的注册名,以 Identifier 表示。例如泥土方块是 minecraft:dirt,僵尸是 minecraft:zombie。Mod 对象应使用自己的 Mod ID 作为命名空间,而不是 minecraft。

原版与 Mod 的区别

要理解 NeoForge 注册系统的设计,先看 Minecraft 如何处理。这里以方块注册表为例,其他注册表大多类似。

注册表通常注册单例,也就是每个条目只存在一个实例。例如,游戏中所有石头方块实际上都是同一个石头对象被重复显示。需要使用石头时,引用已注册的方块实例即可。

Minecraft 在 Blocks 类中注册所有方块。它的 register 方法调用 Registry#register(),第一个参数是 BuiltInRegistries.BLOCK。所有方块注册后,Minecraft 会根据方块列表执行各种检查,例如检查所有方块是否已加载模型。

这套机制能工作的主要原因是 Minecraft 足够早地加载了 Blocks 类。Minecraft 不会自动加载 Mod 类,因此 Mod 需要额外的处理方式。

注册方式

NeoForge 提供 DeferredRegister 类和 RegisterEvent 两种注册方式。前者是对后者的封装,推荐使用以减少出错。

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.BLOCK,
        // Our mod id.
        ExampleMod.MOD_ID
);

接着用以下方法之一,将注册条目添加为 static 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 Identifier.
        registryName -> new SlabBlock(...)
);

DeferredHolder<R, T extends R> 用来持有对象。类型参数 R 是注册表的内容类型,本例为 Block;T 是 supplier 提供的具体类型。第一个示例直接注册 Block,所以第二个类型参数也是 Block;第二个示例注册其子类 SlabBlock,因此这里使用 SlabBlock。

DeferredHolder<R, T extends R> 是 Supplier<T> 的子类型。需要已注册对象时调用 DeferredHolder#get()。也可以把字段声明为 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 Identifier.
        registryName -> new SlabBlock(...)
);

提示 少数位置明确要求 Holder 或 DeferredHolder,不能接受任意 Supplier。遇到这种情况,应根据需要将字段类型改回 Holder 或 DeferredHolder。

整个系统封装了注册表事件,因此最后还需要让 DeferredRegister 把自身接入相应事件:

//This is our mod constructor
public ExampleMod(IEventBus modBus) {
    //highlight-next-line
    ExampleBlocksClass.BLOCKS.register(modBus);
    //Other stuff here
}

说明 DeferredRegister 为方块、物品、数据组件和实体提供了带便捷方法的专用类型,分别是 DeferredRegister.Blocks、DeferredRegister.Items、DeferredRegister.DataComponents 和 DeferredRegister.Entities。

RegisterEvent

RegisterEvent 是第二种注册方式。每个注册表都会触发这个事件,时间在 Mod 构造函数之后、配置加载之前,因为 DeferredRegister 会在构造函数中注册内部处理器。该事件在 Mod 事件总线上触发。

@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.BLOCK,
            // Register your objects here.
            registry -> {
                registry.register(Identifier.fromNamespaceAndPath(MODID, "example_block_1"), new Block(...));
                registry.register(Identifier.fromNamespaceAndPath(MODID, "example_block_2"), new Block(...));
                registry.register(Identifier.fromNamespaceAndPath(MODID, "example_block_3"), new Block(...));
            }
    );
}

查询注册表

有时需要根据 ID 获取已注册对象,或根据对象获取 ID。注册表本质上是 ID(Identifier)与独立对象之间的可逆映射,因此两个方向都支持:

BuiltInRegistries.BLOCK.getValue(Identifier.fromNamespaceAndPath("minecraft", "dirt")); // returns the dirt block
BuiltInRegistries.BLOCK.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.BLOCK.getValue(Identifier.fromNamespaceAndPath("yourmodid", "example_block")); // returns the example block
BuiltInRegistries.BLOCK.getKey(ExampleBlocksClass.EXAMPLE_BLOCK.get()); // returns the resource location "yourmodid:example_block"

也可以检查对象是否存在,不过只能通过键来检查:

BuiltInRegistries.BLOCK.containsKey(Identifier.fromNamespaceAndPath("minecraft", "dirt")); // true
BuiltInRegistries.BLOCK.containsKey(Identifier.fromNamespaceAndPath("create", "brass_ingot")); // true only if Create is installed

如最后一行所示,这适用于任意 Mod ID,因此很适合检查其他 Mod 是否提供了某种物品。

还可以遍历注册表中的所有键或条目,条目使用 Java 的 Map.Entry 类型:

for (Identifier id : BuiltInRegistries.BLOCK.keySet()) {
    // ...
}
for (Map.Entry<ResourceKey<Block>, Block> entry : BuiltInRegistries.BLOCK.entrySet()) {
    // ...
}

提示 查询操作始终使用原版 Registry,而非 DeferredRegister,因为后者只是注册工具。

重要警告 只有注册完成后才可以安全查询。注册尚在进行时,不要查询注册表!

自定义注册表

自定义注册表可以提供供附属 Mod 扩展的系统。例如,Mod 添加法术时,可以将法术设计为注册表,让其他 Mod 直接添加法术,无需你额外修改代码。注册表还能够自动处理条目同步等工作。

先创建注册表键和注册表本身:

// 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(Identifier.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(Identifier.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();

然后在 NewRegistryEvent 中将其注册到根注册表,让游戏知道这个注册表的存在:

@SubscribeEvent // on the mod event bus
public static void registerRegistries(NewRegistryEvent event) {
    event.register(SPELL_REGISTRY);
}

现在即可像其他注册表一样,使用 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(Identifier.fromNamespaceAndPath("yourmodid", "example_spell"), () -> new Spell(...));
    });
}

数据包注册表

数据包注册表也称为动态注册表,或根据主要用途称为世界生成注册表。它在世界加载时从数据包 JSON 读取内容,而不是在游戏启动时加载。原版的此类注册表主要包括大多数世界生成注册表,以及少数其他注册表。

它允许通过 JSON 文件指定内容,因此无需编写代码;若不想手写 JSON,可使用数据生成。每个数据包注册表都有用于序列化的 Codec,其 ID 决定了数据包路径:

  • Minecraft 的数据包注册表使用 data/yourmodid/registrypath,例如 data/yourmodid/worldgen/biome,其中 worldgen/biome 是注册表路径。
  • 其他数据包注册表(NeoForge 或 Mod 的注册表)使用 data/yourmodid/registrynamespace/registrypath,例如 data/yourmodid/neoforge/biome_modifier,其中 neoforge 是注册表命名空间,biome_modifier 是注册表路径。

数据包注册表可以从 RegistryAccess 中获得。服务端调用 ServerLevel#registryAccess(),客户端调用 Minecraft.getInstance().getConnection()#registryAccess()。客户端必须已连接世界,否则连接为 null。随后可以像使用其他注册表一样,取得具体元素或遍历内容。

自定义数据包注册表

自定义数据包注册表无需构造 Registry,只需要注册表键,以及至少一个用于序列化和反序列化内容的 Codec。继续用前面的法术示例,将它注册为数据包注册表的代码如下:

public static final ResourceKey<Registry<Spell>> SPELL_REGISTRY_KEY = ResourceKey.createRegistryKey(Identifier.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)
    );
}

数据包注册表的数据生成

手写全部 JSON 既繁琐又容易出错,因此 NeoForge 提供数据提供器来生成文件,内置和自定义数据包注册表都可以使用。

先创建 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)
    });

实际注册对象的是 bootstrap lambda 参数,其类型为 BootstrapContext。调用它的 #register 即可注册对象:

// The resource key of our object.
public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
    Registries.CONFIGURED_FEATURE,
    Identifier.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 -> {
        // ...
    });

需要时还可通过 BootstrapContext 查询其他注册表的条目:

public static final ResourceKey<ConfiguredFeature<?, ?>> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create(
    Registries.CONFIGURED_FEATURE,
    Identifier.fromNamespaceAndPath(MOD_ID, "example_configured_feature")
);
public static final ResourceKey<PlacedFeature> EXAMPLE_PLACED_FEATURE = ResourceKey.create(
    Registries.PLACED_FEATURE,
    Identifier.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
        ));
    });

最后,在实际的数据提供器中使用 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.
    event.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`
    // ...
}

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。