资源元数据

资源元数据

扩展名为 .mcmeta 的文件可以为游戏中的任意资产或数据对象保存 JSON 元数据。它们最常用于定义包的信息和纹理的应用方式;不过,只要在文件名末尾加上 .mcmeta,就可以为任何文件提供元数据(例如,apple.png 的资源元数据文件是 apple.png.mcmeta)。

提示 数据包内的 JSON 对象也可以拥有资源元数据,但这些元数据不会被使用,因为相应信息可以直接放在 JSON 文件本身中。

元数据节

JSON 元数据对象分为多个节,键表示节的类型,值则是该节的数据。在代码中,这由 MetadataSectionType 表示,它接受一个 String 键以及用于序列化值的 Codec。

原版和 NeoForge 目前提供以下元数据节:

节 类 所在文件 用途
pack PackMetadataSection pack.mcmeta 包的信息
features FeatureFlagsMetadataSection pack.mcmeta 启用实验性功能
filter ResourceFilterSection pack.mcmeta 过滤在此包之后应用的包中的文件
overlays / neoforge:overlays OverlayMetadataSection / GeneratingOverlayMetadataSection pack.mcmeta 在给定条件下覆盖主包的子包
language LanguageMetadataSection pack.mcmeta 额外语言,仅用于资源包
animation AnimationMetadataSection .png.mcmeta(纹理) 纹理图集中的动画纹理
gui GuiMetadataSection .png.mcmeta(纹理) GUI 精灵纹理
texture TextureMetadataSection .png.mcmeta(纹理) 纹理
villager VillagerMetadataSection .png.mcmeta(纹理) 村民帽子的可见性

提示 模组的主 pack.mcmeta 不需要 PackMetadataSection,因为 NeoForge 会自动合成这一部分。不过,通过 AddPackFindersEvent 模组总线事件 添加的内置包必须包含它。

要获取某个元数据节中的数据,需要先通过 ResourceManager 获得该文件的 Resource,调用 Resource#metadata 获取 ResourceMetadata,再向 ResourceMetadata#getSection 传入 MetadataSectionType。

// For some `ResourceManager` resourceManager

// Get the metadata for the topmost resource
Optional<AnimationMetadataSection> waterStillMetadata = resourceManager.getResource(
    // Identifier must specify exact path to the backing resource resource.
    Identifier.fromNamespaceAndPath("minecraft", "textures/block/water_still.png")
).flatMap(resource -> {
    try {
        // Get the metadata for the resource if present, otherwise an empty optional.
        return resource.metadata().getSection(AnimationMetadataSection.TYPE);
    } catch (IOException e) {
        // If an exception is thrown trying to read the metadata file.
        return Optional.empty();
    }
});

// Get the metadata of every resource for the identifier
List<AnimationMetadataSection> waterStillsMetadata = resourceManager.getResourceStack(
    // Identifier must specify exact path to the backing resource resource.
    Identifier.fromNamespaceAndPath("minecraft", "textures/block/water_still.png")
).map(resource -> {
    try {
        // Get the metadata for the resource if present, otherwise an empty optional.
        return resource.metadata().getSection(AnimationMetadataSection.TYPE);
    } catch (IOException e) {
        // If an exception is thrown trying to read the metadata file.
        return Optional.empty();
    }
}).filter(Optional::isPresent).map(Optional::get);

客户端的 ResourceManager 可以通过 Minecraft#getResourceManager 获取。服务端则不会在 MinecraftServer#reloadResources 之外暴露 ResourceManager,因此只能在 PreparableReloadListener 中使用它。

自定义节

创建自定义元数据节只需要定义对象、用于序列化和反序列化该对象的 Codec,以及用于从 mcmeta 读取它的 MetadataSectionType。

public record ExampleMetadataSection(String value) {
    public static final Codec<ExampleMetadataSection> CODEC = Codec.STRING.xmap(ExampleMetadataSection::new, ExampleMetadataSection::value);

    public static final MetadataSectionType<ExampleMetadataSection> TYPE = new MetadataSectionType<>(
        // The key for the section in the .mcmeta, should be prefixed with your mod id.
        "examplemod:example_section",
        // The codec to serialize and deserialize the section data.
        CODEC
    );
}

随后便可以将该元数据节添加到对象的 .mcmeta 文件中:

// In 'assets/examplemod/textures/block/example_block.png.mcmeta'
{
    "examplemod:example_section": "Hello world!"
}

并像其他元数据节一样,通过对应类型获取它。

数据生成

原版通过 PackMetadataGenerator 为 pack.mcmeta 提供数据生成支持。其他文件的元数据需要使用自定义 DataProvider。

PackMetadataGenerator

PackMetadataGenerator 用于生成模组或其内置子包的 pack.mcmeta。通过 add 方法添加元数据节,传入 MetadataSectionType 及其值即可。PackMetadataGenerator 还提供 forFeaturePack,用于生成带有 PackMetadataSection,并可选带有 FeatureFlagsMetadataSection 的 pack.mcmeta:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    event.createProvider(PackMetadataGenerator::new)
        // Can chain multiple `add` calls.
        .add(
            // The metadata section to add.
            LanguageMetadataSection.TYPE,
            // The value of the metadata section.
            Map.of(
                "hello_world",
                new LanguageInfo(
                    "Programmer's Land",
                    "Hello world!",
                    false
                )
            )
        );
}

其他资源元数据

其他文件的资源元数据需要使用自定义 DataProvider:

public class ResourceMetadataProvider implements DataProvider {

    private final PackOutput output;
    private final Map<Path, ResourceMetadata> metadata;

    public ResourceMetadataProvider(PackOutput output) {
        this.output = output;
        this.metadata = new HashMap<>();
    }

    protected void add() {
        // Add metadata here.
        this.textureMetadata(Identifier.fromNamespaceAndPath(
            "examplemod", "block/example_texture"
        ))
            // Can chain multiple `add` calls.
            .add(
                // The metadata section to add.
                TextureMetadataSection.TYPE
                // The value of the metadata section.
                new TextureMetadataSection(
                    true, TextureMetadataSection.DEFAULT_CLAMP, MipmapStrategy.AUTO, TextureMetadataSection.DEFAULT_ALPHA_CUTOFF_BIAS
                )
            ).add(
                ExampleMetadataSection.TYPE,
                new ExampleMetadataSection("Hello world!")
            );
    }

    protected ResourceMetadata textureMetadata(Identifier resource) {
        return this.metadata(
            PackOutput.Target.RESOURCE_PACK,
            "textures",
            resource.withSuffix(".png")
        );
    }

    protected ResourceMetadata metadata(PackOutput.Target type, String directory, Identifier resource) {
        return this.metadata.computeIfAbsent(
            this.output.createPathProvider(type, directory).file(resource, "mcmeta"),
            p -> new ResourceMetadata()
        );
    }

    @Override
    public CompletableFuture<?> run(CachedOutput cache) {
        Executor executor = Util.backgroundExecutor().forName("serializeMetadata");
        return CompletableFuture.allOf(
            this.metadata.entrySet().stream().map(entry -> CompletableFuture.runAsync(() -> {
                    JsonObject result = new JsonObject();
                    entry.getValue().sections().forEach((type, data) -> result.add(type, data.get()));
                    return result;
                }, executor).thenComposeAsync(json -> DataProvider.saveStable(cache, json, entry.getKey()), executor)
            ).toArray(CompletableFuture[]::new)
        );
    }

    @Override
    public final String getName() {
        return "Resource Metadata";
    }

    public record ResourceMetadata(Map<String, Supplier<JsonElement>> sections) {

        public ResourceMetadata() {
            this(new HashMap<>());
        }

        public <T> ResourceMetadata add(MetadataSectionType<T> type, T value) {
            this.sections.put(type.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow(IllegalArgumentException::new).getAsJsonObject());
            return this;
        }
    }
}

然后可以将其添加到 GatherDataEvent:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    event.createProvider(ResourceMetadataProvider::new);
}

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