架构设计
模组系统的分层结构与核心抽象(面向开发者的公开版)
本文介绍 Theseus 模组系统的分层结构与关键抽象,帮助开发者理解「模组能做什么、为什么这样设计」。 接口签名的唯一权威定义见 API 参考;完整开发流程见 开发指南。
1. 分层架构
┌──────────────────────────────────────────────────────────────┐
│ 第三方模组 / 内置示例模组 (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 与作为开发参考 |
强制规则:
- 模组代码只允许
import com.seecmd.mod.api.*与com.seecmd.mod.api.spi.*(含子包)。 - 禁止 import 宿主实现包(
com.seecmd.mod.host.*)或游戏内核包(com.seecmd.core.*/com.seecmd.gameplay.*/com.seecmd.model.*等),也不得通过反射访问非 API 类。 - 标准库(
java.*/javax.*/java.util.*/java.nio.file.*等)按需 import;java.awt.Color作为纯值对象允许使用;禁止依赖java.awt.Graphics2D/java.awt.image.BufferedImage等图形实现类。 mod-api是零内核依赖的独立构件(仅含api与api.spi),已从物理上隔离 API 与宿主/内核;该边界由verify/ApiBoundaryCheck在每次校验中强制(禁止javafx/lwjgl/joml/ AWT 实现类型,java.awt.Color例外;com.seecmd下只允许 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 核心循环分发
核心循环只做一件事——把控制权交给内容定义对象:
noteType.onCreateRuntime(chartNote); // 谱面解析 → 运行时音符
noteType.onJudge(input, sink); // 判定 → 定义对象决定产出哪个判定等级
noteType.render(canvas, renderCtx); // 渲染 → 定义对象决定怎么画接入现状:解析、行为判断、判定(
onJudge)与渲染(render)均已接入核心循环——自定义类型被判定时宿主调用onJudge派发判定等级,渲染时由定义对象自绘;内置类型不派发,回调异常由宿主捕获并回退内置逻辑。自定义类型的外观已可由皮肤系统按类型定制(见资源制作指南)。
3. 能力目录(capabilities)
能力接口是「模组影响游戏每一处」的载体。每个能力接口按「读数据 / 改状态 / 渲染覆盖」三件套组织。能力实例在 Mod.onInit 中经 ModContext.capabilities() 注册(能力注入不是内容注册,不受注册表冻结限制),宿主在每个游玩场景自动注入可用。
| 能力接口 | 域 | 读数据 | 改状态 | 渲染覆盖 |
|---|---|---|---|---|
GameplayHud | 游玩 HUD | values() → HudValues(progress/combo/score/accuracy/musicTimeMs) | setProgressVisible(boolean) / setComboVisible / setScoreVisible / setAccuracyVisible | renderOverlay(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:
① 开关:setXxxVisible(false) —— 宿主在默认渲染该组件时检查开关,被覆盖则不画原版
② 读数:values() / context() —— 模组读取被覆盖组件的底层数据
③ 覆盖:renderOverlay(canvas, hudCtx) —— 宿主在覆盖渲染阶段调用,模组在 canvas 上自绘宿主先查覆盖、被覆盖则不画原版:GameplayHud.renderOverlay 只在有模组调用过 setXxxVisible(false) 时才被调用;同一 HUD 组件被多个模组覆盖时按注册顺序叠加,后注册的覆盖先绘制(位于上层)。
z 顺序约定(自底向上):轨道/音符 → 判定特效 → 原版 HUD → renderOverlay 覆盖层 → 全屏覆盖层。模组在 HudContext.layer() 中可查询当前绘制阶段。
3.2 跨域组合示例
「miss 一次打开模组自带的 galgame」由能力接口自由组合即可实现,无需宿主提供任何专门钩子:
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 |