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