标签

标签

简单来说,标签是由同一种类型的已注册对象组成的列表。它们从数据文件加载,可用于检查对象是否属于某个集合。例如,合成木棍时可以使用任意组合的木板(带有 minecraft:planks 标签的物品)。通常会在标签前加上 #,以便与“普通”对象区分(例如 #minecraft:planks,而普通对象是 minecraft:oak_planks)。

任何注册表 都可以拥有标签文件。方块和物品最常见,但流体、实体类型或伤害类型等注册表也经常使用标签。需要时,也可以创建自己的标签。

Minecraft 注册表的标签位于 data/<tag_namespace>/tags/<registry_path>/<tag_path>.json,非 Minecraft 注册表的标签位于 data/<tag_namespace>/tags/<registry_namespace>/<registry_path>/<tag_path>.json。例如,要修改 minecraft:planks 物品标签,应将标签文件放在 data/minecraft/tags/item/planks.json。

说明 与大多数其他 NeoForge 数据文件不同,NeoForge 添加的标签通常不使用 neoforge 命名空间,而是使用 c 命名空间(例如 c:ingots/gold)。这是应许多同时面向多个加载器开发的模组作者的要求,让 NeoForge 和 Fabric 模组加载器统一使用这些标签。

少数与 NeoForge 系统紧密关联的标签例外,例如许多伤害类型 标签。

标签文件的覆盖通常采用追加而非替换的方式。这意味着,如果两个数据包指定了 ID 相同的标签文件,两个文件的内容将会合并(除非另有指定)。这与大多数其他数据文件不同,后者会替换所有已有值。

标签文件格式

标签文件使用以下语法:

{
    // The values of the tag.
    "values": [
        // A value object. Must specify the id of the object to add, and whether it is required.
        // If the entry is required, but the object is not present, the tag will not load. The "required" field
        // is technically optional, but when removed, the entry is equivalent to the shorthand below.
        {
            "id": "examplemod:example_ingot",
            "required": false
        }
        // Shorthand for {"id": "minecraft:gold_ingot", "required": true}, i.e. a required entry.
        "minecraft:gold_ingot",
        // A tag object. Distinguished from regular entries by the leading #. In this case, all planks
        // will be considered entries of the tag. Like normal entries, this can also have the "id"/"required" format.
        // Warning: Circular tag dependencies will lead to a datapack not being loaded!
        "#minecraft:planks"
    ],
    // Whether to remove all pre-existing entries before adding your own (true) or just add your own (false).
    // This should generally be false, the option to set this to true is primarily aimed at pack developers.
    "replace": false,
    // A finer-grained way to remove entries from the tag again, if present. Optional, NeoForge-added.
    // Entry syntax is the same as in the "values" array.
    "remove": [
        "minecraft:iron_ingot"
    ]
}

查找和命名标签

查找已有标签时,通常建议按以下步骤进行:

  • 查看 Minecraft 的标签,确认是否已有所需标签。Minecraft 的标签可以在 BlockTags、ItemTags、EntityTypeTags 等类中找到。
  • 如果没有,再查看 NeoForge 的标签。它们位于 Tags.Blocks、Tags.Items、Tags.EntityTypes 等类中。
  • 如果仍然没有,就可以认为 Minecraft 或 NeoForge 尚未定义这个标签,需要自行创建。

创建标签时,应考虑以下问题:

  • 它是否用于调整自己模组的行为?如果是,应使用模组自己的命名空间。(例如“我的某种对象可以在这些方块上生成”一类的标签通常如此。)
  • 其他模组是否也可能使用该标签?如果是,应使用 c 命名空间。(例如新的金属或宝石标签通常如此。)
  • 其他情况下,使用模组自己的命名空间。

标签本身的命名也有一些约定:

  • 使用复数形式,例如 minecraft:planks、c:ingots。
  • 同一类型的多个对象使用文件夹组织,并为每个文件夹提供一个总标签。例如 c:ingots/iron、c:ingots/gold,以及包含这两者的 c:ingots。(注意:这是 NeoForge 的约定,Minecraft 的大多数标签不遵循此约定。)

使用标签

要在代码中引用标签,必须使用注册表键 和标识符 创建 TagKey<T>,其中 T 是标签的类型(Block、Item、EntityType<?> 等):

public static final TagKey<Block> MY_TAG = TagKey.create(
        // The registry key. The type of the registry must match the generic type of the tag.
        Registries.BLOCK,
        // The location of the tag. This example will put our tag at data/examplemod/tags/blocks/example_tag.json.
        Identifier.fromNamespaceAndPath("examplemod", "example_tag")
);

警告 由于 TagKey 是 record,它的构造方法是公开的。不过,不应直接使用构造方法,否则可能在查询标签条目等操作中导致各种问题。

然后便可以使用标签执行各种操作。先从最直观的操作开始:检查对象是否在标签中。以下示例使用方块标签,但所有类型的标签功能都完全相同(除非另有说明):

// Check whether dirt is in our tag.
// Assume access to Level level
boolean isInTag = level.registryAccess().lookupOrThrow(BuiltInRegistries.BLOCK).getOrThrow(MY_TAG).stream().anyMatch(holder -> holder.is(Items.DIRT));

这样的语句相当冗长,频繁使用时尤其如此。因此,标签系统最常见的两个使用者 BlockState 和 ItemStack 都定义了 #is 辅助方法,用法如下:

// Check whether the blockState's block is in our tag.
boolean isInBlockTag = blockState.is(MY_TAG);
// Check whether the itemStack's item is in our tag. Assumes the existence of MY_ITEM_TAG as a TagKey<Item>.
boolean isInItemTag = itemStack.is(MY_ITEM_TAG);

如果需要,也可以获取标签条目集合并对其进行流操作:

// Assume access to Level level
Stream<Holder<Block>> blocksInTag = level.registryAccess().lookupOrThrow(BuiltInRegistries.BLOCK).getOrThrow(MY_TAG).stream();

引导期间的静态注册表标签

有时需要在注册过程中访问 HolderSet。在数据组件上下文 中,初始化器会提供 HolderLookup.Provider,供解析引用:

Item.Properties props = new Item.Properties().delayedComponent(
    // The component to initialize
    DataComponents.DAMAGE_RESISTANT,
    // The initializer function, typically provides at least the registry lookup
    registries -> new DamageResistant(
        // Get the HolderSet from the TagKey
        registries.getOrThrow(DamageTypeTags.IS_FIRE)
    )
);

在数据组件上下文之外,仅对静态注册表,可以通过 BuiltInRegistries#acquireBootstrapRegistrationLookup 获取所需的 HolderGetter:

// Assume access to Level level
HolderSet<Block> blockTag = BuiltInRegistries.acquireBootstrapRegistrationLookup(BuiltInRegistries.BLOCK).getOrThrow(MY_TAG);

数据生成

与许多其他 JSON 文件一样,标签也可以通过数据生成 创建。每种标签都有自己的数据生成基类,例如方块标签和物品标签各有一个,因此也需要为每种标签分别创建类。这些类都继承 TagsProvider<T> 基类,其中 T 仍然是标签类型(Block、Item 等)。TagsProvider 进一步分为两大类:IntrinsicHolderTagsProvider<T> 通常用于静态注册表对象,允许直接将对象传入标签;KeyTagProvider 通常用于数据包注册表对象,允许传入对象的 ResourceKey。此外,还有针对由 Holder 包装的静态注册表对象的 HolderTagProvider<T>,不过原版仅将其用于药水标签。

下表列出了不同对象对应的标签提供器:

类型 标签提供器类 提供器类型
BannerPattern BannerPatternTagsProvider KeyTagProvider
Biome BiomeTagsProvider KeyTagProvider
Block BlockTagsProvider* IntrinsicHolderTagsProvider
ConfiguredFeature FeatureTagsProvider KeyTagProvider
DamageType DamageTypeTagsProvider KeyTagProvider
Dialog DialogTagsProvider KeyTagProvider
Enchantment EnchantmentTagsProvider KeyTagProvider
EntityType EntityTypeTagsProvider IntrinsicHolderTagsProvider
FlatLevelGeneratorPreset FlatLevelGeneratorPresetTagsProvider IntrinsicHolderTagsProvider
Fluid FluidTagsProvider KeyTagProvider
GameEvent GameEventTagsProvider KeyTagProvider
Instrument InstrumentTagsProvider KeyTagProvider
Item ItemTagsProvider* IntrinsicHolderTagsProvider
PaintingVariant PaintingVariantTagsProvider KeyTagProvider
PoiType PoiTypeTagsProvider KeyTagProvider
Potion PotionTagsProvider HolderTagProvider
Structure StructureTagsProvider KeyTagProvider
Timeline TimelineTagsProvider KeyTagProvider
VillagerTrade VillagerTradesTagsProvider KeyTagProvider
WorldPreset WorldPresetTagsProvider KeyTagProvider

* 这些提供器由 NeoForge 提供。

以生成方块标签(使用内在持有者)为例:

public class MyBlockTagsProvider extends BlockTagsProvider {
    // Get parameters from one of the `GatherDataEvent`s.
    public MyBlockTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider) {
        super(output, lookupProvider, ExampleMod.MOD_ID);
    }

    // Add your tag entries here.
    @Override
    protected void addTags(HolderLookup.Provider lookupProvider) {
        // Create a TagAppender of registry objects for our tag. This could also be e.g. a vanilla or NeoForge tag.
        this.tag(MY_TAG)
            // Add entries. This is a vararg parameter.
            // Key tag providers must provide ResourceKeys here instead of the actual objects.
            .add(Blocks.DIRT, Blocks.COBBLESTONE)
            // Add optional entries that will be ignored if absent. This example uses Botania's Pure Daisy.
            // This is not a vararg parameter.
            .add(TagEntry.optionalElement(Identifier.fromNamespaceAndPath("botania", "pure_daisy")))
            // Add a tag entry.
            .addTag(BlockTags.PLANKS)
            // Add multiple tag entries. This is a vararg parameter.
            // Can cause unchecked warnings that can safely be suppressed.
            .addTags(BlockTags.LOGS, BlockTags.WOODEN_SLABS)
            // Add an optional tag entry that will be ignored if absent.
            .addOptionalTag(ItemTags.create(Identifier.fromNamespaceAndPath("c", "ingots/tin")))
            // Add multiple optional tag entries. This is a vararg parameter.
            // Can cause unchecked warnings that can safely be suppressed.
            .addOptionalTags(ItemTags.create(Identifier.fromNamespaceAndPath("c", "nuggets/tin")), ItemTags.create(Identifier.fromNamespaceAndPath("c", "storage_blocks/tin")))
            // Set the replace property to true.
            .replace()
            // Set the replace property back to false.
            .replace(false)
            // Remove entries. This is a vararg parameter.
            // Key tag providers must provide ResourceKeys here instead of the actual objects.
            // Can cause unchecked warnings that can safely be suppressed.
            .remove(Blocks.CRIMSON_SLAB, Blocks.WARPED_SLAB);
    }
}

此示例会生成以下标签 JSON:

{
    "values": [
        "minecraft:dirt",
        "minecraft:cobblestone",
        {
            "id": "botania:pure_daisy",
            "required": false
        },
        "#minecraft:planks",
        "#minecraft:logs",
        "#minecraft:wooden_slabs",
        {
            "id": "c:ingots/tin",
            "required": false
        },
        {
            "id": "c:nuggets/tin",
            "required": false
        },
        {
            "id": "c:storage_blocks/tin",
            "required": false
        }
    ],
    "remove": [
        "minecraft:crimson_slab",
        "minecraft:warped_slab"
    ]
}

与所有数据提供器一样,将每个标签提供器添加到 GatherDataEvent:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    // Call event.createDatapackRegistryObjects(...) first if adding datapack objects

    event.createProvider(MyBlockTagsProvider::new);
}

自定义标签提供器

无论面向现有注册表 还是自定义注册表,创建自定义标签提供器只需继承 TagsProvider<T>,其中 T 是要为其生成标签的注册表对象类型。

public class MyRecipeTypeTagsProvider extends TagsProvider<RecipeType<?>> {
    // Get parameters from the `GatherDataEvent`s.
    public MyRecipeTypeTagsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider) {
        // Second parameter is the registry key we are generating the tags for.
        super(output, Registries.RECIPE_TYPE, lookupProvider, ExampleMod.MOD_ID);
    }
    
    @Override
    protected void addTags(HolderLookup.Provider lookupProvider) { /*...*/ }
}

接下来,通过 getOrCreateRawBuilder 创建 TagBuilder,从而在提供器中生成标签。构建器包含根据 Identifier 添加或移除元素和标签的方法。此外,还可以通过 setReplace 指定 replace 属性:

public class MyRecipeTypeTagsProvider extends TagsProvider<RecipeType<?>> {
    
    // ...

    // Lets assume the following TagKey<RecipeType<?>> SMELTERS, CRAFTERS, SMITHERS
    @Override
    protected void addTags(HolderLookup.Provider lookupProvider) {
        // Create a TagBuilder for `Identifier`s.
        this.getOrCreateRawBuilder(MY_TAG)
            // Add entries.
            .addElement(Identifier.fromNamespaceAndPath("minecraft", "crafting"))
            .addElement(Identifier.fromNamespaceAndPath("minecraft", "smelting"))
            // Add optional entries that will be ignored if absent.
            .addOptionalElement(Identifier.fromNamespaceAndPath("minecraft", "blasting"))
            // Add a tag entry.
            .addTag(SMELTERS.location())
            // Add an optional tag entry that will be ignored if absent.
            .addOptionalTag(CRAFTERS.location())
            // Set the replace property to true.
            .setReplace(true)
            // Set the replace property back to false.
            .setReplace(false)
            // Remove entries.
            .removeElement(Identifier.fromNamespaceAndPath("minecraft", "campfire_cooking"))
            // Remove a tag entry.
            .removeTag(SMITHERS.location());
    }
}

目前,整个标签都由 Identifier 构建。不过,每次都指定原始标识符可能很繁琐,尤其是在已有 ResourceKey 或可直接使用对象时。这正是 TagAppender 的用途。TagAppender<E, T> 实质上是对 TagBuilder 的包装,它接受任意条目对象 E,并将其转换为针对注册表对象 T 的 TagBuilder 调用。只要能够将新对象类型转换为之前的条目对象 E,就可以通过 map 将 TagAppender 重新映射为任意对象。这基本上就是 KeyTagProvider 和 IntrinsicHolderTagsProvider 所做的事情:它们提供 tag 方法,创建 TagAppender,分别将 ResourceKey 或直接对象映射为 Identifier:


public class MyRecipeTypeTagsProvider extends TagsProvider<RecipeType<?>> {
    
    // ...

    // Let's assume we have the TagKey<RecipeType<?>>s SMELTERS, CRAFTERS, SMITHERS
    @Override
    protected void addTags(HolderLookup.Provider lookupProvider) {
        // Create the TagAppender for `Identifier`s.
        this.tag(MY_TAG)
            // Replace property info
            .replace()
            // Handle any optional elements that may not be present
            .addOptional(Identifier.fromNamespaceAndPath("examplemod", "example_type"))
            // Can take in a TagKey
            .addOptionalTag(CRAFTERS)

            // Map to ResourceKey (KeyTagProvider)
            .map((Function<ResourceKey<RecipeType<?>>, Identifier>) ResourceKey::location)
            .add(BuiltInRegistries.RECIPE_TYPE.getResourceKey(RecipeType.CRAFTING).orElseThrow())

            // Map to direct object (IntrinsicHolderTagsProvider)
            .map((Function<RecipeType<?>, ResourceKey<RecipeType<?>>) type -> BuiltInRegistries.RECIPE_TYPE.getResourceKey(type).orElseThrow())
            .add(RecipeType.SMELTING)
            .addTag(SMELTERS)
            .remove(RecipeType.CAMPFIRE_COOKING)
            .remove(SMITHERS);
    }

    private TagAppender<Identifier, RecipeType<?>> tag(TagKey<RecipeType<?>> tag) {
        // Create the builder
        TagBuilder builder = this.getOrCreateRawBuilder(tag);

        // Generate the appender (can use TagAppender#forBuilder) instead
        return new TagAppender<Identifier, T>() {

            @Override
            public TagAppender<Identifier, T> add(Identifier element) {
                builder.addElement(element);
                return this;
            }

            @Override
            public TagAppender<Identifier, T> addOptional(Identifier element) {
                builder.addOptionalElement(element);
                return this;
            }

            @Override
            public TagAppender<Identifier, T> addTag(TagKey<T> tag) {
                builder.addTag(tag.location());
                return this;
            }

            @Override
            public TagAppender<Identifier, T> addOptionalTag(TagKey<T> tag) {
                builder.addOptionalTag(tag.location());
                return this;
            }

            // For situations where you cannot access the current entry object
            @Override
            public TagAppender<Identifier, T> add(TagEntry entry) {
                builder.add(entry);
                return this;
            }

            @Override
            public TagAppender<Identifier, T> replace(boolean value) {
                builder.setReplace(value);
                return this;
            }

            @Override
            public TagAppender<Identifier, T> remove(Identifier element) {
                builder.removeElement(element);
                return this;
            }

            @Override
            public TagAppender<ResourceKey<T>, T> remove(TagKey<T> tag) {
                builder.removeTag(tag.location());
                return this;
            }
        };
    }
}

复制标签内容

NeoForge 提供了一种特殊的 IntrinsicHolderTagsProvider,名为 BlockTagCopyingItemTagProvider,用于让物品标签与对应方块标签的内容保持一致。此时不使用 TagAppender,而是调用 copy,传入要从中复制内容的方块标签以及目标物品标签。

public class ExampleBlockTagCopyingItemTagProvider extends BlockTagCopyingItemTagProvider {

    public ExampleBlockTagCopyingItemTagProvider(
        PackOutput output,
        CompletableFuture<HolderLookup.Provider> lookupProvider,
        CompletableFuture<TagLookup<Block>> blockTags // Obtained from BlockTagsProvider#contentsGetter
    ) {
        super(output, lookupProvider, blockTags, ExampleMod.MOD_ID);
    }

    @Override
    protected void addTags(HolderLookup.Provider lookupProvider) {
        // Assuming types TagKey<Block> and TagKey<Item> for the two parameters
        this.copy(EXAMPLE_BLOCK_TAG, EXAMPLE_ITEM_TAG);

        // You can also add normal item tags here
    }

}

与所有数据提供器一样,复制标签的提供器也必须添加到 GatherDataEvent:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    // Call event.createDatapackRegistryObjects(...) first if adding datapack objects

    event.createBlockAndItemTags(MyBlockTagsProvider::new, ExampleBlockTagCopyingItemTagProvider::new);
}

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