Theseus 模组文档
架构

架构设计

模组系统的分层结构与核心抽象(面向开发者的公开版)


本文介绍 Theseus 模组系统的分层结构与关键抽象,帮助开发者理解「模组能做什么、为什么这样设计」。 接口签名的唯一权威定义见 API 参考;完整开发流程见 开发指南。


1. 分层架构

text
┌──────────────────────────────────────────────────────────────┐
│  第三方模组 / 内置示例模组 (com.seecmd.mod.builtin)            │
│  仅 import com.seecmd.mod.api.* 与 com.seecmd.mod.api.spi.*   │
└───────────────▲──────────────────────────────────────────────┘
                │ 依赖(编译期 + 运行期)
┌───────────────┴──────────────────────────────────────────────┐
│  com.seecmd.mod.api          (消费侧稳定接口)               │
│  ├─ 入口:Mod / GameplayMod / ModInfo / ModContext / ModConfig│
│  ├─ registry/ ModRegistries / Registry / RegistryKey / ...   │
│  ├─ collect/  CollectibleDefinition                          │
│  ├─ resource/ ResourceManager                                │
│  ├─ context/  GameplayContext / HudValues / ChartView        │
│  │            NoteView / LaneView                             │
│  ├─ render/   RenderContext / RenderCanvas / RenderImage     │
│  │            RenderLayer / MatrixStack / RenderFilter       │
│  ├─ capability/ GameplayHud / GameStateAccess / SceneHost     │
│  │            AudioAccess / InputAccess / StoryAccess          │
│  │            ModManagerAccess(进程级)                       │
│  ├─ judge/    JudgmentPolicy / JudgmentAdjuster / JudgeResult │
│  └─ event/    Event / EventBus / Subscription / EventPriority│
├──────────────────────────────────────────────────────────────┤
│  com.seecmd.mod.api.spi     (供给侧注册骨架)                │
│  ├─ NoteTypeDefinition / NoteData                            │
│  ├─ GameModeDefinition                                       │
│  ├─ JudgmentKindDefinition                                   │
│  └─ ChartExtensionParser                                     │
└───────────────▲──────────────────────────────────────────────┘
                │ 实现
┌───────────────┴──────────────────────────────────────────────┐
│  com.seecmd.mod.host   (宿主实现,模组禁止引用)             │
│  ModManager / GameplayModHost / 各注册表实现 /                │
│  RenderCanvas 实现 / 上下文快照实现                           │
└───────────────▲──────────────────────────────────────────────┘
                │ 调用
┌───────────────┴──────────────────────────────────────────────┐
│  游戏内核  MainApp / GameplayScene / FolderChartScene / ...   │
└──────────────────────────────────────────────────────────────┘

包边界强制规则

包面向依赖职责
com.seecmd.mod.api(含 registry / context / render / capability / judge / event / resource 等子包)第三方模组开发者仅标准库 + java.awt.Color(纯值对象),不依赖游戏内核消费侧纯接口与不可变 record:模组可触达的全部读口、开关、绘制入口
com.seecmd.mod.api.spi第三方模组开发者仅 api 包 + 标准库供给侧定义骨架:内容定义接口(音符类型 / 游戏模式 / 判定等级 / 谱面扩展段),模组实现后经 api.registry 的注册表登记
com.seecmd.mod.host宿主自身api + spi + 游戏内核实现管理器、场景托管、注册表、RenderCanvas 实现、上下文快照
com.seecmd.mod.builtin宿主自带示例仅 api + spi(与第三方模组同等约束)内置示例模组,验证 API 与作为开发参考

强制规则:


2. 内容注册表语义

模组 API 3.0 有 16 个注册表,全部经统一入口 ModContext.registries()(ModRegistries)取得,并共享同一套注册语义(本节统一说明)。

2.1 通用语义

语义规格
标识唯一注册项的身份是 RegistryKey / ResourceLocation(命名空间格式 <modId>:<name>,如 seecmd:hold、mymod:slide),全局唯一;定义对象不再自带 id()。同一 location 重复注册抛 IllegalArgumentException,注册表冻结后继续注册抛 RegistryFrozenException。
注册时机进程级,在 Mod.onPreInit(ModContext) 中经 ModContext.registries()(ModRegistries)的 noteTypes() / gameModes() / judgmentKinds() / chartExtensions() 等注册表注册一次;注册窗口一次性,冻结后写入抛 RegistryFrozenException。非场景级、非每局重注册,启用/停用模组需重启生效。
生命周期每个注册项按「解析 → 行为 → 渲染」三阶段被宿主调用:谱面加载时解析(deserialize / parse)→ 游玩中派发行为(onJudge / onJudgeTick / onJudged / onRemoved)→ 每帧渲染(render)。
异常隔离注册项的每个回调方法单独 try-catch,抛异常则跳过本次调用并记 LogLimiter 限频日志,不影响其他注册项与游戏运行。
通用路径消费宿主的核心循环(谱面解析、判定引擎、轨道渲染)不再为每种类型写 if/else,而是遍历注册表分发。当前谱面解析、行为判断、判定(onJudge)与渲染(render)均已按定义对象委托——判定时宿主调用 onJudge 派发自定义判定等级,渲染时宿主对自定义类型调用 render 由其自绘,回调异常由宿主捕获并回退内置逻辑。外观方面,皮肤系统已支持为自定义类型提供专属槽位(图片 <类型key>.png / 字符 custom 映射,见资源制作指南),未提供时回退到行为分类对应槽位。

2.2 十六个注册表

注册表注册内容解决的问题
Registry<NoteTypeDefinition>(ctx.registries().noteTypes())NoteTypeDefinition音符「类型」本身开放为内容:新增一种音符,定义其解析、判定与渲染方式
Registry<GameModeDefinition>(ctx.registries().gameModes())GameModeDefinition游戏模式开放为内容:定义模式专属的行为与规则
Registry<JudgmentKindDefinition>(ctx.registries().judgmentKinds())JudgmentKindDefinition判定等级开放为内容:新增 PERFECT/GREAT/MISS 之外的判定等级,评分、统计、特效按注册表遍历
Registry<ChartExtensionParser>(ctx.registries().chartExtensions())ChartExtensionParser谱面 JSON 增加 extensions 扩展段解析机制,谱面可携带模组自定义数据
Registry<HitEffectStyleProvider>(ctx.registries().hitEffectStyles())HitEffectStyleProvider打击特效样式开放给模组:全部返回非 null 的 provider 同时生效、按注册顺序依次绘制(可组合),全为 null 时用默认文字特效
Registry<BackgroundRenderer>(ctx.registries().backgroundRenderers())BackgroundRenderer游玩内容之前绘制的背景,执行顺序按 zOrder() 升序
Registry<RenderFilter>(ctx.registries().renderFilters())RenderFilter游玩内容之上叠加的画面滤镜,执行顺序按 zOrder() 升序
Registry<JudgmentPolicy>(ctx.registries().judgmentPolicies())JudgmentPolicy判定窗口策略,多项合并时取「最宽松生效」
Registry<JudgmentAdjuster>(ctx.registries().judgmentAdjusters())JudgmentAdjuster判定结果调整器,按注册顺序链式串联(前一个输出作为后一个输入)
Registry<CollectibleDefinition>(ctx.registries().collectibles())CollectibleDefinition收集品开放为内容:模组注册后由宿主转入收集品系统,模组用 ctx.unlockCollect(...) 主动解锁
Registry<SkinProvider>(ctx.registries().skins())SkinProvider皮肤来源开放给模组:注册键即皮肤 id(<modId>:<name>),注册后出现在视觉资源包场景的可选列表,与内置 respack 皮肤同一管道
Registry<LayerOverride>(ctx.registries().layerOverrides())LayerOverride声明替换某内置层的宿主默认绘制:仅 BACKGROUND / FULLSCREEN / JUDGE_FX / HUD 四层支持,WORLD / NOTES / OVERLAY 注册被忽略并告警
Registry<CustomRenderLayer>(ctx.registries().customRenderLayers())CustomRenderLayer模组声明带小数层号的新层并参与全局排序:index 落在 [L, L+1) 即渲染于第 L 层之后、第 L+1 层之前(index<4 处于世界 3D 作用域内)
Registry<JudgmentWindowSynthesizer>(ctx.registries().judgmentWindowSynthesizers())JudgmentWindowSynthesizer判定窗口尺度协商:宿主先对全部策略取最宽松合并,再交本链按注册顺序链式改写(前一个输出作为后一个输入,与判定调整器链同构)
Registry<TrackPoseAdjuster>(ctx.registries().trackPoseAdjusters())TrackPoseAdjuster轨道区位姿调整链:每帧在宿主默认位姿(含既有 setter 合成值)之上链式传阅,后写者可见前者结果——多模组叠加互不覆盖
Registry<CameraPoseAdjuster>(ctx.registries().cameraPoseAdjusters())CameraPoseAdjuster相机位姿调整链:每帧在宿主默认/接管位姿之上链式传阅(同上叠加语义)

2.3 核心循环分发

核心循环只做一件事——把控制权交给内容定义对象:

java
noteType.onCreateRuntime(chartNote);   // 谱面解析 → 运行时音符
noteType.onJudge(input, sink);         // 判定 → 定义对象决定产出哪个判定等级
noteType.render(canvas, renderCtx);    // 渲染 → 定义对象决定怎么画

接入现状:解析、行为判断、判定(onJudge)与渲染(render)均已接入核心循环——自定义类型被判定时宿主调用 onJudge 派发判定等级,渲染时由定义对象自绘;内置类型不派发,回调异常由宿主捕获并回退内置逻辑。自定义类型的外观已可由皮肤系统按类型定制(见资源制作指南)。


3. 能力目录(capabilities)

能力接口是「模组影响游戏每一处」的载体。每个能力接口按「读数据 / 改状态 / 渲染覆盖」三件套组织。能力实例在 Mod.onInit 中经 ModContext.capabilities() 注册(能力注入不是内容注册,不受注册表冻结限制),宿主在每个游玩场景自动注入可用。

能力接口域读数据改状态渲染覆盖
GameplayHud游玩 HUDvalues() → HudValues(progress/combo/score/accuracy/musicTimeMs)setProgressVisible(boolean) / setComboVisible / setScoreVisible / setAccuracyVisiblerenderOverlay(RenderCanvas, HudContext)
HudElement / HudElementOverride原版 HUD 元素对象化HudElement 枚举标识原版 HUD 元素(SCORE / COMBO / ACCURACY / HP_BAR / NOTES_INFO / BPM_SPEED / SYSTEM_LOG / STATUS)经 GameplayHud.override(HudElement) 返回 HudElementOverride,声明该元素的偏移 / 颜色 / 透明度(属性级原语,非预设样式)宿主渲染该元素时应用声明;多模组按启用顺序叠加(offset 求和、alpha 连乘、color 后者生效)
GameStateAccess游玩状态机musicTimeMs() / isPaused() / isFinished()play() / pause() / resume() / seek(ms) / setSpeed(x) / endGame()—
GameStateAccess(3D / 相机,实验性)轨道 3D 倾斜与相机位姿位姿链入参(TrackPose / CameraPose)setTrackTilt3D(deg) / setCameraPose(x,y,z,yaw,pitch,fov) / setCameraPoseAuto();位姿链经 ModRegistries.trackPoseAdjusters() / cameraPoseAdjusters() 注册重建 WORLD/BACKGROUND 层的相机视图 / 投影矩阵(视点与轨道姿态),判定零消费
SceneHost场景栈current()push(Scene) / pop() / replace(Scene)—
AudioAccess音频—playSound(String soundId, double volume) / stopSound(String) / playMusic(String) / stopMusic() / setMusicVolume(double)—
InputAccess输入isKeyDown(int)subscribeKeyHandler(Consumer<InputEvent>) / consumeKey(int)—
StoryAccess剧情currentNode()trigger(String nodeId, StoryContext) / skip()—
ModManagerAccess模组管理(进程级)registeredMods() / enabledMods() / isEnabled(String) / isActive(String) / getLoadStatus(String) / getLoadError(String)setEnabled(String, boolean)(立即生效并持久化)—
UiCustomization原版 UI 嵌入(进程级)—addStatusBarItem(String id, String text) / settings() / addResultOverlay(String id, ResultOverlayRenderer renderer) / addTrackStyleProvider(String id, TrackStyleProvider provider)状态栏块、结算页覆盖、轨道样式覆盖均由模组自绘
SettingsExtension / SettingsItem原生设置界面扩展—addCollection(String id, String titleKey) / addItem(SettingsItem item) / addCollectionItem(String collectionId, SettingsItem item) / removeItem(SettingsItem item)—
LaneInputMapper按键 → 轨道映射(游戏侧能力钩子)—init(int laneCount) / mapKeyToLane(int keyCode) / mapKeyToLanes(int keyCode) / triggerKeys()—
GlobalHotkeys / GlobalHotkey全局快捷键—add(GlobalHotkey hotkey)(进入原生设置的可改键面板,玩家可改键)—
ModServiceRegistry / ModService跨模组联动原语find(String globalId)register(String name, ModService service) / unregister(String name)—
NetworkAccess网络—executor()(宿主托管的守护线程池,模组在其上实现任意网络功能)—

能力接口只含原语方法。例如「把进度条换成自定义风格」= setProgressVisible(false) + 读 values().progress() + renderOverlay 自绘,宿主不提供「进度条皮肤」概念。

3.1 渲染覆盖模型

所有「替换原版渲染」的场景统一使用同一套覆盖机制,避免为每种组件发明新 API:

text
① 开关:setXxxVisible(false)           —— 宿主在默认渲染该组件时检查开关,被覆盖则不画原版
② 读数:values() / context()           —— 模组读取被覆盖组件的底层数据
③ 覆盖:renderOverlay(canvas, hudCtx)  —— 宿主在覆盖渲染阶段调用,模组在 canvas 上自绘

宿主先查覆盖、被覆盖则不画原版:GameplayHud.renderOverlay 只在有模组调用过 setXxxVisible(false) 时才被调用;同一 HUD 组件被多个模组覆盖时按注册顺序叠加,后注册的覆盖先绘制(位于上层)。

z 顺序约定(自底向上):轨道/音符 → 判定特效 → 原版 HUD → renderOverlay 覆盖层 → 全屏覆盖层。模组在 HudContext.layer() 中可查询当前绘制阶段。

3.2 跨域组合示例

「miss 一次打开模组自带的 galgame」由能力接口自由组合即可实现,无需宿主提供任何专门钩子:

text
NoteJudgedEvent (judgment == MISS)
  → GameStateAccess.pause()          // 改状态:暂停游玩
  → SceneHost.push(myGalgameScene)   // 改状态:压入自定义场景
  → AudioAccess.playSound(...)       // 改状态:播放自配音效
  → (galgame 场景内由模组自行渲染,回到游玩后)
  → GameStateAccess.resume()         // 改状态:恢复游玩

4. 渲染上下文 RenderCanvas 规格

4.1 定位

RenderCanvas 是模组唯一允许的绘制入口。宿主内部以 LWJGL3 + OpenGL 实现(lwjgl/GlRenderSurface、lwjgl/GlGameLoop),模组只面对抽象,不感知底层平台。渲染后端经中立缝接口 core/RenderSurface 与场景、中立组件库、文本渲染器及模组 API 全部分离——渲染后端与文档站 / 模组 API 相互解耦,模组只见中立缝,后端更换不影响模组。

4.2 能力边界

可画说明
填充矩形 fillRect(x, y, w, h)实心矩形,支持半透明(setAlpha)
边框矩形 drawRect(x, y, w, h)1px 描边矩形
直线 drawLine(x1, y1, x2, y2)1px 直线
文本 drawText(String, x, y, TextStyle) / measureText(...)复用游戏现有位图字形体系(等宽 CmdCharRenderer,GL 后端为 GlCmdCharRenderer)
图像 drawImage(RenderImage, x, y, w, h)经 ctx.resources().loadImage(...)(ResourceManager)获取的句柄
像素级图像创建 / 读取ResourceManager.createImage(w, h, argbPixels) 由像素数据创建图像;RenderImage.getPixelsARGB() 读回原始像素(模组可自行做模糊 / 变暗 / 缩放等后处理,详见开发指南 5.2)
裁剪 clip(x, y, w, h)配合 save()/restore() 使用
变换 translate/scale/rotate状态栈管理,save 与 restore 必须成对
不可说明
接触 GraphicsContext / Graphics2D实现细节,模组永远拿不到
接触窗口 / Canvas 组件由 SceneHost 负责,渲染层不开放

4.3 颜色表示

颜色统一使用 java.awt.Color(纯值对象,非图形上下文)。宿主在 RenderCanvas 实现内部转换为平台颜色。允许使用 java.awt.Color 不构成「实现泄漏」——它只是值对象,不携带任何绘图能力。


5. 实现状态总览

模块状态说明
入口接口(Mod / GameplayMod / ModInfo / ModContext / ModConfig)已实现签名见 API 参考;自 3.0 起公共签名冻结(3.x 内只增不破)
事件总线(EventBus + Event 层次)已实现一条总线、全进程唯一;游玩事件与跨场景事件按事件类型区分,事件清单见 API 参考 的事件总表
判定扩展(JudgmentPolicy / JudgmentAdjuster / JudgeResult)已实现判定等级经 Registry<JudgmentKindDefinition> 注册,评分/统计/特效按注册表遍历
渲染扩展(RenderCanvas / RenderFilter / BackgroundRenderer / HitEffectStyle)已实现三者分别经 ctx.registries() 的 renderFilters() / backgroundRenderers() / hitEffectStyles() 注册;绘制入口为 RenderCanvas,滤镜耗时 > 5ms 触发警告
能力接口(21 个类型)已实现游玩侧(含支撑类型):GameplayHud / GameStateAccess / SceneHost / AudioAccess / InputAccess / StoryAccess / StoryContext / StoryNode / LaneInputMapper / HudElement(枚举)/ HudElementOverride;进程级:ModManagerAccess / UiCustomization / SettingsExtension / SettingsItem / GlobalHotkeys / GlobalHotkey / NetworkAccess / ModServiceRegistry / ModService / CapabilityRegistry。完整签名见 API 参考
内容注册表(16 个)已实现全部经 ctx.registries() 取得并接入核心循环;音符类型的解析 / 行为 / 判定 / 渲染与生命周期回调均已接入
谱面扩展段已实现extensions 段解析框架落地
收集品已实现经 ctx.registries().collectibles() 注册 CollectibleDefinition,模组用 ctx.unlockCollect(...) 主动解锁
配置 / 数据目录 / 打包 / 依赖拓扑 / 日志限流保留config.json / data/mods/<id>/ / manifest + mod-manager.jar / DFS 拓扑排序 + 环检测 / LogLimiter
工程模板与构建已提供mods-template 最小模板,build.bat 一键构建(无需 Maven)或 Maven 构建,产物均带完整 manifest

相关文档:API 参考(签名唯一权威)、开发指南(完整开发流程)。