Skip to main content
Version: 1.21.4

数据映射(Data Maps)

数据映射(data map)是一种可以附加到已注册对象上的、基于数据驱动并可重新加载的对象集合。该系统让游戏行为的数据驱动变得更加简单易行,因为它们提供了如同步(syncing)或冲突解决(conflict resolution)等功能,从而带来了更好、更易配置的用户体验。你可以将 标签 理解为“注册表对象(registry object) ➜ 布尔值(boolean)”的映射,而数据映射则更灵活,是“注册表对象 ➜ 对象”的映射。类似于 标签,数据映射会在原有数据的基础上添加内容,而不是覆盖已有内容。

数据映射既可以附加到静态的内置注册表,也可以附加到动态、数据驱动的数据包注册表(datapack registry)。通过 /reload 命令或任何其他重新加载服务器资源的方式,数据映射都支持热重载。

NeoForge 提供了多种 内置数据映射,用于常见场景,替代了原版 Minecraft 中的硬编码字段。更多信息请参阅相关链接文章。

文件位置(File Location)

数据映射会从一个 JSON 文件中加载,该文件路径为 <mapNamespace>/data_maps/<registryNamespace>/<registryPath>/<mapPath>.json,其中:

  • <mapNamespace> 是数据映射 ID 的命名空间(namespace);
  • <mapPath> 是数据映射 ID 的路径(path);
  • <registryNamespace> 是注册表 ID 的命名空间(如果为 minecraft 可省略);
  • <registryPath> 是注册表 ID 的路径。

举例如下:

  • 如果数据映射名为 mymod:drop_healing,对应 minecraft:item 注册表(见下方示例),路径应为 mymod/data_maps/item/drop_healing.json
  • 如果数据映射名为 somemod:somemap,对应 minecraft:block 注册表,路径应为 somemod/data_maps/block/somemap.json
  • 如果数据映射名为 example:stuff,对应 somemod:custom 注册表,路径应为 example/data_maps/somemod/custom/stuff.json

JSON 结构(JSON Structure)

数据映射文件本身可以包含以下字段:

  • replace:布尔值(boolean),在添加本文件中的值之前会清空整个数据映射。此字段不应被模组(mod)发布时携带,仅供希望完全覆盖该映射的资源包开发者使用。
  • neoforge:conditions:一个 加载条件 列表。
  • values:一个将注册表 ID 或标签 ID 映射到对应值的映射表。具体值的结构由该数据映射的 codec(编解码器)定义(见下文)。
  • remove:要从数据映射中移除的注册表 ID 或标签 ID 列表。

添加值(Adding Values)

例如,假设我们有一个数据映射对象,针对 minecraft:item 注册表,每个映射值包含两个浮点型(float)键:amountchance。对应的数据映射文件可能如下所示:

{
"values": {
// 为胡萝卜物品附加一个值
"minecraft:carrot": {
"amount": 12,
"chance": 1
},
// 为 logs 标签下的所有物品附加一个值
"#minecraft:logs": {
"amount": 1,
"chance": 0.1
}
}
}

数据映射(data maps)可能支持 合并器,这会在发生冲突时(例如两个模组为同一个物品添加数据映射值时)触发自定义的合并行为。为了避免触发合并器,我们可以在元素级别指定 replace 字段,如下所示:

{
"values": {
// 覆盖胡萝卜物品的值
"minecraft:carrot": {
"replace": true,
// 新的值将放在 value 子对象下
"value": {
"amount": 12,
"chance": 1
}
}
}
}

移除已有值(Removing Existing Values)

移除元素可以通过指定要移除的物品 ID 或标签 ID 列表来实现:

{
// 即使其他模组的数据映射添加了它,我们也不希望土豆拥有一个值
"remove": [
"minecraft:potato"
]
}

移除操作会在添加之后执行,因此我们可以先包含一个标签,然后再从中排除某些元素:

{
"values": {
"#minecraft:logs": { /* ... */ }
},
// 再次排除猩红菌柄
"remove": [
"minecraft:crimson_stem"
]
}

数据映射可能支持带有额外参数的自定义 移除器。为了提供这些参数,可以将 remove 列表转换为一个 JSON 对象,其中待移除的元素作为键,附加的数据作为对应的值。例如,假设我们的移除器对象会被序列化为字符串,那么移除器映射可以像这样:

{
"remove": {
// 移除器会从值(这里是 `somekey1`)反序列化,并应用到胡萝卜物品对应的值上
"minecraft:carrot": "somekey1"
}
}

自定义数据映射(Custom Data Maps)

首先,我们需要定义数据映射条目的格式。数据映射条目必须是不可变的(immutable),因此 record 类型非常适合。重申上面提到的例子,包含两个浮点值 amountchance,我们的数据映射条目可以这样写:

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

和许多其他内容一样,数据映射的序列化和反序列化使用 编解码器(codecs)。这意味着我们需要为数据映射条目提供一个编解码器,稍后会用到:

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));
}

接下来,我们创建数据映射本身:

// 在本例中,我们为 minecraft:item 注册表(Registry)注册数据映射(data map),因此泛型使用 Item。
// 如果你希望为其他注册表创建数据映射,请相应调整类型。
public static final DataMapType<Item, ExampleData> EXAMPLE_DATA = DataMapType.builder(
// 数据映射的 ID。该数据映射对应的数据文件将位于
// <yourmodid>:examplemod/data_maps/item/example_data.json。
ResourceLocation.fromNamespaceAndPath("examplemod", "example_data"),
// 要为其注册数据映射的注册表。
Registries.ITEM,
// 数据映射条目的编解码器(codec)。
ExampleData.CODEC
).build();

最后,在 mod 事件总线 上的 RegisterDataMapTypesEvent 事件期间注册数据映射:

@SubscribeEvent
private static void registerDataMapTypes(RegisterDataMapTypesEvent event) {
event.register(EXAMPLE_DATA);
}

同步(Syncing)

被同步的数据映射(data map)会将其值同步到客户端。你可以通过在 builder 上调用 #synced 方法将数据映射标记为同步,如下所示:

public static final DataMapType<Item, ExampleData> EXAMPLE_DATA = DataMapType.builder(...)
.synced(
// 用于同步的编解码器(codec)。它可以与普通编解码器相同,
// 也可以是一个字段更少的编解码器,用于省略客户端不需要的部分。
ExampleData.CODEC,
// 该数据映射是否为强制(mandatory)。如果标记为强制,客户端缺失数据映射时会被断开连接;
// 这包括原版(vanilla)客户端。
false
).build();

用法(Usage)

由于数据映射可以应用于任意注册表(Registry),因此必须通过 Holder 查询数据,而不是直接通过注册表对象。此外,这只适用于引用类型的 holder(reference holder),不适用于 Direct holder。不过,大多数情况下你获得的都是引用 holder,例如通过 Registry#wrapAsHolderRegistry#getHolder 或各种 builtInRegistryHolder 方法,所以通常这不是问题。

随后,你可以通过 Holder#getData(DataMapType) 查询数据映射的值。如果某个对象没有关联的数据映射值,该方法会返回 null。继续使用之前的 ExampleData,下面演示当玩家拾取物品时为其恢复生命值:

@SubscribeEvent
private static void itemPickup(ItemPickupEvent event) {
ItemStack stack = event.getItemStack();
// 通过 ItemStack#getItemHolder 获取一个 Holder<Item>。
Holder<Item> holder = stack.getItemHolder();
// 从 holder 中获取数据。
ExampleData data = holder.getData(EXAMPLE_DATA);
if (data != null) {
// 数据存在,可以在这里进行相关操作!
Player player = event.getPlayer();
if (player.getLevel().getRandom().nextFloat() > data.chance()) {
player.heal(data.amount());
}
}
}

当然,这一过程同样适用于 NeoForge 提供的所有数据映射(data map)。

高级数据映射(Advanced Data Maps)

高级数据映射是指使用 AdvancedDataMapType 而不是标准的 DataMapTypeAdvancedDataMapTypeDataMapType 的子类)创建的数据映射。它们具备一些额外功能,主要包括自定义合并器(merger)和自定义移除器(remover)的能力。如果你的数据映射的值是集合或类似集合的数据类型,比如 ListMap,强烈建议实现这些功能。

DataMapType 有两个泛型参数:R(注册表类型,registry type)和 T(数据映射值类型,data map value type);而 AdvancedDataMapType 多了一个泛型参数:VR extends DataMapValueRemover<R, T>。这个泛型参数允许你以类型安全的方式为数据生成(datagen)移除器。

AdvancedDataMapType 需要通过 AdvancedDataMapType#builder() 方法创建,而不是 DataMapType#builder(),并返回一个 AdvancedDataMapType.Builder。这个构建器多了两个方法:#remover#merger,分别用于指定移除器(remover)和合并器(merger)(详见下文)。其余功能,包括同步机制,与标准数据映射保持一致。

合并器(Mergers)

合并器用于处理多个数据包(data pack)试图为同一个对象添加值时产生的冲突。默认的合并器(DataMapValueMerger#defaultMerger)会用新值覆盖已有值(比如覆盖优先级较低的数据包中的值),如果你不希望出现这种行为,则需要自定义合并器。

合并器会接收两个冲突的值,以及这些值要附加到的对象(以 Either<TagKey<R>, ResourceKey<R>> 形式传递,因为值既可以附加到某个标签下的全部对象,也可以附加到单个对象),还会收到对象所属的注册表(registry)。你需要返回最终应该附加的值。通常情况下,合并器应当尽量合并而不是覆盖(只有在常规合并无法实现时才考虑覆盖)。如果某个数据包希望绕过合并器,可以在对象上指定 replace 字段(详见 添加值)。

假设我们有一个为物品添加整数值的数据映射(data map),那么我们可以这样简单地通过累加两个值来解决冲突,例如:

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,另一个数据包为 minecraft:carrot 指定了值 15,那么 minecraft:carrot 的最终值将会是 27。如果其中任意一个对象指定了 "replace": true,那么将会使用该对象的值。如果两个对象都指定了 "replace": true,则会使用优先级更高的数据包的值。

最后,不要忘记在构建器中实际指定合并器(merger),如下所示:

// 数据映射的类型必须与合并器的类型一致。
AdvancedDataMapType<Item, Integer> ADVANCED_MAP = AdvancedDataMapType.builder(...)
.merger(new IntMerger())
.build();
tip

NeoForge 在 DataMapValueMerger 中为列表(list)、集合(set)和映射(map)提供了默认的合并器。

移除器(Removers)

与用于更复杂数据的合并器类似,移除器(remover)可以用于正确处理元素的 remove 子句。默认的移除器(DataMapValueRemover.Default.INSTANCE)会简单地移除与指定对象相关的所有信息,因此我们通常需要自定义移除器,只移除对象数据中的部分内容。

传递给构建器(builder)的编解码器(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": {
// 由于移除器被解码为字符串,这里可以直接用字符串作为值。
// 如果解码为对象,则需要使用对象格式。
"minecraft:carrot": "somekey1"
}
}

这样,在两个文件都应用后,最终结果将会是如下(内存中的表示):

{
"values": {
"minecraft:carrot": {
"somekey1": "value1"
}
}
}

和合并器(mergers)一样,不要忘记把它们添加到构建器(builder)中。注意,这里我们直接使用了 codec:

// 假设 AdvancedData 包含一个 Map<String, String> 类型的属性。
AdvancedDataMapType<Item, AdvancedData> ADVANCED_MAP = AdvancedDataMapType.builder(...)
.remover(MapRemover.CODEC)
.build();

数据生成(Data Generation)

数据映射(data maps)可以通过继承 DataMapProvider 并重写 #gather 方法来 自动生成(datagenned),以创建你自己的条目。继续使用前面例子的 ExampleData(包含 amountchance 两个 float 类型的值),我们的数据生成文件可以像这样:

public class MyDataMapProvider extends DataMapProvider {
public MyDataMapProvider(PackOutput packOutput, CompletableFuture<HolderLookup.Provider> lookupProvider) {
super(packOutput, lookupProvider);
}

@Override
protected void gather() {
// 我们为 EXAMPLE_DATA 数据映射创建一个构建器,并通过 #add 方法添加条目。
this.builder(EXAMPLE_DATA)
// 启用替换功能。切记不要以这种方式发布你的模组!这里只是为了教学演示。
.replace(true)
// 为所有台阶(slabs)添加值 "amount": 10, "chance": 1。布尔参数控制
// "replace" 字段,在模组中应始终为 false。
.add(ItemTags.SLABS, new ExampleData(10, 1), false)
// 为苹果添加值 "amount": 5, "chance": 0.2。
.add(Items.APPLE.builtInRegistryHolder(), new ExampleData(5, 0.2f), false) // 也可以用 Registry#wrapAsHolder 获取注册对象的 holder
// 再次移除木制台阶(wooden slabs)。
.remove(ItemTags.WOODEN_SLABS)
// 为 Botania 模组添加一个加载条件,仅作演示。
.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"
}
]
}

和所有数据提供器(data providers)一样,不要忘记把你的 provider 添加到事件(event)中:

@SubscribeEvent
public static void gatherData(GatherDataEvent.Client event) {
// 如果要添加数据包对象,先调用 event.createDatapackRegistryObjects(...)

event.createProvider(MyDataMapProvider::new);
}