站点主管 · 模组开发手册
本手册面向第一次做模组的作者。目标是:不看源码也能做出可安装的模组;进阶作者能写 C# 脚本扩展 SCP 行为与界面。
玩家侧安装与下载见 模组工坊。开发问题可到社区反馈。
API 版本:
apiVersion= 1(程序集SiteDirector.ModSDK)
游戏版本:建议gameVersionMin≥4.1.0
官方示例:游戏仓库SiteDirector/examples/sample-mod/
清单文件:必须叫mod.json(小写)
建议阅读顺序
快速开始:10 分钟改一条走廊文案
只想先验证「模组能加载」?按下面做,不需要安装 .NET,也不用写代码。
1. 找到 Mods 目录
| 环境 | 路径怎么找 |
|---|---|
| Windows 正式版 | 打开 SiteDirector.exe 所在文件夹,新建或进入同级 Mods/ |
| Godot 编辑器 / 源码 | 工程目录 SiteDirector/Mods/(对应 res://Mods,编辑器模式下优先用这个) |
| Android | 与可执行文件同级的 Mods/(常在应用私有目录附近)。用文件管理器或 ADB 放入;主菜单「模组」页顶部会显示实际路径 |
没有 Mods 就自己新建。游戏启动时也会尝试创建该目录。
2. 新建模组文件夹
文件夹名建议与模组 id 一致,例如:
Mods/hello-corridor/
mod.json
content/
buildings.json
注意:扫描的是 Mods 下的子文件夹。不要把
mod.json直接丢在Mods/根目录。
3. 写 mod.json
用记事本 / VS Code 新建 UTF-8 文件(不要用「另存为 ANSI」):
{
"id": "hello-corridor",
"name": "你好走廊",
"version": "1.0.0",
"author": "你的名字",
"description": "把走廊描述改成自定义文案的最小示例。",
"apiVersion": 1,
"gameVersionMin": "4.1.0",
"capabilities": ["content"],
"loadPriority": 0,
"dependencies": []
}
4. 写 content/buildings.json
走廊在游戏里的 id 是 corridor。你可以只写要改的字段,其余字段(造价、占地等)会从游戏基线保留:
[
{
"id": "corridor",
"name": "走廊(模组)",
"description": "【模组生效】如果你能在建造列表里看到这句,说明内容覆盖成功了。",
"gameplayNotes": "这是练习模组,可随时删除整个 hello-corridor 文件夹。"
}
]
5. 启用并验证
- 启动游戏 → 主菜单点 模组
- 列表里应出现「你好走廊」;打开启用开关
- 界面会提示需重启——请完全退出游戏再开
- 新开一局 → 打开建造/图纸 → 找走廊,看名称与描述是否变了
6. 若没变化,按这个顺序查
- 模组页是否列出该模组?没有 → 路径错,或不在子文件夹里,或缺
mod.json - 有错误红字?常见:
apiVersion不是1、JSON 语法坏了 - 已启用但文案没变?是否忘了重启?是否开了另一个优先级更高的模组也改了
corridor? - 是否看了旧存档里已缓存的 UI?优先新开局确认
练完后,删除整个 hello-corridor 文件夹并重启,即可恢复。
模组能做什么 / 不能做什么
能力一览(写在 capabilities)
| 能力 | 你能改什么 | 主要手段 |
|---|---|---|
content | 建筑、SCP、科研、实验、机动特遣学说、百科系统条目、姓名池 | content/ 下 JSON,或脚本 IContentMod |
locale | 主菜单、设置等界面键值(中/英) | locale/zh.json、locale/en.json |
audio | 背景音乐、CASSIE 语音 wav | assets/audio/... 或脚本注册 |
theme | 设置里可选的配色主题 | assets/theme.json |
script | 自定义 SCP profile、游戏事件、主界面 Tab / 选项按钮 | mod.dll + ModSDK |
一个模组可以同时声明多种能力,例如:["content","locale","script"]。
第一版明确不做 / 限制
- 改启用状态必须重启(无热插拔)
- 没有游戏内一键下载(仍手动解压到
Mods/) - 多人联机不支持模组
- 脚本 不能 通过 SDK 直接改资金、解锁科技、改编制(异常 tick API 有意收窄)
- 模组是本地信任代码:只装你信任的来源
启用状态保存在 user://mod_settings.cfg。云存档会记录启用列表,格式如 ["hello-corridor@1.0.0"]。
目录与加载顺序
正式版安装树
<游戏目录>/
SiteDirector.exe
Mods/
my-mod/ ← 每个模组一个子文件夹
mod.json ← 必需
SampleMod.dll ← 脚本时需要(文件名以 entryAssembly 为准)
content/ …
locale/ …
assets/ …
ModSDK/
SiteDirector.ModSDK.dll ← 给作者引用,玩家一般不用动
README.md
推荐完整结构(按需取用)
my-mod/
mod.json
mod.dll
content/
buildings.json
scp/
scp-173.json ← 可覆盖已有,也可新增 scp-xxxx.json
scp-custom.json
research/
tree.json
experiments/
scp-173.json ← 与游戏 content/experiments 同结构
mtf/
doctrines.json
encyclopedia/
systems.json
names.json ← 注意:整表替换,不要只写半份
locale/
zh.json
en.json
assets/
audio/
music.mp3 ← 或 music.ogg,二选一即可
cassie/
boot.wav
breach.wav
theme.json
加载顺序(理解「谁覆盖谁」)
- 游戏加载基线内容(正式版 Lattice / 调试明文)
- 扫描
Mods/*/,读每个mod.json - 按 依赖拓扑 排序,同层再比
loadPriority(越大越晚) - 依次应用文件覆盖与脚本
OnLoad - 后加载的模组覆盖先加载的同 id / 同键
因此:想「压过别人」→ 提高自己的 loadPriority,并确保依赖已启用。
mod.json 字段详解
完整示例
{
"id": "my-mod",
"name": "我的模组",
"version": "1.2.0",
"author": "作者名",
"description": "一句话说明玩家能感到什么。",
"apiVersion": 1,
"gameVersionMin": "4.1.0",
"gameVersionMax": null,
"capabilities": ["content", "locale", "script"],
"loadPriority": 10,
"dependencies": ["some-base-mod"],
"entryAssembly": "mod.dll",
"entryType": "MyMod.MyModEntry"
}
字段表
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
id | string | 是 | 唯一标识;建议与文件夹名一致;勿空格;依赖列表也写这个 |
name | string | 否 | 模组管理界面显示名;缺省用 id |
version | string | 否 | 建议语义化版本;写入云存档 ModList;默认 0.0.1 |
author | string | 否 | 作者 |
description | string | 否 | 详情区说明 |
apiVersion | number | 强烈建议写 | 必须为 1;不匹配会禁用 |
gameVersionMin | string | 建议写 | 最低兼容游戏版本(语义比较) |
gameVersionMax | string | 否 | 最高兼容;省略/null 表示无上限 |
capabilities | string[] | 建议写 | content / locale / audio / theme / script |
loadPriority | number | 否 | 越大越晚加载;默认 0 |
dependencies | string[] | 否 | 其它模组的 id;对方须已安装并启用 |
entryAssembly | string | 脚本建议写 | 入口 DLL 文件名,默认 mod.dll |
entryType | string | 脚本建议写 | 实现 IMod 的完整类型名;可省略(自动扫描第一个实现) |
写法注意
- JSON 允许注释与尾逗号(游戏解析时开启了对应选项),但工坊/其它工具未必支持,发布包建议用严格 JSON。
- 声明了
script却找不到 DLL → 模组页报missing …。 - 只做数据模组时 不要 写
script,避免误报缺 DLL。 entryType必须是命名空间.类名,例如SampleMod.SampleModEntry。
数据模组(content)
合并规则(务必读懂)
| 类型 | 规则 |
|---|---|
带 id 的数组(建筑、SCP、科研、实验、学说、百科等) | 同 id 字段级深合并:你写的字段覆盖基线,没写的字段保留 |
嵌套对象(如 behavior) | 同样按字段合并,可只改 behavior.profile |
names.json | 整份对象替换(不是数组合并)。请从游戏拷完整表再改 |
新 id | 追加为新条目 |
因此「只改走廊文案」是安全的;「新增建筑」则必须自备完整字段,并想清楚解锁与放置链路。
文件 ↔ 内部 payload
| 模组相对路径 | payload 名 | 格式 |
|---|---|---|
content/buildings.json | buildings | 对象数组 |
content/scp/*.json | scp | 每文件一个对象,或数组;文件夹内按文件名排序后合并 |
content/research/tree.json | research | 对象数组 |
content/experiments/*.json | experiments | 同 scp 文件夹规则 |
content/mtf/doctrines.json | mtf_doctrines | 对象数组 |
content/encyclopedia/systems.json | encyclopedia_systems | 对象数组 |
content/names.json | names | 对象(非数组) |
路径必须按上表;写错文件名或少一层目录 → 静默不加载该文件。
建筑 buildings.json
对照游戏 content/buildings.json。常用字段:
| 字段 | 类型 | 含义 |
|---|---|---|
id | string | 唯一 id,覆盖时必须一致 |
name | string | 显示名 |
description | string | 长描述(百科/详情) |
gameplayNotes | string | 玩法提示 |
width / height | number | 占地格数 |
cost | number | 造价 |
powerUse / powerGen | number | 耗电 / 发电 |
category | string | 如 corridor、room、stair |
role | number | 岗位角色枚举(-1 常表示无岗位) |
maxWorkers | number | 最大工人数 |
requiresDoor / doorCount | bool / number | 门需求 |
placement | string | 放置约束,如 surface、area_edge |
只改文案示例(已在快速开始用过):
[
{
"id": "corridor",
"name": "走廊(模组)",
"description": "自定义描述……"
}
]
改造价示例:
[
{
"id": "stair",
"cost": 100
}
]
SCP:覆盖与新增
对照 content/scp/scp-173.json。关键字段:
| 字段 | 含义 |
|---|---|
id | 如 scp-173;新异常请用新 id |
title | 显示标题 |
objectClass | Safe / Euclid / Keter… |
category | 如 biological |
summary / gameplayNotes | 简介与玩法 |
requiredContainment | 收容强度需求 |
containmentBuildingId | 专属舱建筑 id(新增 SCP 时常需配套建筑) |
appearWeight / minAppearDay | 出现权重与最早天数 |
researchSeconds / bioResearchSeconds | 研究耗时 |
allowsBioResearch | 是否生物研究 |
mtfSquadMin / mtfTags | 特遣人数与标签 |
behavior | 行为块,见下 |
behavior 常用子字段:
| 字段 | 含义 |
|---|---|
profile | 行为配置名(内置或脚本注册) |
moveSpeed | 移动速度 |
pauseMin / pauseMax | 停顿区间 |
requiresWatch | 是否需要注视 |
breachRiskPerSecond | 破出风险 |
mtfDifficultyBonus | 特遣难度加成 |
内置 profile 示例(以当前游戏为准):
observation_snap、old_ai、bounce_hazard、cold_aura、electronics_glitch、growth_hazard、shy_rage、plague_doctor、skin_seeker、phase_abduct、pack_hunter
只改 173 简介:
{
"id": "scp-173",
"title": "SCP-173(模组注释版)",
"summary": "塑像还是塑像,但简介被模组改写了。",
"gameplayNotes": "玩法未改,仅文案演示。"
}
保存为 content/scp/scp-173.json。
只改 profile(需脚本注册同名 profile,或改用内置名):
{
"id": "scp-173",
"behavior": {
"profile": "my_pulse"
}
}
科研 research/tree.json
对照 content/research/tree.json。节点示例字段:
| 字段 | 含义 |
|---|---|
id | 节点 id |
name / description | 显示 |
kind | 如 standard |
column / row | 树坐标 |
prereqs | 前置节点 id 数组 |
unlocks | 解锁的建筑等 id |
researchSeconds | 研究时间 |
startsUnlocked | 是否开局已掌握 |
branch | 分支,如 site / power |
覆盖已有节点时只改 description 即可;新增节点需自己串好 prereqs / unlocks。
实验 experiments/*.json
对照 content/experiments/scp-173.json。单条实验常用字段:
| 字段 | 含义 |
|---|---|
id | 实验唯一 id |
scpId | 关联 SCP |
name / description | 显示 |
researchSeconds | 耗时 |
elucidationGain | 阐明收益 |
o5Reward | O5 奖励 |
requiresDClass / dClassCount / dDeathChance | D 级需求与风险 |
prereqs | 前置实验 id |
elucidationMin | 最低阐明 |
riskNote | 风险说明 |
机动特遣 mtf/doctrines.json
对照 content/mtf/doctrines.json:
| 字段 | 含义 |
|---|---|
id | 学说 id |
designation / codename | 番号与代号 |
specialtyTag | 专长标签 |
matchBonus / firstContainBonus / recontainBonus | 加成 |
unlockId | 解锁科技 id |
summary / gameplayNotes | 文案 |
百科 encyclopedia/systems.json
对照 content/encyclopedia/systems.json:
| 字段 | 含义 |
|---|---|
id | 条目 id |
name | 标题 |
category | 分类 |
sortOrder | 排序 |
description / gameplayNotes | 正文与玩法 |
姓名池 names.json
结构为对象,至少含姓/名等数组(见游戏 content/names.json)。
替换时请复制完整文件再改,只放半份会导致招聘姓名异常。
语言包(locale)
文件
locale/zh.json— 中文locale/en.json— 英文
格式为扁平键值(不要嵌套对象):
{
"start": "单人经营(模组)",
"tagline": "Secure. Contain. Protect. — 模组已加载。",
"mods": "模组",
"settings_title": "站点设置"
}
后加载模组覆盖同键。切换语言后,主界面会刷新建筑 / SCP / 科研列表。
常用键(主菜单与设置)
| 键 | 默认中文含义 |
|---|---|
brand | 顶栏品牌 |
sub | 副标题「站点主管」 |
tagline | 标语 |
boot_skip_hint | 跳过开场提示 |
start | 单人经营 |
multiplayer | 多人联机 |
settings | 设置 |
mods | 模组按钮 |
mods_title | 模组管理标题 |
mods_restart_hint | 需重启提示 |
mods_empty | 空列表提示 |
quit | 退出 |
login / login_title | 登录相关 |
settings_title | 设置页标题 |
settings_lang | 语言 |
settings_ui_theme | 颜色主题 |
settings_volume / settings_music_volume / settings_cassie_volume | 音量 |
theme_navy / theme_pink / theme_red | 内置主题名 |
带 {0} {1} 的键是格式串,覆盖时请保留占位符。
完整表以游戏 Loc 源码为准:先搜现有中文句子,再反查键名。
脚本注册(与文件等效)
ctx.Locale.RegisterStrings("zh", new Dictionary<string, string>
{
["tagline"] = "来自脚本的标语",
});
音频模组
背景音乐
在模组内放置其中一个即可(后加载覆盖):
assets/audio/music.mp3assets/audio/music.ogg
脚本:
ctx.Audio.SetBackgroundMusic("assets/audio/my_theme.ogg");
// 路径相对模组根目录,也可写绝对路径
建议:循环友好、音量别过大;玩家仍可用设置里的音乐音量调节。
CASSIE 语音
把 wav 放到:
assets/audio/cassie/{code}.wav
code = 文件名去掉 .wav,必须与游戏消息码一致。
系统类常用码
| code | 用途 |
|---|---|
boot | 上线自检 |
online / offline | 恢复 / 离线 |
voice_test | 试听 |
unknown | 未分类通报 |
态势类示例:pressure_high、expansion_lock、expansion_unlock、power_restored、reactor_warning、reactor_scram…
收容 / 封锁:breach、area_lockdown(缺文件时会回退尝试其它码)
核弹 / 协议:warhead_arm_alpha、warhead_cancel、o5_arm、terminus_arm 等
完整对照见游戏仓库 assets/audio/cassie/VOICE_LINES.md。
脚本注册:
ctx.Audio.RegisterCassieVoice("breach", "assets/audio/cassie/breach.wav");
格式建议:PCM wav,单声道或立体声均可;过长的句子会被播报逻辑截断观感,尽量贴近原时长。
UI 主题
文件方式
assets/theme.json:颜色为 RGBA 0–1 浮点数组。未填字段继承海军蓝默认。
启用后主题 id 为 mod:<模组id>,在 设置 → 颜色主题 中选择(仅已启用且提供主题的模组会出现)。
{
"Accent": [0.20, 0.70, 0.50, 1],
"AccentSoft": [0.40, 0.85, 0.70, 1],
"Panel": [0.05, 0.10, 0.08, 0.97],
"PanelDeep": [0.03, 0.07, 0.06, 0.98],
"Edge": [0.25, 0.45, 0.35, 1],
"HeaderRule": [0.30, 0.55, 0.40, 1],
"ButtonNormalBg": [0.08, 0.14, 0.12, 0.95],
"ButtonHoverBg": [0.12, 0.20, 0.16, 0.95],
"ButtonPressedBg": [0.06, 0.12, 0.10, 0.98],
"TabNormalBg": [0.06, 0.10, 0.09, 0.9],
"TabActiveBg": [0.10, 0.18, 0.14, 0.95],
"FieldBg": [0.04, 0.08, 0.07, 0.95],
"ItemSelected": [0.15, 0.35, 0.25, 0.85],
"BannerBg": [0.05, 0.12, 0.10, 0.92],
"StatusBg": [0.05, 0.10, 0.09, 0.9],
"AlertBg": [0.25, 0.08, 0.08, 0.92],
"ScrollBorder": [0.20, 0.40, 0.30, 1],
"FontPressed": [0.85, 0.95, 0.90, 1],
"Sheen": [0.40, 0.80, 0.60, 0.15]
}
ModThemePalette 全部可选字段:Panel、PanelDeep、Edge、Accent、AccentSoft、HeaderRule、ButtonNormalBg、ButtonHoverBg、ButtonPressedBg、ButtonDisabledBg、ButtonDisabledBorder、TabNormalBg、TabActiveBg、TabHoverBg、TabShadow、FieldBg、ItemSelected、ItemSelectedFocus、ItemCursor、BannerBg、StatusBg、AlertBg、ScrollBorder、FontPressed、Sheen。
脚本方式
ctx.Theme.RegisterPalette("emerald", new ModThemePalette
{
Accent = new[] { 0.2f, 0.7f, 0.5f, 1f },
AccentSoft = new[] { 0.4f, 0.85f, 0.7f, 1f },
});
C# 脚本模组:从零到能加载
你需要准备
- .NET 9 SDK(
dotnet --version能看到 9.x) SiteDirector.ModSDK.dll- 正式版:游戏目录
ModSDK/SiteDirector.ModSDK.dll - 源码:工程
mod-sdk/SiteDirector.ModSDK/(可 ProjectReference)
- 正式版:游戏目录
- 不要引用主游戏程序集(
SiteDirector.dll)——模组只能依赖 ModSDK
建立项目
dotnet new classlib -n MyMod -f net9.0
cd MyMod
MyMod.csproj 示例(引用发行 DLL):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>mod</AssemblyName>
<RootNamespace>MyMod</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="SiteDirector.ModSDK">
<HintPath>..\path\to\ModSDK\SiteDirector.ModSDK.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
若在游戏仓库内开发,可改为:
<ProjectReference Include="..\..\mod-sdk\SiteDirector.ModSDK\SiteDirector.ModSDK.csproj" />
最小入口类
using SiteDirector.ModSDK;
namespace MyMod;
public sealed class MyModEntry : IMod
{
public string ModId => "my-mod"; // 建议与 mod.json 的 id 一致
public void OnLoad(ModContext ctx)
{
ctx.Locale.RegisterStrings("zh", new Dictionary<string, string>
{
["tagline"] = "来自脚本模组的标语",
});
}
public void OnUnload() { }
}
对应 mod.json
{
"id": "my-mod",
"name": "我的脚本模组",
"version": "1.0.0",
"author": "你",
"description": "演示脚本加载",
"apiVersion": 1,
"gameVersionMin": "4.1.0",
"capabilities": ["locale", "script"],
"entryAssembly": "mod.dll",
"entryType": "MyMod.MyModEntry"
}
编译与部署清单
dotnet build -c Release
把下列文件拷到 Mods/my-mod/:
| 文件 | 说明 |
|---|---|
mod.json | 清单 |
bin/Release/net9.0/mod.dll | 与 entryAssembly 同名 |
(可选)locale/、content/、assets/ | 数据与资源 |
不要把 SiteDirector.ModSDK.dll 拷进模组文件夹(游戏已自带)。
然后:模组页启用 → 重启 → 看主菜单标语。
对照官方示例
仓库 examples/sample-mod/:
dotnet build examples/sample-mod/SampleMod.csproj -c Release
把 content/、locale/、mod.json 与输出的 SampleMod.dll 放到 Mods/sample-mod/(entryAssembly 已是 SampleMod.dll)。
ModContext API(v1)一览
加载时游戏会传入 ModContext:
| 成员 | 用途 |
|---|---|
ModId / ModRootPath / ApiVersion / GameVersion | 只读环境信息 |
Content.Patch(Action<ModContentBuilder>) | 程序化写 payload |
Locale.RegisterStrings(locale, dict) | 文案 |
Audio.SetBackgroundMusic(path) | BGM |
Audio.RegisterCassieVoice(code, path) | CASSIE |
Theme.RegisterPalette(id, palette) | 主题 |
Profiles.Register(handler) | SCP profile |
Events.Subscribe(subscriber) | 事件 |
Ui.RegisterMainTab(...) | 主界面 Tab(factory 返回 Godot Control) |
Ui.RegisterOptionsButton(...) | 选项菜单按钮 |
也可实现接口让游戏自动挂钩:
| 接口 | 方法 |
|---|---|
IMod | OnLoad / OnUnload(入口必须) |
IContentMod | PatchContent(ModContentBuilder) |
IAnomalyProfileHandler | ProfileId、TickContained、可选 TickBreach |
IUiMod | RegisterUi(IModUiRegistry) |
IGameEventSubscriber | OnGameEvent(GameEvent) |
ModContentBuilder:
SetPayloadJson(payloadId, json)— 整段替换某 payloadUpsertJsonObject(payloadId, objectJson)— 按对象id插入/覆盖单条
payloadId 取值:buildings、scp、research、experiments、mtf_doctrines、encyclopedia_systems、names。
自定义 SCP 行为
思路
- 用脚本注册一个
ProfileId(如my_pulse) - 在 SCP JSON 的
behavior.profile写成同一个名字 - 每帧收容态调用
TickContained;破出态可实现TickBreach
完整示例
using SiteDirector.ModSDK;
public sealed class PulseProfile : IAnomalyProfileHandler
{
public string ProfileId => "my_pulse";
public void TickContained(AnomalyTickContext ctx)
{
if (ctx.GetAbilityCooldown() > 0f) return;
if (!ctx.Powered) return; // 断电时可以什么都不做
ctx.EmitMessage("[模组] my_pulse 轻微推高收容压力。");
ctx.AdjustPressure(0.5f);
ctx.SetAbilityCooldown(10f);
}
public void TickBreach(AnomalyTickContext ctx)
{
// 破出态逻辑;不需要可留空(接口有默认空实现)
}
}
在 OnLoad:
ctx.Profiles.Register(new PulseProfile());
// 若入口类自身实现了 IAnomalyProfileHandler,也可 ctx.Profiles.Register(this);
SCP JSON:
{
"id": "scp-xxxx",
"title": "自定义异常",
"objectClass": "Euclid",
"summary": "……",
"behavior": {
"profile": "my_pulse",
"moveSpeed": 1.5,
"breachRiskPerSecond": 0.001
}
}
放在 content/scp/scp-xxxx.json。若改的是已有 SCP,保留原 id 只改 behavior 即可。
AnomalyTickContext
| 成员 | 说明 |
|---|---|
ScpId / ProfileId | 标识 |
DeltaTime | 帧间隔 |
Powered | 舱室是否供电 |
IsBreaching | 是否破出 |
Floor / PosX / PosY | 位置 |
GetAbilityCooldown() / SetAbilityCooldown(seconds) | 冷却 |
EmitMessage(text) | 主界面消息 |
AdjustPressure(delta) | 调整收容压力 |
没有:改资金、解锁、招聘、直接杀员工等。请用消息与压力表达设计意图。
调试建议
- 先
EmitMessage确认 tick 在跑 - 用较长冷却,避免刷屏
Random触发时注意DeltaTime,别每帧高概率触发
UI 扩展
第一版只支持 注入:选项菜单按钮、主界面 Tab(返回 Godot Control)。不能替换整场景。
public sealed class MyUiMod : IMod, IUiMod
{
public string ModId => "my-mod";
public void OnLoad(ModContext ctx) { }
public void OnUnload() { }
public void RegisterUi(IModUiRegistry registry)
{
registry.RegisterOptionsButton("my_about", "关于本模组", () =>
{
// 轻量回调;勿长时间阻塞主线程
// 复杂面板请用 RegisterMainTab
});
// registry.RegisterMainTab("my_tab", "模组页", () =>
// {
// var panel = new Godot.PanelContainer();
// // …构建子控件
// return panel;
// });
}
}
标题参数可以是 locale 键(优先翻译)或字面量。
Tab 的 factory 若抛错,游戏会回退到空面板,请在本地先测。
事件订阅
事件种类
GameEventKind | 何时 |
|---|---|
GameStarted | 新局开始 |
DayEnded | 跨日结算 |
ScpContained | 收容成功(Data 可能含 scpId) |
BreachStarted | 破出开始 |
Message | 主界面消息(很频繁) |
ResearchCompleted | 科研完成 |
StaffChanged | 人事变化(较频繁) |
GameEvent 字段:Kind、Text、DayIndex、Data(只读字典)。
示例
public sealed class MyEvents : IMod, IGameEventSubscriber
{
public string ModId => "my-mod";
public void OnLoad(ModContext ctx) => ctx.Events.Subscribe(this);
public void OnUnload() { }
public void OnGameEvent(GameEvent evt)
{
switch (evt.Kind)
{
case GameEventKind.GameStarted:
break;
case GameEventKind.DayEnded:
// evt.DayIndex
break;
case GameEventKind.ScpContained:
// evt.Text / evt.Data?["scpId"]
break;
case GameEventKind.Message:
// 慎用:几乎所有提示都会进来
break;
}
}
}
入口类若实现了 IGameEventSubscriber,游戏在加载时也会自动加入订阅(与 Subscribe 二选一或并存时注意别重复逻辑)。
版本与兼容
apiVersion必须等于游戏当前 Mod API(现为 1)。gameVersionMin/gameVersionMax与游戏config/version做语义比较。- SDK 破坏性变更会递增
apiVersion;届时需重编译并升清单。 dependencies中的模组必须已安装、已启用,且不能循环依赖。- 发工坊时写清:测试过的游戏版本、平台(Windows / Android)、已知冲突模组。
发布到工坊
打包
- 确认本地启用 → 重启 → 功能正常
- 打 zip:解压后玩家应得到
my-mod/mod.json这一层(或按工坊页安装说明) - 不要打包
bin/、obj/、.cs、.csproj(脚本模组只留编译好的 dll + 资源)
发帖
- 官网社区 → 分类 自制模组
- 写清:模组版本、兼容游戏版本、平台、简介、截图
- 安装步骤建议与工坊字段对齐:
[
{ "title": "解压", "body": "解压到游戏 Mods/ 目录,保证出现 Mods/你的模组/mod.json。" },
{ "title": "启用", "body": "主菜单 → 模组 → 打开开关 → 完全重启游戏。" },
{ "title": "验证", "body": "新开局检查某某文案/功能是否变化。" }
]
兼容矩阵:
[
{ "gameVersion": "4.1.0", "status": "supported", "note": "完整测试" },
{ "gameVersion": "4.0.2", "status": "unsupported" }
]
status 只能是:supported | partial | unsupported。
依赖(工坊元数据):
[
{ "name": "某前置模组", "required": true, "url": "https://…" }
]
调试与排错
日志在哪
- Godot / 游戏控制台:搜前缀
[ModManager] - 主菜单「模组」详情:显示
Error字符串
常见错误对照
| 现象 / 报错 | 原因与处理 |
|---|---|
| 列表根本没有模组 | 不在 Mods/子文件夹/;缺 mod.json;编辑器模式应放工程内 Mods/ |
missing mod.json | 文件名大小写或放错层 |
apiVersion X != 1 | 改成 1 |
missing SampleMod.dll 等 | capabilities 含 script 但 DLL 名与 entryAssembly 不一致,或没拷到模组目录 |
no IMod entry type | entryType 写错命名空间;或类未 public、未实现 IMod、是抽象类 |
IMod construct failed | 入口类没有无参构造函数 |
| 启用了没效果 | 没重启;被更高 loadPriority 覆盖;看的不是新开局 |
| 内容部分字段丢失 | 已支持字段合并;若仍异常检查 JSON 是否不是数组、或 id 写错 |
names 姓名异常 | names.json 是整表替换,勿只提交半份 |
| 脚本一加载就挂 | OnLoad 抛异常;先做空 OnLoad 再逐步加逻辑 |
| Android 找不到目录 | 看模组页路径提示;用 ADB/adb push 到该路径 |
推荐工作流
- 纯 content:只改一条建筑文案,验证目录与重启流程
- 加 locale:改
tagline,确认中英文 - 加 script:入口只
RegisterStrings,确认 DLL 能加载 - 再加 profile / 事件 / UI
- 发布前用「关闭模组 → 重启」确认可卸载
限制与公平性
- 脚本 DLL 是本地信任代码,等同于在用户机器上跑你的程序。
- 正式版基线仍受保护;模组是显式明文扩展通道,不是用来偷偷改发行包的。
- 云存档带
ModList,便于识别模组局与排查。 - 多人联机暂不支持模组。
- 平衡:异常 tick 不能直接改钱;请用压力、消息、内容表做设计。
附录 A:发布前自检清单
-
mod.json:id/version/apiVersion=1/gameVersionMin -
capabilities与真实文件、DLL 一致 - Windows 正式版路径可被扫描;编辑器路径已测
- 启用 → 完全重启 → 可感知变化
- 关闭模组 → 重启 → 行为恢复(或文档写明不可逆)
- JSON UTF-8、无 BOM 问题;数组
id正确 - 脚本未引用主游戏程序集;只依赖 ModSDK
- 工坊写清兼容版本、平台、安装三步、已知问题
附录 B:最小文件速查
| 目标 | 最少文件 |
|---|---|
| 改建筑/SCP 文案 | mod.json + content/... |
| 改菜单文案 | mod.json + locale/zh.json |
| 换 BGM | mod.json + assets/audio/music.ogg |
| 换主题 | mod.json + assets/theme.json |
| Hello 脚本 | mod.json + mod.dll(capabilities 含 script) |
附录 C:官方示例对照
examples/sample-mod/ 演示了:
content/buildings.json覆盖走廊文案locale/*.json改菜单文案SampleModEntry:locale 注册、sample_idleprofile、事件订阅
按该目录 README 编译并拷入 Mods/sample-mod/ 即可对照。
祝制作顺利。完整可运行示例见游戏仓库 examples/sample-mod/;玩家安装说明见 模组工坊。