可扩展枚举

可扩展枚举

可扩展枚举对某些原版枚举进行增强,允许添加新枚举项。实现方式是在运行时修改枚举编译后的字节码,插入新元素。

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<?> 对象调用 Class#cast() 转换返回值,使 ClassCastException 在 Mod 代码中抛出。

警告 提供参数值的字段或方法应放在单独的类中,避免意外过早加载 Mod 类。

某些参数还有额外规则:

  • 如果参数是与枚举上的 @IndexedEnum 注解关联的整数 ID,它会被忽略,并替换为枚举项的序号。如果在 JSON 中内联指定此参数,必须将其设为 -1,否则会抛出异常。
  • 如果参数是与枚举上的 @NamedEnum 注解关联的字符串名称,必须按 Identifier 使用的 namespace:path 格式添加 Mod ID 前缀,否则会抛出异常。

获取生成的常量

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

向 NeoForge 贡献

要为 NeoForge 新增一个可扩展枚举,至少需要完成以下两项:

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

根据枚举的具体情况,还可能需要执行以下操作:

  • 如果枚举有应与枚举项序号一致的整数 ID 参数,为枚举添加 @IndexedEnum。如果 ID 不是第一个参数,还要将其参数索引设为注解的值。
  • 如果枚举有用于序列化、因而需要命名空间的字符串名称参数,为枚举添加 @NamedEnum。如果名称不是第一个参数,还要将其参数索引设为注解的值。
  • 如果枚举会通过网络发送,为枚举添加 @NetworkedEnum,通过注解参数指定允许发送的方向:发往客户端、发往服务端或双向。
  • 如果枚举有不适合 Mod 使用的构造方法,例如依赖注册表对象,而枚举可能在 Mod 注册阶段之前初始化,应为这些构造方法添加 @ReservedConstructor。

提示 如果枚举确实被添加了新枚举项,getExtensionInfo 方法会在运行时被转换,提供动态生成的 ExtensionInfo。

// This is an example, not an actual enum within Vanilla

// The first argument must match the enum constant's ordinal
@net.neoforged.fml.common.asm.enumextension.IndexedEnum
// The second argument is a string that must be prefixed with the mod id
@net.neoforged.fml.common.asm.enumextension.NamedEnum(1)
// This enum is used in networking and must be checked for mismatches between the client and server
@net.neoforged.fml.common.asm.enumextension.NetworkedEnum(net.neoforged.fml.common.asm.enumextension.NetworkedEnum.NetworkCheck.BIDIRECTIONAL)
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(26.1)。中文翻译依据提交 4a680cdd4d15。