数据映射

数据映射

数据映射包含可附加到注册对象上的、数据驱动且可重载的对象。它提供同步、冲突处理等功能,让游戏行为更易由数据控制,用户体验也更灵活。可以将标签视为“注册对象→布尔值”的映射,而数据映射是更灵活的“注册对象→对象”映射。与标签类似,多份数据会追加到对应映射,而非直接覆盖。

数据映射既可附加到静态内置注册表,也可附加到动态数据包注册表。通过 /reload 或其他重载服务端资源的方式,可以重新加载数据映射。

NeoForge 提供多种内置映射,替代常用的原版硬编码字段,详见链接文章。

文件位置

JSON 文件路径为 <mapNamespace>/data_maps/<registryNamespace>/<registryPath>/<mapPath>.json,其中 mapNamespace、mapPath 分别是映射 ID 的命名空间与路径;registryNamespace、registryPath 分别是注册表 ID 的命名空间与路径,注册表命名空间为 minecraft 时省略。

例如:

  • minecraft:item 注册表的 mymod:drop_healing 映射位于 mymod/data_maps/item/drop_healing.json。
  • minecraft:block 的 somemod:somemap 映射位于 somemod/data_maps/block/somemap.json。
  • somemod:custom 的 example:stuff 映射位于 example/data_maps/somemod/custom/stuff.json。

JSON 结构

文件可包含以下字段:

  • replace:布尔值,为 true 时先清空映射再添加此文件的值。Mod 不应随包提供此设置;它仅供希望覆盖映射的整合包开发者使用。
  • neoforge:conditions:加载条件列表。
  • values:注册 ID 或标签 ID 到待添加值的映射。值的结构由 codec 决定(见下文)。
  • remove:待移除注册 ID 或标签 ID 的列表。

添加值

假设为 minecraft:item 注册表定义了包含 amount 与 chance 两个浮点字段的映射对象,对应文件可写为:

{
    "values": {
        // Attach a value to the carrot item
        "minecraft:carrot": {
            "amount": 12,
            "chance": 1
        },
        // Attach a value to all items in the logs tag
        "#minecraft:logs": {
            "amount": 1,
            "chance": 0.1
        }
    }
}

映射可支持合并器,在两个 Mod 为同一物品添加值等冲突场景执行自定义合并。要绕过合并器,可在元素级指定 replace:

{
    "values": {
        // Overwrite the value of the carrot item
        "minecraft:carrot": {
            // highlight-next-line
            "replace": true,
            // The new value will be under a value sub-object
            "value": {
                "amount": 12,
                "chance": 1
            }
        }
    }
}

移除已有值

可提供待移除物品 ID 或标签 ID 的列表:

{
    // We do not want the potato to have a value, even if another mod's data map added it
    "remove": [
        "minecraft:potato"
    ]
}

移除操作在添加之后执行,因此可以先包含整个标签,再排除部分元素:

{
    "values": {
        "#minecraft:logs": { /* ... */ }
    },
    // Exclude crimson stem again
    "remove": [
        "minecraft:crimson_stem"
    ]
}

映射还可支持带额外参数的自定义移除器。此时将 remove 列表改为 JSON 对象,以待移除元素为键、额外数据为值。例如,若移除器对象序列化为字符串:

{
    "remove": {
        // The remover will be deserialized from the value (`somekey1` in this case)
        // and applied to the value attached to the carrot item
        "minecraft:carrot": "somekey1"
    }
}

自定义数据映射

先定义条目格式。数据映射条目必须不可变,因此 record 很适合。沿用 amount 与 chance 两个浮点值的例子:

public record ExampleData(float amount, float chance) {}

与许多系统相同,数据映射使用 codec 序列化和反序列化,因此需要为条目提供 codec:

public record ExampleData(float amount, float chance) {
    public static final Codec<ExampleData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
            Codec.FLOAT.fieldOf("amount").forGetter(ExampleData::amount),
            Codec.floatRange(0, 1).fieldOf("chance").forGetter(ExampleData::chance)
    ).apply(instance, ExampleData::new));
}

接着创建数据映射:

// In this example, we register the data map for the minecraft:item registry, hence we use Item as the generic.
// Adjust the types accordingly if you want to create a data map for a different registry.
public static final DataMapType<Item, ExampleData> EXAMPLE_DATA = DataMapType.builder(
        // The ID of the data map. Data map files for this data map will be located at
        // <yourmodid>:examplemod/data_maps/item/example_data.json.
        ResourceLocation.fromNamespaceAndPath("examplemod", "example_data"),
        // The registry to register the data map for.
        Registries.ITEM,
        // The codec of the data map entries.
        ExampleData.CODEC
).build();

最后,在 Mod 事件总线的 RegisterDataMapTypesEvent 中注册:

@SubscribeEvent // on the mod event bus
public static void registerDataMapTypes(RegisterDataMapTypesEvent event) {
    event.register(EXAMPLE_DATA);
}

同步

同步映射会把值发送到客户端。可在构建器上调用 #synced 启用:

public static final DataMapType<Item, ExampleData> EXAMPLE_DATA = DataMapType.builder(...)
        .synced(
                // The codec used for syncing. May be identical to the normal codec, but may also be
                // a codec with less fields, omitting parts of the object that are not required on the client.
                ExampleData.CODEC,
                // Whether the data map is mandatory or not. Marking a data map as mandatory will disconnect clients
                // that are missing the data map on their side; this includes vanilla clients.
                false
        ).build();

使用

由于可用于任意注册表,数据映射必须通过 Holder 查询,而非实际注册对象;并且只能用于引用 holder,不能用于 Direct holder。不过,Registry#wrapAsHolder、Registry#getHolder 及各类 builtInRegistryHolder 方法通常都返回引用,因此多数情况不成问题。

使用 Holder#getData(DataMapType) 查询值,没有附加值时返回 null。沿用 ExampleData,可以在玩家拾取物品时为其恢复生命值:

@SubscribeEvent // on the game event bus
public static void itemPickup(ItemPickupEvent event) {
    ItemStack stack = event.getItemStack();
    // Get a Holder<Item> via ItemStack#getItemHolder.
    Holder<Item> holder = stack.getItemHolder();
    // Get the data from the holder.
    //highlight-next-line
    ExampleData data = holder.getData(EXAMPLE_DATA);
    if (data != null) {
        // The values are present, so let's do something with them!
        Player player = event.getPlayer();
        if (player.getLevel().getRandom().nextFloat() > data.chance()) {
            player.heal(data.amount());
        }
    }
}

NeoForge 提供的全部映射也可用同样方式查询。

高级数据映射

高级映射使用 AdvancedDataMapType,而非其父类 DataMapType。它额外支持自定义合并器与移除器。对于值为 List、Map 等集合或类似结构的映射,强烈建议使用。

DataMapType 有 R(注册表类型)与 T(值类型)两个泛型;AdvancedDataMapType 额外具有 VR extends DataMapValueRemover<R, T>,让移除器数据生成具备正确的类型安全。

用 AdvancedDataMapType#builder() 创建 AdvancedDataMapType.Builder,它额外提供 #remover 与 #merger 设置移除器和合并器。同步等其他功能不变。

合并器

合并器解决多个数据包为同一对象提供值的冲突。默认合并器 DataMapValueMerger#defaultMerger 使用新值覆盖已有值(例如低优先级数据包的值),若这不符合需求就需要自定义实现。

合并器接收两个冲突值、它们附加的对象(以 Either<TagKey<R>, ResourceKey<R>> 表示,因为可能来自标签或单个对象)和所属注册表,并返回最终附加值。通常应尽可能合并,只有无法正常合并时才覆盖。数据包可通过元素上的 replace 绕过合并器(见添加值)。

例如,将整数附加到物品,可通过相加解决冲突:

public class IntMerger implements DataMapValueMerger<Item, Integer> {
    @Override
    public Integer merge(Registry<Item> registry,
            Either<TagKey<Item>, ResourceKey<Item>> first, Integer firstValue,
            Either<TagKey<Item>, ResourceKey<Item>> second, Integer secondValue) {
        return firstValue + secondValue;
    }
}

如果一个包为 minecraft:carrot 指定 12,另一个指定 15,最终值就是 27。若其中一个元素指定 replace 为 true,则使用该值;两者都指定时使用优先级更高的数据包值。

最后在构建器上指定合并器:

// We assume AdvancedData contains an integer property of some sort.
AdvancedDataMapType<Item, AdvancedData> ADVANCED_MAP = AdvancedDataMapType.builder(...)
        .merger(new IntMerger())
        .build();

建议 NeoForge 在 DataMapValueMerger 中提供列表、集合和映射的默认合并器。

移除器

与复杂数据的合并器类似,移除器可精确处理元素的 remove 子句。默认 DataMapValueRemover.Default.INSTANCE 移除指定对象的全部信息;若只想移除部分数据,需要自定义移除器。

传给构建器的 codec 用于解码移除器实例。移除器随后接收当前值及其来源,并返回包含替换值的 Optional;空 Optional 表示实际移除此值。

下面从基于 Map<String, String> 的数据映射中移除指定键:

public record MapRemover(String key) implements DataMapValueRemover<Item, Map<String, String>> {
    public static final Codec<MapRemover> CODEC = Codec.STRING.xmap(MapRemover::new, MapRemover::key);
    
    @Override
    public Optional<Map<String, String>> remove(Map<String, String> value, Registry<Item> registry, Either<TagKey<Item>, ResourceKey<Item>> source, Item object) {
        final Map<String, String> newMap = new HashMap<>(value);
        newMap.remove(key);
        return Optional.of(newMap);
    }
}

考虑以下数据文件:

{
    "values": {
        "minecraft:carrot": {
            "somekey1": "value1",
            "somekey2": "value2"
        }
    }
}

再看优先级更高的第二份文件:

{
    "remove": {
        // As the remover is decoded as a string, we can use a string as the value here.
        // If it were decoded as an object, we would have needed to use an object.
        "minecraft:carrot": "somekey1"
    }
}

两份文件应用后,最终内存中的结果相当于:

{
    "values": {
        "minecraft:carrot": {
            "somekey2": "value2"
        }
    }
}

与合并器一样,记得添加到构建器,此处直接使用 codec:

// We assume AdvancedData contains a Map<String, String> property of some sort.
AdvancedDataMapType<Item, AdvancedData> ADVANCED_MAP = AdvancedDataMapType.builder(...)
        .remover(MapRemover.CODEC)
        .build();

数据生成

继承 DataMapProvider 并重写 #gather,即可生成条目。沿用带 amount 与 chance 浮点值的 ExampleData:

public class MyDataMapProvider extends DataMapProvider {
    public MyDataMapProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> lookupProvider) {
        super(packOutput, lookupProvider);
    }
    
    @Override
    protected void gather() {
        // We create a builder for the EXAMPLE_DATA data map and add our entries using #add.
        builder(EXAMPLE_DATA)
                // We turn on replacing. Don't ever ship a mod like this! This is purely for educational purposes.
                .replace(true)
                // We add the value "amount": 10, "chance": 1 for all slabs. The boolean parameter controls
                // the "replace" field, which should always be false in a mod.
                .add(ItemTags.SLABS, new ExampleData(10, 1), false)
                // We add the value "amount": 5, "chance": 0.2 for apples.
                .add(Items.APPLE.builtInRegistryHolder(), new ExampleData(5, 0.2f), false) // Can also use Registry#wrapAsHolder to get the holder of a registry object
                // We remove wooden slabs again.
                .remove(ItemTags.WOODEN_SLABS)
                // We add a mod loaded condition for Botania, because why not.
                .conditions(new ModLoadedCondition("botania"));
    }
}

生成的 JSON 如下:

{
    "replace": true,
    "values": {
        "#minecraft:slabs": {
            "amount": 10,
            "chance": 1.0
        },
        "minecraft:apple": {
            "amount": 5,
            "chance": 0.2
        }
    },
    "remove": [
        "#minecraft:wooden_slabs"
    ],
    "neoforge:conditions": [
        {
            "type": "neoforge:mod_loaded",
            "modid": "botania"
        }
    ]
}

与其他数据提供器一样,记得添加到事件:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent event) {
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();

    // other providers here
    generator.addProvider(
            event.includeServer(),
            new MyDataMapProvider(output, lookupProvider)
    );
}

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