可扩展枚举(Extensible Enums)
可扩展枚举(Extensible Enums)是对特定原版(Vanilla)枚举类型的增强,允许你向其中添加新的枚举项。这一机制通过在运行时修改枚举的已编译字节码来实现,从而动态添加新的元素。
IExtensibleEnum
所有支持扩展的枚举类型都会实现 IExtensibleEnum 接口。该接口作为标记,便于 RuntimeEnumExtender 启动插件服务识别哪些枚举需要被转换和增强。
你不应该在自己的枚举类型上实现该接口。根据实际需求,请优先使用映射(map)或注册表(registry)。
没有被官方补丁处理、实现该接口的枚举类型,无法通过 mixin 或 coremod 等方式后补添加该接口,因为转换器的执行顺序不允许这样做。
创建枚举项(Enum Entry)
要创建新的枚举项,需要新建一个 JSON 文件,并在 neoforge.mods.toml 的 [[mods]] 块中通过 enumExtensions 条目进行引用。指定的路径必须是相对于 resources 目录的相对路径:
# 在 neoforge.mods.toml 中:
[[mods]]
## 文件路径相对于资源输出目录,或者编译后 jar 包内的根路径
## 'resources' 目录即资源的根输出目录
enumExtensions="META-INF/enumextensions.json"
条目的定义包括目标枚举的类名、新字段的名称(必须以 mod ID 为前缀)、用于构造该枚举项的构造方法描述符(descriptor),以及传递给该构造方法的参数。
{
"entries": [
{
// 要添加条目的枚举类
"enum": "net/minecraft/world/item/ItemDisplayContext",
// 新条目的字段名,必须以模组 ID 为前缀
"name": "EXAMPLEMOD_STANDING",
// 要使用的构造方法
"constructor": "(ILjava/lang/String;Ljava/lang/String;)V",
// 直接提供的常量参数
"parameters": [ -1, "examplemod:standing", null ]
},
{
"enum": "net/minecraft/world/item/Rarity",
"name": "EXAMPLEMOD_CUSTOM",
"constructor": "(ILjava/lang/String;Ljava/util/function/UnaryOperator;)V",
// 要使用的参数,通过给定类中的一个 EnumProxy<Rarity> 字段引用提供
"parameters": {
"class": "example/examplemod/MyEnumParams",
"field": "CUSTOM_RARITY_ENUM_PROXY"
}
},
{
"enum": "net/minecraft/world/damagesource/DamageEffects",
"name": "EXAMPLEMOD_TEST",
"constructor": "(Ljava/lang/String;Ljava/util/function/Supplier;)V",
// 要使用的参数,通过给定类中的一个方法引用提供
"parameters": {
"class": "example/examplemod/MyEnumParams",
"method": "getTestDamageEffectsParameter"
}
}
]
}
public class MyEnumParams {
public static final EnumProxy<Rarity> CUSTOM_RARITY_ENUM_PROXY = new EnumProxy<>(
Rarity.class, -1, "examplemod:custom", (UnaryOperator<Style>) style -> style.withItalic(true)
);
public static Object getTestDamageEffectsParameter(int idx, Class<?> type) {
return type.cast(switch (idx) {
case 0 -> "examplemod:test";
case 1 -> (Supplier<SoundEvent>) () -> SoundEvents.DONKEY_ANGRY;
default -> throw new IllegalArgumentException("Unexpected parameter index: " + idx);
});
}
}
构造方法(Constructor)
构造方法(constructor)必须以 方法描述符 的形式指定,并且只能包含源代码中可见的参数,省略掉隐藏的常量名和序号参数。
如果某个构造方法被 @ReservedConstructor 注解标记,则该构造方法不能用于模组添加的枚举常量。
参数(Parameters)
参数可以通过三种方式指定,不同方式对参数类型有不同的限制:
- 直接在 JSON 文件中以内联数组的形式传递常量(仅允许原始值、字符串(String),以及为任何引用类型传递 null)
- 作为对模组(mod)中某个类的
EnumProxy<TheEnum>类型字段的引用(参见上文中的EnumProxy示例)- 第一个参数指定目标枚举(enum),后续参数将传递给枚举的构造函数
- 作为返回
Object的方法引用,该方法的返回值将作为参数值使用。该方法必须严格接收两个参数,类型分别为int(参数索引)和Class<?>(参数期望的类型)- 应使用
Class<?>对象进行类型转换(Class#cast()),以保证在模组代码中能捕获到ClassCastException异常。
- 应使用
作为参数值来源的字段和/或方法应放在单独的类中,以避免过早地意外加载模组类。
某些参数有额外的规则:
- 如果参数是与枚举上的
@IndexedEnum注解相关的 int ID 参数,则该参数会被忽略,并由条目的序号(ordinal)替代。如果该参数在 JSON 中以内联方式指定,则必须填为-1,否则会抛出异常。 - 如果参数是与枚举上的
@NamedEnum注解相关的字符串(String)名称参数,则必须以namespace:path格式(即ResourceLocation中常见的格式)加上模组 ID 作为前缀,否则会抛出异常。
获取生成的常量(Retrieving the Generated Constant)
生成的枚举常量可以通过 TheEnum.valueOf(String) 方法获取。如果参数是通过字段引用提供的,也可以通过 EnumProxy 对象的 EnumProxy#getValue() 方法获取该常量。
参与 NeoForge 开发(Contributing to NeoForge)
要向 NeoForge 添加一个新的可扩展枚举(extensible enum),至少需要做以下两件事:
- 让该枚举实现
IExtensibleEnum接口,以标记该枚举应由RuntimeEnumExtender进行转换。 - 添加一个
getExtensionInfo方法,并返回ExtensionInfo.nonExtended(TheEnum.class)。
根据枚举的具体情况,可能还需要进一步的操作:
- 如果枚举(enum)有一个 int 类型的 ID 参数,并且该参数应与条目的序号(ordinal)保持一致,那么应为该枚举添加
@NumberedEnum注解。如果 ID 不是第一个参数,则注解的值应为 ID 参数的索引。 - 如果枚举有一个 String 类型的名称参数用于序列化(serialization),并且因此需要带命名空间(namespaced),那么应为该枚举添加
@NamedEnum注解。如果名称不是第一个参数,则注解的值应为名称参数的索引。 - 如果该枚举会通过网络发送,则应为其添加
@NetworkedEnum注解,注解的参数用于指定允许发送的方向(clientbound、serverbound 或 bidirectional)。 - 如果枚举的构造函数(constructor)无法被模组(mod)使用(例如需要注册表对象,而这些对象可能在模组注册之前就已初始化),则应为其添加
@ReservedConstructor注解。
如果该枚举实际被扩展添加了条目,则 getExtensionInfo 方法会在运行时被转换为动态生成的 ExtensionInfo 实例。
// 这是一个示例,不是 Vanilla 中的实际枚举
public enum ExampleEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum {
// VALUE_1 这里代表名称参数
VALUE_1(0, "value_1", false),
VALUE_2(1, "value_2", true),
VALUE_3(2, "value_3");
ExampleEnum(int arg1, String arg2, boolean arg3) {
// ...
}
ExampleEnum(int arg1, String arg2) {
this(arg1, arg2, false);
}
public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() {
// 返回未扩展的 ExtensionInfo
return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(ExampleEnum.class);
}
}