Theseus 模组文档
开发指南

资源与语言


模组自带的图片、文本与译文都经 ModContext 提供的两个入口访问:ctx.resources()(ResourceManager)与 ctx.language()(LanguageRegistry)。两者共用同一套 assets/ 路径约定。

5.1 路径约定与查找顺序

5.2 ResourceManager 各方法用途

java
byte[] readBytes(ResourceLocation location) throws ResourceNotFoundException;
String readText(ResourceLocation location) throws ResourceNotFoundException;
RenderImage loadImage(ResourceLocation location) throws ResourceNotFoundException;
CompletableFuture<RenderImage> loadImageAsync(ResourceLocation location);
List<ResourceLocation> list(String pathPrefix);
RenderImage createImage(int width, int height, int[] argbPixels);
方法用途与要点
readBytes读取资源原始字节;两级查找均未命中或读取失败抛 ResourceNotFoundException
readText以 UTF-8 读取资源文本(语言文件、JSON 配置等);未命中或读取失败抛 ResourceNotFoundException
loadImage同步加载并解码图像(在调用线程解码),返回 RenderImage 句柄;解码只依赖 JDK,与后端无关,返回的句柄可在任意后端绘制
loadImageAsync在宿主 IO 线程池解码。游戏 / 渲染线程加载大图应优先用它,避免阻塞当帧;失败时 future 以 ResourceNotFoundException 异常完成
list列出本模组 assets/ 下的资源(“扫描目录里全部皮肤 / 音效”这类需求)。只列模组 jar 内的条目,磁盘覆盖目录不参与枚举。参数形如 "mymod:"(全部)或 "mymod:textures/note"(前缀);返回的 ResourceLocation 的 path 是相对 assets/<namespace>/ 的完整路径,因此返回值可直接再喂给 readBytes / loadImage;结果按 ResourceLocation 字典序稳定排序。pathPrefix 不含 ':' 或命名空间非法时抛 IllegalArgumentException
createImage像素级处理的出口:模组经 RenderImage#getPixelsARGB() 读出原始像素,自行完成模糊 / 变暗 / 缩放等计算后,用本方法生成新图像用于渲染。width / height 须大于 0,argbPixels 为 0xAARRGGBB(Alpha 前置)且长度须不小于 width * height,否则抛 IllegalArgumentException

RenderImage 提供 width()、height() 与 getPixelsARGB()(长度为 width() * height()、按行优先、每像素 0xAARRGGBB)。每次调用 getPixelsARGB() 都会复制一份像素数据,成本随图像尺寸上升,应一次性读取后自行缓存处理结果,不要每帧调用。

5.3 线程注意

5.4 语言

模组有两种等价的贡献译文方式:

  1. 资源文件(推荐):在模组 jar 内放 assets/<模组id>/lang/<locale>.json(形如 {"mymod.hud.label": "准度"}),宿主在模组装配后自动扫描并加载;
  2. 代码注册:ctx.language().add(locale, map),适合译文由代码计算或来自配置的场景。可对同一 locale 多次调用,后注册的覆盖先注册的同名键。
java
void add(String locale, Map<String, String> entries);
Optional<String> get(String key);
Optional<String> get(String locale, String key);

读取时机的坑(务必注意):宿主在你的 onInit 返回之后才扫描你的 assets/<模组id>/lang/*.json 语言文件。因此:

5.5 完整示例:读取自带贴图 + 自带语言文件

假设模组 id 为 example_assets,jar 内资源布局为:

text
src/main/resources/assets/example_assets/textures/badge.png
src/main/resources/assets/example_assets/lang/zh.json    // {"example_assets.badge.label": "徽章"}
src/main/resources/assets/example_assets/lang/en.json    // {"example_assets.badge.label": "Badge"}
java
package com.example.assetmod;

import com.seecmd.mod.api.GameplayMod;
import com.seecmd.mod.api.ModContext;
import com.seecmd.mod.api.ModInfo;
import com.seecmd.mod.api.registry.RegistryKey;
import com.seecmd.mod.api.registry.ResourceLocation;
import com.seecmd.mod.api.render.FilterFrame;
import com.seecmd.mod.api.render.RenderCanvas;
import com.seecmd.mod.api.render.RenderFilter;
import com.seecmd.mod.api.render.RenderImage;
import com.seecmd.mod.api.render.TextStyle;
import com.seecmd.mod.api.resource.ResourceNotFoundException;

import java.awt.Color;
import java.util.List;
import java.util.Map;

/**
 * 资源与语言示例:加载自带贴图、贡献并读取自带译文。
 *
 * <p>资源文件:assets/example_assets/textures/badge.png
 * <p>语言文件:assets/example_assets/lang/{zh,en}.json(宿主自动扫描)
 */
public final class AssetExampleMod implements GameplayMod {

    private static final String MOD_ID = "example_assets";

    /** 对应 assets/example_assets/textures/badge.png(小写、两级查找、无裸路径回退)。 */
    private static final ResourceLocation BADGE =
            ResourceLocation.of(MOD_ID, "textures/badge.png");

    /** 译文键必须以模组 id 作前缀。 */
    private static final String LABEL_KEY = MOD_ID + ".badge.label";

    /** 图像句柄:onInit 加载一次,之后在渲染回调里复用(不要每帧加载)。 */
    private RenderImage badge;

    /** 译文缓存:在 onPostInit 读取(onInit 里读会被宿主扫描时序挡住,读到空值)。 */
    private String label = LABEL_KEY;

    @Override
    public ModInfo info() {
        return ModInfo.of(MOD_ID, "Asset Example", "1.0.0", List.of("YourName"),
                "演示读取自带贴图与自带语言文件");
    }

    @Override
    public void onPreInit(ModContext ctx) {
        // 渲染内容仍在注册窗口内注册(运行期再注册会抛 RegistryFrozenException)。
        ctx.registries().renderFilters().register(
                RegistryKey.of(MOD_ID, "badge", RenderFilter.class),
                new BadgeFilter(this));
    }

    @Override
    public void onInit(ModContext ctx) {
        loadBadge(ctx);

        // 代码注册译文:与 jar 内 lang/*.json 等价,二者可同时使用;
        // 这里的注册立即生效(若 locale 恰为当前语言)。
        ctx.language().add("zh", Map.of(LABEL_KEY, "徽章"));
        ctx.language().add("en", Map.of(LABEL_KEY, "Badge"));

        // 反例提示:此处 ctx.language().get(LABEL_KEY) 对 jar 内 lang 文件提供的键
        // 可能是空的——宿主在本方法返回之后才扫描语言文件。
    }

    @Override
    public void onPostInit(ModContext ctx) {
        // 正确时机:此时 jar 内 assets/example_assets/lang/<locale>.json 已被宿主加载。
        label = ctx.language().get(LABEL_KEY).orElse(LABEL_KEY);
        ctx.getLogger().info("译文就绪:" + label);
    }

    /** 加载自带贴图;缺图不阻断加载,渲染回调里直接跳过绘制。 */
    private void loadBadge(ModContext ctx) {
        try {
            badge = ctx.resources().loadImage(BADGE);
        } catch (ResourceNotFoundException e) {
            // 当前版本的 ResourceManager 找不到资源时抛异常(不再静默返回 null),
            // 所以要用 try/catch 明确处理,而不是靠判空。
            ctx.getLogger().warning("贴图缺失:" + BADGE + "(" + e.getMessage() + ")");
        }
    }

    /** 在画面左上角绘制自带贴图与自带译文(渲染滤镜,叠加在游玩内容之上)。 */
    private static final class BadgeFilter implements RenderFilter {

        private final AssetExampleMod mod;

        BadgeFilter(AssetExampleMod mod) {
            this.mod = mod;
        }

        @Override
        public int zOrder() {
            return 50; // 滤镜层内偏上(数字越大越靠上)
        }

        @Override
        public void apply(RenderCanvas canvas, FilterFrame frame) {
            RenderImage image = mod.badge;
            if (image == null) {
                return; // 贴图缺失时静默跳过
            }
            double size = 48;
            canvas.save();
            canvas.drawImage(image, 16, 16, size, size);
            canvas.setColor(Color.WHITE);
            canvas.drawText(mod.label, 16, 16 + size + 18, TextStyle.of(16, Color.WHITE));
            canvas.restore();
        }
    }
}