数据映射(Data Maps)
数据映射包含可附加到注册对象的数据驱动、可重新加载的对象。该系统允许更轻松地数据驱动游戏行为,因为它们提供同步或冲突解决等功能,从而带来更好和更可配置的用户体验。您可以将标签视为注册表对象 ➜ 布尔映射,而数据映射是更灵活的注册表对象 ➜ 对象映射。与标签类似,数据映射将添加到其相应的数据映射中,而不是覆盖。
数据映射可以附加到静态、内置的注册表以及动态数据驱动的数据包注册表。数据映射支持通过使用/reload命令或任何其他重新加载服务器资源的方式进行重新加载。
NeoForge为常见用例提供了各种内置数据映射,替换了硬编码的原版字段。更多信息可以在链接文章中找到。
文件位置(File Location)
数据映射从位于<mapNamespace>/data_maps/<registryNamespace>/<registryPath>/<mapPath>.json的JSON文件加载,其中:
<mapNamespace>是数据映射ID的命名空间,<mapPath>是数据映射ID的路径,<registryNamespace>是注册表ID的命名空间(如果是minecraft则省略),以及<registryPath>是注册表ID的路径。
示例:
- 对于
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结构(JSON Structure)
数据映射文件本身可能包含以下字段:
replace:一个布尔值,将在添加此文件的值之前清除数据映射。模组永远不应提供此字段,仅应由希望为此目的覆盖此映射的数据包开发者使用。neoforge:conditions:加载条件列表。values:注册表ID 或标签ID到应由您的模组添加到数据映射的值的映射。值本身的结构由数据映射的编解码器定义(见下文)。remove:要从数据映射中移除的注册表ID或标签ID列表。
添加值(Adding Values)
例如,假设我们有一个用于minecraft:item注册表的数据映射对象,具有两个浮点键amount和chance。相应的数据映射文件可能如下所示:
{
"values": {
// 将值附加到胡萝卜物品
"minecraft:carrot": {
"amount": 12,
"chance": 1
},
// 将值附加到logs标签中的所有物品
"#minecraft:logs": {
"amount": 1,
"chance": 0.1
}
}
}
数据映射可能支持合并器,这将在冲突情况下导致自定义合并行为,例如,如果两个模组为同一物品添加数据映射值。为避免触发合并器,我们可以在元素级别指定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)
首先,我们定义数据映射条目的格式。数据映射条目必须是不可变的,使得记录非常适合此目的。重申我们上面具有两个浮点值amount和chance的示例,我们的数据映射条目将如下所示:
public record ExampleData(float amount, float chance) {}
与许多其他事物一样,数据映射使用编解码器进行序列化和反序列化。这意味着我们需要为我们的数据映射条目提供一个编解码器,稍后将使用:
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注册表注册数据映射,因此我们使用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,
// 数据映射条目的编解码器。
ExampleData.CODEC
).build();
最后,在模组事件总线上的RegisterDataMapTypesEvent期间注册数据映射:
@SubscribeEvent // 在模组事件总线上
public static void registerDataMapTypes(RegisterDataMapTypesEvent event) {
event.register(EXAMPLE_DATA);
}
同步(Syncing)
同步的数据映射将把它们的值同步到客户端。可以通过在构建器上调用#synced来将数据映射标记为同步,如下所示:
public static final DataMapType<Item, ExampleData> EXAMPLE_DATA = DataMapType.builder(...)
.synced(
// 用于同步的编解码器。可能与普通编解码器相同,但也可能是
// 字段较少的编解码器,省略客户端不需要的对象部分。
ExampleData.CODEC,
// 数据映射是否是必需的。将数据映射标记为必需将断开
// 在其端缺少数据映射的客户端连接;这包括原版客户端。
false
).build();
用法(Usage)
由于数据映射可以在任何注册表上使用,它们必须通过Holders查询,而不是通过实际的注册表对象。此外,它仅适用于引用持有者,不适用于Direct持有者。然而,大多数地方将返回引用持有者,例如Registry#wrapAsHolder、Registry#getHolder或不同的builtInRegistryHolder方法,因此在大多数情况下这不应该是一个问题。
然后,您可以通过Holder#getData(DataMapType)查询数据映射值。如果对象没有附加数据映射值,该方法将返回null。重用我们之前的ExampleData,让我们在玩家拾取它们时使用它们来治疗玩家:
@SubscribeEvent // 在游戏事件总线上
public static void itemPickup(ItemPickupEvent event) {
ItemStack stack = event.getItemStack();
// 通过ItemStack#getItemHolder获取Holder<Item>。
Holder<Item> holder = stack.getItemHolder();
// 从持有者获取数据。
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提供的所有数据映射 。
高级数据映射(Advanced Data Maps)
高级数据映射是使用AdvancedDataMapType而不是标准DataMapType(AdvancedDataMapType是其子类)的数据映射。它们具有一些额外功能,即指定自定义合并器和自定义移除器的能力。对于值集合或类似集合(例如Lists或Maps)的数据映射,强烈建议实现此功能。
虽然DataMapType有两个泛型R(注册表类型)和T(数据映射值类型),但AdvancedDataMapType还有一个:VR extends DataMapValueRemover<R, T>。此泛型允许以适当的类型安全性进行数据生成移除器。
AdvancedDataMapTypes使用AdvancedDataMapType#builder()而不是DataMapType#builder()创建,返回一个AdvancedDataMapType.Builder。此构建器有两个额外方法#remover和#merger,分别用于指定移除器和合并器(见下文)。所有其他功能,包括同步,保持不变。
合并器(Mergers)
合并器可用于处理尝试为同一对象添加值的多个数据包之间的冲突。默认合并器(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,另一个数据包为minecraft:carrot指定值15,那么minecraft:carrot的最终值将是27。如果这些对象中的任何一个指定"replace": true,那么将使用该对象的值。如果两者都指定"replace": true,则使用优先级较高的数据包的值。
最后,不要忘记在构建器中实际指定合并器,如下所示:
// 数据映射的类型必须与合并器的类型匹配。
AdvancedDataMapType<Item, Integer> ADVANCED_MAP = AdvancedDataMapType.builder(...)
.merger(new IntMerger())
.build();
NeoForge在DataMapValueMerger中为列表、集合和映射提供了默认合并器。
移除器(Removers)
与用于更复杂数据的合并器类似,移除器可用于正确处理元素的remove子句。默认移除器(DataMapValueRemover.Default.INSTANCE)将简单地移除与指定对象相关的任何和所有信息,因此我们希望使用自定义移除器仅移除对象数据的一部分。
传递给构建器的编解码器(继续阅读)将用于解码移除器实例。然后,移除器将被传递当前附加到对象的值及其来源,并应返回要替换旧值的值的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": {
"somekey2": "value2"
}
}
}
与合并器一样,不要忘记将它们添加到构建器中。请注意,我们在此处仅使用编解码器:
// 我们假设AdvancedData包含某种Map<String, String>属性。
AdvancedDataMapType<Item, AdvancedData> ADVANCED_MAP = AdvancedDataMapType.builder(...)
.remover(MapRemover.CODEC)
.build();
数据生成(Data Generation)
数据映射可以通过扩展DataMapProvider并重写#gather来创建条目进行数据生成。重用之前的ExampleData(具有浮点值amount和chance),我们的数据生成文件可能如下所示:
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)
// 我们为所有台阶添加值"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获取注册表对象的持有者
// 我们再次移除木质台阶。
.remove(ItemTags.WOODEN_SLABS)
// 我们为植物魔法添加模组加载条件,为什么不呢。
.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 // 在模组事件总线上
public static void gatherData(GatherDataEvent.Client event) {
// 如果添加数据包对象,首先调用event.createDatapackRegistryObjects(...)
event.createProvider(MyDataMapProvider::new);
}