站点主管:收容协议

Docs · Modding

模组开发手册

左侧目录可跳转。建议从「快速开始」做第一个无代码模组,再进入脚本与 SCP 行为。

站点主管 · 模组开发手册

本手册面向第一次做模组的作者。目标是:不看源码也能做出可安装的模组;进阶作者能写 C# 脚本扩展 SCP 行为与界面。

玩家侧安装与下载见 模组工坊。开发问题可到社区反馈。

API 版本apiVersion = 1(程序集 SiteDirector.ModSDK
游戏版本:建议 gameVersionMin4.1.0
官方示例:游戏仓库 SiteDirector/examples/sample-mod/
清单文件:必须叫 mod.json(小写)

建议阅读顺序

  1. 快速开始(无代码,10 分钟)
  2. 目录与加载顺序mod.json
  3. 按需要看 数据 / 语言 / 音频 / 主题
  4. 要写脚本再看 C# 与后续章节

快速开始: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. 启用并验证

  1. 启动游戏 → 主菜单点 模组
  2. 列表里应出现「你好走廊」;打开启用开关
  3. 界面会提示需重启——请完全退出游戏再开
  4. 新开一局 → 打开建造/图纸 → 找走廊,看名称与描述是否变了

6. 若没变化,按这个顺序查

  1. 模组页是否列出该模组?没有 → 路径错,或不在子文件夹里,或缺 mod.json
  2. 有错误红字?常见:apiVersion 不是 1、JSON 语法坏了
  3. 已启用但文案没变?是否忘了重启?是否开了另一个优先级更高的模组也改了 corridor
  4. 是否看了旧存档里已缓存的 UI?优先新开局确认

练完后,删除整个 hello-corridor 文件夹并重启,即可恢复。


模组能做什么 / 不能做什么

能力一览(写在 capabilities)

能力你能改什么主要手段
content建筑、SCP、科研、实验、机动特遣学说、百科系统条目、姓名池content/ 下 JSON,或脚本 IContentMod
locale主菜单、设置等界面键值(中/英)locale/zh.jsonlocale/en.json
audio背景音乐、CASSIE 语音 wavassets/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

加载顺序(理解「谁覆盖谁」)

  1. 游戏加载基线内容(正式版 Lattice / 调试明文)
  2. 扫描 Mods/*/,读每个 mod.json
  3. 依赖拓扑 排序,同层再比 loadPriority(越大越晚)
  4. 依次应用文件覆盖与脚本 OnLoad
  5. 后加载的模组覆盖先加载的同 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"
}

字段表

字段类型必需说明
idstring唯一标识;建议与文件夹名一致;勿空格;依赖列表也写这个
namestring模组管理界面显示名;缺省用 id
versionstring建议语义化版本;写入云存档 ModList;默认 0.0.1
authorstring作者
descriptionstring详情区说明
apiVersionnumber强烈建议写必须为 1;不匹配会禁用
gameVersionMinstring建议写最低兼容游戏版本(语义比较)
gameVersionMaxstring最高兼容;省略/null 表示无上限
capabilitiesstring[]建议写content / locale / audio / theme / script
loadPrioritynumber越大越晚加载;默认 0
dependenciesstring[]其它模组的 id;对方须已安装并启用
entryAssemblystring脚本建议写入口 DLL 文件名,默认 mod.dll
entryTypestring脚本建议写实现 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.jsonbuildings对象数组
content/scp/*.jsonscp每文件一个对象,或数组;文件夹内按文件名排序后合并
content/research/tree.jsonresearch对象数组
content/experiments/*.jsonexperiments同 scp 文件夹规则
content/mtf/doctrines.jsonmtf_doctrines对象数组
content/encyclopedia/systems.jsonencyclopedia_systems对象数组
content/names.jsonnames对象(非数组)

路径必须按上表;写错文件名或少一层目录 → 静默不加载该文件。

建筑 buildings.json

对照游戏 content/buildings.json。常用字段:

字段类型含义
idstring唯一 id,覆盖时必须一致
namestring显示名
descriptionstring长描述(百科/详情)
gameplayNotesstring玩法提示
width / heightnumber占地格数
costnumber造价
powerUse / powerGennumber耗电 / 发电
categorystringcorridorroomstair
rolenumber岗位角色枚举(-1 常表示无岗位)
maxWorkersnumber最大工人数
requiresDoor / doorCountbool / number门需求
placementstring放置约束,如 surfacearea_edge

只改文案示例(已在快速开始用过):

[
  {
    "id": "corridor",
    "name": "走廊(模组)",
    "description": "自定义描述……"
  }
]

改造价示例

[
  {
    "id": "stair",
    "cost": 100
  }
]

SCP:覆盖与新增

对照 content/scp/scp-173.json。关键字段:

字段含义
idscp-173;新异常请用新 id
title显示标题
objectClassSafe / Euclid / Keter…
categorybiological
summary / gameplayNotes简介与玩法
requiredContainment收容强度需求
containmentBuildingId专属舱建筑 id(新增 SCP 时常需配套建筑)
appearWeight / minAppearDay出现权重与最早天数
researchSeconds / bioResearchSeconds研究耗时
allowsBioResearch是否生物研究
mtfSquadMin / mtfTags特遣人数与标签
behavior行为块,见下

behavior 常用子字段:

字段含义
profile行为配置名(内置或脚本注册)
moveSpeed移动速度
pauseMin / pauseMax停顿区间
requiresWatch是否需要注视
breachRiskPerSecond破出风险
mtfDifficultyBonus特遣难度加成

内置 profile 示例(以当前游戏为准):
observation_snapold_aibounce_hazardcold_auraelectronics_glitchgrowth_hazardshy_rageplague_doctorskin_seekerphase_abductpack_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显示
kindstandard
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阐明收益
o5RewardO5 奖励
requiresDClass / dClassCount / dDeathChanceD 级需求与风险
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.mp3
  • assets/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_highexpansion_lockexpansion_unlockpower_restoredreactor_warningreactor_scram

收容 / 封锁breacharea_lockdown(缺文件时会回退尝试其它码)

核弹 / 协议warhead_arm_alphawarhead_cancelo5_armterminus_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 全部可选字段:PanelPanelDeepEdgeAccentAccentSoftHeaderRuleButtonNormalBgButtonHoverBgButtonPressedBgButtonDisabledBgButtonDisabledBorderTabNormalBgTabActiveBgTabHoverBgTabShadowFieldBgItemSelectedItemSelectedFocusItemCursorBannerBgStatusBgAlertBgScrollBorderFontPressedSheen

脚本方式

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# 脚本模组:从零到能加载

你需要准备

  1. .NET 9 SDKdotnet --version 能看到 9.x)
  2. SiteDirector.ModSDK.dll
    • 正式版:游戏目录 ModSDK/SiteDirector.ModSDK.dll
    • 源码:工程 mod-sdk/SiteDirector.ModSDK/(可 ProjectReference)
  3. 不要引用主游戏程序集(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.dllentryAssembly 同名
(可选)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(...)选项菜单按钮

也可实现接口让游戏自动挂钩:

接口方法
IModOnLoad / OnUnload(入口必须)
IContentModPatchContent(ModContentBuilder)
IAnomalyProfileHandlerProfileIdTickContained、可选 TickBreach
IUiModRegisterUi(IModUiRegistry)
IGameEventSubscriberOnGameEvent(GameEvent)

ModContentBuilder

  • SetPayloadJson(payloadId, json) — 整段替换某 payload
  • UpsertJsonObject(payloadId, objectJson) — 按对象 id 插入/覆盖单条

payloadId 取值:buildingsscpresearchexperimentsmtf_doctrinesencyclopedia_systemsnames


自定义 SCP 行为

思路

  1. 用脚本注册一个 ProfileId(如 my_pulse
  2. 在 SCP JSON 的 behavior.profile 写成同一个名字
  3. 每帧收容态调用 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 字段:KindTextDayIndexData(只读字典)。

示例

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 二选一或并存时注意别重复逻辑)。


版本与兼容

  1. apiVersion 必须等于游戏当前 Mod API(现为 1)。
  2. gameVersionMin / gameVersionMax 与游戏 config/version 做语义比较。
  3. SDK 破坏性变更会递增 apiVersion;届时需重编译并升清单。
  4. dependencies 中的模组必须已安装、已启用,且不能循环依赖。
  5. 发工坊时写清:测试过的游戏版本、平台(Windows / Android)、已知冲突模组。

发布到工坊

打包

  1. 确认本地启用 → 重启 → 功能正常
  2. 打 zip:解压后玩家应得到 my-mod/mod.json 这一层(或按工坊页安装说明)
  3. 不要打包 bin/obj/.cs.csproj(脚本模组只留编译好的 dll + 资源)

发帖

  1. 官网社区 → 分类 自制模组
  2. 写清:模组版本、兼容游戏版本、平台、简介、截图
  3. 安装步骤建议与工坊字段对齐:
[
  { "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.dllcapabilitiesscript 但 DLL 名与 entryAssembly 不一致,或没拷到模组目录
no IMod entry typeentryType 写错命名空间;或类未 public、未实现 IMod、是抽象类
IMod construct failed入口类没有无参构造函数
启用了没效果没重启;被更高 loadPriority 覆盖;看的不是新开局
内容部分字段丢失已支持字段合并;若仍异常检查 JSON 是否不是数组、或 id 写错
names 姓名异常names.json 是整表替换,勿只提交半份
脚本一加载就挂OnLoad 抛异常;先做空 OnLoad 再逐步加逻辑
Android 找不到目录看模组页路径提示;用 ADB/adb push 到该路径

推荐工作流

  1. 纯 content:只改一条建筑文案,验证目录与重启流程
  2. 加 locale:改 tagline,确认中英文
  3. 加 script:入口只 RegisterStrings,确认 DLL 能加载
  4. 再加 profile / 事件 / UI
  5. 发布前用「关闭模组 → 重启」确认可卸载

限制与公平性

  • 脚本 DLL 是本地信任代码,等同于在用户机器上跑你的程序。
  • 正式版基线仍受保护;模组是显式明文扩展通道,不是用来偷偷改发行包的。
  • 云存档带 ModList,便于识别模组局与排查。
  • 多人联机暂不支持模组。
  • 平衡:异常 tick 不能直接改钱;请用压力、消息、内容表做设计。

附录 A:发布前自检清单

  • mod.jsonid / version / apiVersion=1 / gameVersionMin
  • capabilities 与真实文件、DLL 一致
  • Windows 正式版路径可被扫描;编辑器路径已测
  • 启用 → 完全重启 → 可感知变化
  • 关闭模组 → 重启 → 行为恢复(或文档写明不可逆)
  • JSON UTF-8、无 BOM 问题;数组 id 正确
  • 脚本未引用主游戏程序集;只依赖 ModSDK
  • 工坊写清兼容版本、平台、安装三步、已知问题

附录 B:最小文件速查

目标最少文件
改建筑/SCP 文案mod.json + content/...
改菜单文案mod.json + locale/zh.json
换 BGMmod.json + assets/audio/music.ogg
换主题mod.json + assets/theme.json
Hello 脚本mod.json + mod.dllcapabilitiesscript

附录 C:官方示例对照

examples/sample-mod/ 演示了:

  • content/buildings.json 覆盖走廊文案
  • locale/*.json 改菜单文案
  • SampleModEntry:locale 注册、sample_idle profile、事件订阅

按该目录 README 编译并拷入 Mods/sample-mod/ 即可对照。


祝制作顺利。完整可运行示例见游戏仓库 examples/sample-mod/;玩家安装说明见 模组工坊