原文:MinecraftForge/Documentation · 目标版本:Forge 1.20.1
注册表¶
注册(Registration)是将模组的对象(如物品、方块、声音等)登记给游戏知晓的过程。注册非常重要,因为如果没有注册,游戏根本不会知道这些对象的存在,从而引发无法解释的异常行为和崩溃。
游戏中大多数需要注册的内容都由 Forge 注册表(Registry)处理。注册表是一种类似于映射(map)的对象,它为键分配值。Forge 使用以 ResourceLocation 为键的注册表来注册对象,这使得 ResourceLocation 可以充当对象的「注册名」。
每种可注册对象类型都有自己独立的注册表。要查看 Forge 包装了哪些注册表,请参阅 ForgeRegistries 类。同一个注册表内的所有注册名必须唯一;不过,不同注册表之间的名称不会相互冲突。例如,存在一个 Block 注册表和一个 Item 注册表,一个 Block 和一个 Item 可以注册相同的名称 example:thing 而互不冲突;但如果两个不同的 Block 或两个不同的 Item 注册了完全相同的名称,后注册的对象会覆盖先注册的对象。
注册方法¶
注册对象有两种正规方式:延迟注册器(DeferredRegister)类和 RegisterEvent 生命周期(Lifecycle)事件。
延迟注册器(DeferredRegister)¶
DeferredRegister 是注册对象的推荐方式。它既保留了静态初始化器(static initializer)的便捷性,又避免了随之而来的问题。它内部只是维护一个条目供应器(supplier)列表,并在 RegisterEvent 期间根据这些供应器注册对象。
下面是一个模组注册自定义方块的示例:
private static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
public static final RegistryObject<Block> ROCK_BLOCK = BLOCKS.register("rock", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE)));
public ExampleMod() {
BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus());
}
RegisterEvent 事件¶
RegisterEvent 是注册对象的第二种方式。这个事件会在模组构造函数之后、配置加载之前,为每个注册表各触发一次。对象通过 #register 注册,需要传入注册表的键、注册表对象的名称以及对象本身。此外还有一个 #register 重载,它接收一个辅助函数(helper)来按给定名称注册对象。建议使用这种方法,以避免不必要的对象创建。
示例如下(事件处理器注册在模组事件总线上):
@SubscribeEvent
public void register(RegisterEvent event) {
event.register(ForgeRegistries.Keys.BLOCKS,
helper -> {
helper.register(new ResourceLocation(MODID, "example_block_1"), new Block(...));
helper.register(new ResourceLocation(MODID, "example_block_2"), new Block(...));
helper.register(new ResourceLocation(MODID, "example_block_3"), new Block(...));
// ...
}
);
}
未被 Forge 包装的注册表¶
并非所有注册表都由 Forge 包装。其中一些是静态注册表(static registry),例如 LootItemConditionType,可以放心使用。还有一些是动态注册表(dynamic registry),例如 ConfiguredFeature 以及其它一些世界生成(worldgen)注册表,它们通常以 JSON 形式表示。DeferredRegister#create 有一个重载,允许模组开发者指定要为哪个原版(vanilla)注册表创建 RegistryObject。注册方式以及挂接到模组事件总线的方式与其它 DeferredRegister 相同。
Important
动态注册表的对象只能通过数据文件(如 JSON)注册,不能在代码中注册。
private static final DeferredRegister<LootItemConditionType> REGISTER = DeferredRegister.create(Registries.LOOT_CONDITION_TYPE, "examplemod");
public static final RegistryObject<LootItemConditionType> EXAMPLE_LOOT_ITEM_CONDITION_TYPE = REGISTER.register("example_loot_item_condition_type", () -> new LootItemConditionType(...));
Note
有些类本身无法注册,取而代之的是注册其对应的 *Type 类,并在前者的构造函数中使用。例如,BlockEntity 对应 BlockEntityType,Entity 对应 EntityType。这些 *Type 类本质上是工厂(factory),按需创建所包含的类型。
这些工厂通过各自的 *Type$Builder 类创建。示例如下(REGISTER 指的是 DeferredRegister<BlockEntityType>):
引用已注册对象¶
已注册的对象在创建并注册后,不应存储到字段中。每当 RegisterEvent 为该注册表触发时,都应重新创建并重新注册。这是为了支持未来版本的 Forge 能够动态加载和卸载模组。
已注册的对象必须始终通过 RegistryObject 或带有 @ObjectHolder 注解的字段来引用。
使用 RegistryObject¶
RegistryObject 可以在已注册对象可用后用于获取其引用。DeferredRegister 正是通过它来返回已注册对象的引用。当对应注册表的 RegisterEvent 触发后,这些引用以及 @ObjectHolder 注解都会被更新。
要获取 RegistryObject,请调用 RegistryObject#create,传入一个 ResourceLocation 和可注册对象的 IForgeRegistry。自定义注册表也可以通过传入注册表名称来使用。将 RegistryObject 存放在 public static final 字段中,并在需要已注册对象时调用 #get。
使用 RegistryObject 的示例:
public static final RegistryObject<Item> BOW = RegistryObject.create(new ResourceLocation("minecraft:bow"), ForgeRegistries.ITEMS);
// 假设 'neomagicae:mana_type' 是一个有效的注册表,'neomagicae:coffeinum' 是该注册表中的一个有效对象
public static final RegistryObject<ManaType> COFFEINUM = RegistryObject.create(new ResourceLocation("neomagicae", "coffeinum"), new ResourceLocation("neomagicae", "mana_type"), "neomagicae");
使用 @ObjectHolder¶
通过在类或字段上标注 @ObjectHolder,并提供足够的信息来构造一个 ResourceLocation 以标识特定注册表中的特定对象,可以将注册表中的已注册对象注入到 public static 字段中。
@ObjectHolder 的规则如下:
- 如果类上标注了
@ObjectHolder,则当类内字段未显式定义命名空间时,注解的值将作为该类内所有字段的默认命名空间 - 如果类上标注了
@Mod,则当类内被注解字段未显式定义命名空间时,模组 id 将作为该类内所有被注解字段的默认命名空间 - 满足以下条件的字段才会被考虑注入:
- 至少具有
public static修饰符; - 字段上标注了
@ObjectHolder,并且:- 显式定义了名称(name)值;且
- 显式定义了注册表名称(registry name)值
- 如果字段没有对应的注册表或名称,将抛出编译时异常。
- 如果生成的
ResourceLocation不完整或无效(路径中含有非法字符),将抛出异常 - 如果没有其它错误或异常发生,该字段将被注入
- 如果上述规则均不适用,则不采取任何操作(可能会记录一条日志消息)
带有 @ObjectHolder 注解的字段会在其注册表的 RegisterEvent 触发后被注入值,与 RegistryObject 的更新同步进行。
Note
如果注入时对象尚不存在于注册表中,将记录一条调试消息,并且不会注入任何值。
由于这些规则相当复杂,这里给出一些示例:
class Holder {
@ObjectHolder(registryName = "minecraft:enchantment", value = "minecraft:flame")
public static final Enchantment flame = null; // 有注解。[public static] 是必需的。[final] 可选。
// 注册表名称已显式定义:"minecraft:enchantment"
// 资源位置已显式定义:"minecraft:flame"
// 注入方式:从 [Enchantment] 注册表注入 "minecraft:flame"
public static final Biome ice_flat = null; // 字段上没有注解。
// 因此,该字段会被忽略。
@ObjectHolder("minecraft:creeper")
public static Entity creeper = null; // 有注解。[public static] 是必需的。
// 字段上未指定注册表。
// 因此,这会产生编译时异常。
@ObjectHolder(registryName = "potion")
public static final Potion levitation = null; // 有注解。[public static] 是必需的。[final] 可选。
// 注册表名称已显式定义:"minecraft:potion"
// 字段上未指定资源位置。
// 因此,这会产生编译时异常。
}
创建自定义 Forge 注册表¶
自定义注册表通常可以只是一个简单的键到值的映射(map)。这是一种常见的做法;然而,它会强制要求注册表必须存在,形成硬依赖(hard dependency),同时任何需要在逻辑端(sides)之间同步的数据都必须手动处理。自定义 Forge 注册表为创建软依赖(soft dependency)提供了简单的替代方案,并且带来更好的管理以及逻辑端之间的自动同步(除非另有指定)。由于这些对象同样使用 Forge 注册表,注册方式也得以标准化。
自定义 Forge 注册表借助 RegistryBuilder 创建,可以通过 NewRegistryEvent 或 DeferredRegister 两种方式。RegistryBuilder 类接收各种参数(如注册表的名称、id 范围,以及针对注册表上不同事件的各类回调)。NewRegistryEvent 触发结束后,新的注册表会被注册到 RegistryManager。
任何新创建的注册表都应使用其对应的注册方法来注册相关对象。
使用 NewRegistryEvent¶
使用 NewRegistryEvent 时,传入 RegistryBuilder 调用 #create 会返回一个由供应器包装的注册表。只有在 NewRegistryEvent 向模组事件总线发布完成之后,才能从供应器中获取该注册表。在 NewRegistryEvent 触发结束之前从供应器获取自定义注册表,会得到 null 值。
新的数据包注册表¶
可以通过模组事件总线上的 DataPackRegistryEvent$NewRegistry 事件添加新的数据包(datapack)注册表。该注册表通过 #dataPackRegistry 创建,需要传入代表注册表名称的 ResourceKey,以及用于对 JSON 数据进行编码和解码的 Codec(编解码器)。还可以提供一个可选的 Codec,用于将数据包注册表同步到客户端。
Important
数据包注册表不能用 DeferredRegister 创建,只能通过该事件创建。
通过 DeferredRegister 创建¶
DeferredRegister 方法同样是上述事件的另一种包装。一旦在常量字段中使用接收注册表名称和模组 id 的 #create 重载创建了 DeferredRegister,就可以通过 DeferredRegister#makeRegistry 构造注册表。该方法接收一个包含任何额外配置的 RegistryBuilder 供应器,并且默认已经填充了 #setName。由于该方法可以在任意时刻返回,因此返回的是供应器包装的 IForgeRegistry。在 NewRegistryEvent 触发之前从供应器获取自定义注册表,会得到 null 值。
Important
DeferredRegister#makeRegistry 必须在通过 #register 将 DeferredRegister 添加到模组事件总线之前调用。#makeRegistry 也会在 NewRegistryEvent 期间使用 #register 方法创建注册表。
处理缺失条目¶
在某些情况下,当模组更新或(更常见地)被移除时,某些注册表对象会不复存在。可以通过注册表事件中的第三个——MissingMappingsEvent——来指定处理缺失映射(mapping)的操作。在该事件中,可以通过传入注册表键和模组 id 调用 #getMappings 获取缺失映射列表,或通过传入注册表键调用 #getAllMappings 获取全部映射。
Important
MissingMappingsEvent 在 Forge 事件总线上触发。
对于每个 Mapping,可以从四种映射类型中选择一种来处理缺失的条目:
| 操作 | 说明 |
|---|---|
| IGNORE | 忽略缺失的条目并放弃该映射。 |
| WARN | 在日志中生成一条警告。 |
| FAIL | 阻止世界加载。 |
| REMAP | 将条目重新映射到已注册的非 null 对象。 |
如果未指定任何操作,则采取默认操作:通知用户存在缺失条目,并询问是否仍然希望加载世界。除重映射外的所有操作都会阻止其他注册表对象占据现有 id 的位置,以防相关条目日后重新加入游戏。