可扩展枚举

可扩展枚举

可扩展枚举是对特定原版枚举的一项增强,允许向其中添加新条目。实现方式是在运行时修改已编译的字节码,将新元素加入枚举。

IExtensibleEnum

所有允许添加新条目的枚举都会实现 IExtensibleEnum 接口。该接口是一个标记,供启动插件服务 RuntimeEnumExtender 识别需要转换的枚举。

警告 不要在自己的枚举上实现此接口。请根据具体用途改用映射或注册表。

由于转换器的运行顺序,未打补丁以实现该接口的枚举无法通过 Mixin 或 Coremod 为其补加此接口。

创建枚举条目

要创建新的枚举条目,需要创建 JSON 文件,并在 neoforge.mods.toml 的 [[mods]] 区块中使用 enumExtensions 条目引用该文件。指定的路径必须相对于 resources 目录:

# In neoforge.mods.toml:
[[mods]]
## The file is relative to the output directory of the resources, or the root path inside the jar when compiled
## The 'resources' directory represents the root output directory of the resources
enumExtensions="META-INF/enumextensions.json"

条目定义包含目标枚举的类名、新字段名称(必须以 Mod ID 为前缀)、用于构造该条目的构造函数描述符,以及传给该构造函数的参数。

{
    "entries": [
        {
            // The enum class the entry should be added to
            "enum": "net/minecraft/world/item/ItemDisplayContext",
            // The field name of the new entry, must be prefixed with the mod ID
            "name": "EXAMPLEMOD_STANDING",
            // The constructor to be used
            "constructor": "(ILjava/lang/String;Ljava/lang/String;)V",
            // Constant parameters provided directly.
            "parameters": [ -1, "examplemod:standing", null ]
        },
        {
            "enum": "net/minecraft/world/item/Rarity",
            "name": "EXAMPLEMOD_CUSTOM",
            "constructor": "(ILjava/lang/String;Ljava/util/function/UnaryOperator;)V",
            // The parameters to be used, provided as a reference to an EnumProxy<Rarity> field in the given class
            "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",
            // The parameters to be used, provided as a reference to a method in the given class
            "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);
        });
    }
}

构造函数

构造函数必须以方法描述符指定,并且只能包含源代码中可见的参数;隐藏的常量名称参数和序号参数必须省略。

如果构造函数带有 @ReservedConstructor 注解,Mod 就不能用它创建枚举常量。

参数

参数可以通过以下三种方式指定,但能否使用取决于参数类型:

  • 在 JSON 文件中以内联常量数组的形式提供(仅适用于基本类型、字符串,以及为任意引用类型传入 null)
  • 引用 Mod 中某个类里的 EnumProxy<TheEnum> 类型字段(见上面的 EnumProxy 示例)
    • 第一个参数指定目标枚举,后续参数将传给枚举构造函数
  • 引用一个返回 Object 的方法,其返回值就是要使用的参数值。该方法必须恰好包含两个参数,类型分别为 int(参数索引)和 Class<?>(参数的预期类型)
    • 应使用 Class#cast() 方法转换返回值类型,以便在 Mod 代码中保留 ClassCastException 异常信息。

警告 用于提供参数值的字段和/或方法应放在单独的类中,以免过早加载 Mod 类。

某些参数还有额外规则:

  • 如果参数是与枚举上 @IndexedEnum 注解相关联的 int ID 参数,它会被忽略,并由该条目的序号替代。如果在 JSON 中以内联方式指定了此参数,其值必须为 -1,否则会抛出异常。
  • 如果参数是与枚举上 @NamedEnum 注解相关联的字符串名称参数,则必须使用 Mod ID 作为命名空间前缀,格式与 ResourceLocation 的 namespace:path 相同,否则会抛出异常。

获取生成的常量

可以通过 TheEnum.valueOf(String) 获取生成的枚举常量。如果使用字段引用提供参数,也可以通过 EnumProxy 对象的 EnumProxy#getValue() 获取该常量。

为 NeoForge 贡献代码

要在 NeoForge 中新增可扩展枚举,至少需要完成以下两项工作:

  • 让该枚举实现 IExtensibleEnum,标记它需要由 RuntimeEnumExtender 转换。
  • 添加 getExtensionInfo 方法,并让它返回 ExtensionInfo.nonExtended(TheEnum.class)。

根据枚举的具体情况,还可能需要完成以下工作:

  • 如果枚举包含应与条目序号一致的 int ID 参数,且该参数不是第一个参数,则应使用 @NumberedEnum 注解该枚举,并将 ID 参数的索引设为注解值。
  • 如果枚举包含用于序列化、因而应带命名空间的字符串名称参数,且该参数不是第一个参数,则应使用 @NamedEnum 注解该枚举,并将名称参数的索引设为注解值。
  • 如果枚举会通过网络发送,则应使用 @NetworkedEnum 注解,并在注解中指定值可发送的方向(客户端接收、服务端接收或双向)。
    • 警告:NeoForge 实现枚举网络检查后,网络枚举还需要额外处理。
  • 如果枚举具有 Mod 无法使用的构造函数(例如,它要求注册表对象,而该枚举可能在 Mod 注册逻辑运行前初始化),则应使用 @ReservedConstructor 注解这些构造函数。

提示 如果枚举实际添加了条目,getExtensionInfo 方法会在运行时转换,以提供动态生成的 ExtensionInfo。

// This is an example, not an actual enum within Vanilla
public enum ExampleEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum {
    // VALUE_1 represents the name parameter here
    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() {
        return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(ExampleEnum.class);
    }
}

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