ModContext
这一页是签名与行为参考。日志、数据目录、配置、注册表、事件总线都从这里拿。直接做一遍:教程 · 第一个模组。
宿主在生命周期回调中传入的运行上下文,是模组访问宿主基础设施的唯一入口。除 openFilePick 为 default 方法(未实现它的宿主返回 false 且不触发回调,模组据此走降级路径)外,其余方法保持抽象(无 default 实现),因为需返回有意义的宿主提供值。多数返回对象是绑定当前模组的视图,模组停用时宿主按归属统一清理。
方法速览
| 方法 | 用途 |
|---|---|
getLogger() | 本模组日志记录器 |
getDataDir() | 本模组数据目录 data/mods/<id> |
config() | 扁平 key-value 配置读写 |
unlockCollect(String) | 主动解锁自有收集品 |
resources() | 加载模组自带图像/文本资源 |
language() | 贡献模组译文 |
registerSound(ResourceLocation, double, boolean) | 注册模组自带音效 |
registerMusic(ResourceLocation, double, boolean) | 注册模组自带 BGM |
registries() | 统一注册表入口(仅 onPreInit 可写) |
capabilities() | 能力注入注册表 |
modServices() | 跨模组命名服务 |
eventBus() | 全进程唯一事件总线 |
notifier() | toast / 定时弹窗通知 |
panels() | 右下角 复古桌面 弹窗 |
network() | 宿主托管网络线程池 |
uiCustomization() | 原版 UI 扩展点 |
hotkeys() | 全局快捷键注册 |
openFilePick(FilePickRequest, Consumer<Path>) | 游戏内文件选取 |
完整签名
public interface ModContext {
java.util.logging.Logger getLogger();
Path getDataDir();
ModConfig config();
void unlockCollect(String collectId);
ResourceManager resources();
LanguageRegistry language();
void registerSound(ResourceLocation location, double volume, boolean preload);
void registerMusic(ResourceLocation location, double volume, boolean preload);
ModRegistries registries();
CapabilityRegistry capabilities();
ModServiceRegistry modServices();
EventBus eventBus();
Notifier notifier();
Panels panels();
NetworkAccess network();
UiCustomization uiCustomization();
GlobalHotkeys hotkeys();
default boolean openFilePick(FilePickRequest request, java.util.function.Consumer<Path> callback);
}getLogger()
签名:
java.util.logging.Logger getLogger();行为:返回名为 seecmd.mod.<模组id> 的 java.util.logging.Logger。该实例关闭了父处理器(useParentHandlers = false),自带 ConsoleHandler,输出格式为 [<模组id>] 消息,ERROR 红色、WARNING 黄色、其余青色。重复激活时会先移除旧 handler,避免日志重复。线程:任意线程(Logger 自身线程安全)。
示例:
ctx.getLogger().info("初始化完成,数据目录:" + ctx.getDataDir());getDataDir()
签名:
Path getDataDir();行为:返回模组数据目录。默认是相对工作目录的 data/mods/<id>;宿主/编辑器可注入数据根,此时为 <root>/<id>。每次调用都会尝试 Files.createDirectories(...)(幂等),创建失败静默忽略,因此返回的路径不保证真实存在,读写时仍需处理 IO 异常。线程:任意线程。
示例:
Path dir = ctx.getDataDir();
Files.writeString(dir.resolve("state.txt"), "ready");config()
签名:
ModConfig config();行为:返回本模组的配置文件读写接口,封装 <dataDir>/config.json。首次调用时懒创建实例,之后返回同一个实例。线程:实例方法 synchronized 保护,可在事件处理器与滤镜中并发访问。
示例:
ctx.config().set("volume", 0.8);
double volume = ctx.config().getDouble("volume", 1.0);unlockCollect(String collectId)
签名:
void unlockCollect(String collectId);行为:解锁指定 id 的收集品。collectId = 注册该收集品时所用 RegistryKey 的 path。为空或已解锁时无效果;成功解锁会记录获取日期并持久化进度。宿主解锁处理器尚未就绪时(如故事系统未初始化),请求先入队,处理器注册后按序回放。线程:源码未声明额外线程约束。
示例:
ctx.unlockCollect("first_note");resources()
签名:
ResourceManager resources();行为:返回资源入口,首次调用懒建并缓存。路径约定 assets/<命名空间>/<路径>(命名空间约定 = 模组 id),宿主按「data/mods/<模组id>/assets/... 磁盘覆盖 → 模组 jar 内 assets/...」两级查找;找不到抛受检异常 ResourceNotFoundException,调用点必须处理。解码只依赖 JDK,可在任意线程;后端纹理在首次绘制时懒创建,句柄须在渲染线程消费;阻塞 IO 不要放在游戏/渲染线程,改用 loadImageAsync。模组停用时宿主释放本入口产出的图像后端资源。
示例:
try {
RenderImage img = ctx.resources().loadImage(ResourceLocation.of("hello_mod", "textures/icon.png"));
} catch (ResourceNotFoundException e) {
ctx.getLogger().warning("缺少图标:" + e.getMessage());
}language()
签名:
LanguageRegistry language();行为:返回本模组的译文贡献入口(无状态视图)。也可改为在 jar 内放 assets/<模组id>/lang/<locale>.json 由宿主自动加载;两种方式叠加,后注册的覆盖先注册的。自动扫描发生在本模组 onInit 返回之后,故 onInit 中读自有译文为空。
示例:
ctx.language().add("zh", Map.of("hello_mod.greeting", "你好"));registerSound(ResourceLocation location, double volume, boolean preload)
签名:
void registerSound(ResourceLocation location, double volume, boolean preload);行为:注册模组自带音效。资源定位与 resources() 同口径;location.toString() 即音频在引擎内的注册 id,天然带命名空间,不会与内置音频或其它模组冲突。宿主把音频落地到 <dataDir>/audio/<命名空间>/<路径> 后注册进音频引擎(每次覆盖写,jar 更新后副本同步)。volume 为默认音量 0.0~1.0;preload 表示是否立即预加载进内存(短音效建议 true)。资源不可读时仅记录警告,不抛异常。线程:源码未声明额外线程约束。
示例:
ctx.registerSound(ResourceLocation.of("hello_mod", "sfx/click.wav"), 1.0, true);registerMusic(ResourceLocation location, double volume, boolean preload)
签名:
void registerMusic(ResourceLocation location, double volume, boolean preload);行为:注册模组自带 BGM,语义同 registerSound,类别为 BGM(受 bgmVolume 音量控制)。preload 仅表示是否预校验文件存在,BGM 不会预加载进内存。
示例:
ctx.registerMusic(ResourceLocation.of("hello_mod", "bgm/menu.ogg"), 0.9, false);registries()
签名:
ModRegistries registries();行为:返回绑定本模组的统一注册表入口,注册音符类型 / 游戏模式 / 判定等级 / 谱面扩展段 / 渲染滤镜 / 背景渲染器 / 打击特效样式 / 判定策略 / 判定调整器 / 判定窗口合成器 / 收集品 / 皮肤 / 层覆盖 / 自定义渲染层 / 轨道与相机姿态调整器。写路径做归属校验:注册键命名空间必须等于本模组 id,否则 IllegalArgumentException;读路径直接透传全局注册表。
只能在注册窗口内(Mod.onPreInit)写入:宿主在全部模组的 onPreInit 结束后冻结所有注册表,此后写入抛 RegistryFrozenException。注册在进程生命周期内一次性完成、不再撤销,因此启用/停用模组需要重启才生效。
示例:
ctx.registries().renderFilters().register(
RegistryKey.of("hello_mod", "scanline", RenderFilter.class),
new MyScanlineFilter());capabilities()
签名:
CapabilityRegistry capabilities();行为:返回绑定本模组的能力注入注册表视图。模组可在 onInit 中注册自定义能力实例(如 GameplayHud 覆盖);宿主在每个游玩场景自动把可用的能力(GameStateAccess / SceneHost / AudioAccess 等)以同类型注册,模组用 get(Class) 取用,未注册返回 null。模组停用时宿主按归属清理其注册项。详见 CapabilityRegistry。
示例:
GameStateAccess state = ctx.capabilities().get(GameStateAccess.class);
if (state != null) {
ctx.getLogger().info("当前音乐时间:" + state.musicTimeMs());
}modServices()
签名:
ModServiceRegistry modServices();行为:返回绑定本模组的跨模组命名服务注册表。register(name, service) 暴露自己的状态控制面,全局 id 自动拼接为 <本模组id>:<name>;find(globalId) 查询并读写其它模组暴露的服务,无需编译期耦合。模组停用时宿主自动清理其服务。详见 ModServiceRegistry。
示例:
ctx.modServices().register("state", myService);
ModService other = ctx.modServices().find("other_mod:state");eventBus()
签名:
EventBus eventBus();行为:返回带本模组归属的事件总线视图。一条总线、全进程唯一:游玩事件(判定/暂停/帧…)与跨场景事件(切场/启动/设置…)都在同一条总线上,模组用事件类型区分。模组经它建立的全部订阅会在停用/卸载时由宿主一次性退订。线程:任意线程可订阅;投递在宿主线程(游戏/渲染线程)同步发生。
示例:
ctx.eventBus().subscribe(GameStartEvent.class, event -> ctx.getLogger().info("开始游玩"));notifier()
签名:
Notifier notifier();行为:返回 Notifier 实现(宿主单例):右下角 toast(info / warning / error)与屏幕中央定时弹窗(dialog)。任意线程可调用,仅入队后立即返回,游戏线程统一渲染。
示例:
ctx.notifier().warning("配置项缺失,使用默认值");panels()
签名:
Panels panels();行为:返回 Panels 实现(宿主单例):在屏幕右下角创建与游戏 BGM 信息面板同款的 复古桌面 滑入面板,支持定时自动滑出、常驻、点击回调、动态更新内容。任意线程可调用,游戏线程统一渲染。
示例:
PanelHandle handle = ctx.panels().persistent("状态", false);
handle.setContentLines(List.of("已连接"));network()
签名:
NetworkAccess network();行为:返回本模组专属的网络线程池入口,首次调用懒创建核心 4 线程的守护调度池(线程名 mod-net-<id>-N)。模组用标准 java.net / java.nio 在其上实现任意网络功能。严禁在游戏线程做阻塞 IO。宿主在模组停用与游戏退出时 shutdownNow;关停后再调用 executor() 抛 IllegalStateException(不重建线程池)。详见 NetworkAccess。
示例:
ctx.network().executor().execute(() -> {
// 所有 socket / 文件网络读写都在这里
});uiCustomization()
签名:
UiCustomization uiCustomization();行为:返回绑定本模组的 UI 定制能力入口,提供嵌入原版固定 UI 区域的扩展点(主界面底部状态栏、光标渲染、结算覆盖层、轨道样式、设置扩展等),区别于 panels() 的独立悬浮弹窗。模组停用时宿主移除其定制项。详见 UiCustomization。
示例:
ctx.uiCustomization().addStatusBarItem("status", "就绪").setOnClick(() -> {
// 点击回调在游戏线程触发
});hotkeys()
签名:
GlobalHotkeys hotkeys();行为:返回全局快捷键注册入口。模组在任意场景下可触发的快捷键(如开关 HUD)必须经此注册,进入原生设置「全局快捷键」集合供玩家统一改键;禁止经 event.input.KeyEvent 实现玩家无法改键的隐性快捷键。宿主在模组停用与游戏退出时自动清理注册项。线程:注册任意线程可调用;快捷键回调在游戏线程触发(每帧至多一次)。详见 GlobalHotkeys。
示例:
ctx.hotkeys().add(new GlobalHotkey() {
@Override public String id() { return "toggle_hud"; }
@Override public String labelKey() { return "hello_mod.hotkey.toggle_hud"; }
@Override public int defaultKeyCode() { return java.awt.event.KeyEvent.VK_H; }
@Override public void onHotkey() { /* 游戏线程 */ }
});openFilePick(FilePickRequest request, Consumer callback)
签名:
default boolean openFilePick(FilePickRequest request, java.util.function.Consumer<Path> callback);行为:发起一次游戏内文件选取(导入图片 / 谱面等文件的用户入口)。宿主决定选取界面形态,模组只给 FilePickRequest 配置并在回调里消费结果;不要自行调起 Swing/AWT 系统文件对话框(会阻塞游戏线程且脱离宿主界面口径)。
- 返回
true= 已受理,回调将在未来恰好一次触发;返回false= 未受理(已有选取在途、宿主不支持、引擎未就绪或当前状态不允许),此时回调不会被调用,模组可提示用户后重试。 - 用户确认 →
path为所选文件(此后可能被移动/删除,模组应容错);用户取消 / 关闭 →path == null,取消零副作用,模组收到null不得改动任何数据。 - 同一时刻至多一个选取在途,在途时返回
false。 - 调用与回调都必须发生在游戏(GL)线程(在场景 update / 输入回调内调用即可满足)。
request与callback均不可为null(宿主实现直接解引用,传null会抛NullPointerException)。- 1.x 只增不破:本方法是
default,未实现它的宿主返回false且不触发回调。
示例:
boolean accepted = ctx.openFilePick(
FilePickRequest.of().title("导入特效图片").extensions("png", "jpg"),
path -> {
if (path == null) {
return; // 用户取消:不改动任何数据
}
ctx.getLogger().info("已选择:" + path);
});稳定性:2.0 起冻结:2.x 内只增不破(禁止删签名 / 改签名 / 改语义);破坏性变更只在 major 版本发生且须提供迁移指南与至少一个大版本的弃用期。3.x 内继续只增不破。