一个面向 AI 迭代的《杀戮尖塔2》mod 工程,不应该只停留在“能编译一个 DLL”。更实用的框架是四件事合在一起:
- 一个能被游戏识别的 mod 包:
mod_manifest.json、.pck、.dll。 - 一个稳定的 C# 入口:
[ModInitializer]初始化、Harmony patch、运行时服务启动。 - 一个本地自动化链路:构建、打包、复制到游戏
mods/<mod_id>/、重启游戏、等待服务恢复。 - 一个给 AI 用的外部控制面:低 token 的 HTTP API、CLI、日志、等待语义和 smoke check。
STS2_taku-agent 的核心经验是:先把 mod 做成一个“游戏内探针 + 本地服务”,再把 AI 的读写行为收束到 CLI。AI 不直接猜 Godot 节点,也不直接操作游戏窗口,而是通过 mod 暴露的稳定动作面来读状态、执行动作、等待转场、复盘日志。
这个仓库的本地开发环境主要由四类东西组成。
第一类是游戏本体。脚本默认使用 macOS Steam 安装路径:
~/Library/Application Support/Steam/steamapps/common/Slay the Spire 2/SlayTheSpire2.app实际开发时要关心两个目录:
Contents/Resources/data_sts2_macos_arm64:编译时引用游戏程序集,例如sts2.dll、0Harmony.dll、GodotSharp.dll。Contents/MacOS/mods/<mod_id>/:运行时放置 mod 的mod_manifest.json、.pck、.dll。
第二类是 .NET。当前 mod 工程是 net9.0,src/TakuAgentMod.csproj 通过 STS2_GAME_DIR 指向游戏数据目录,并引用游戏暴露出来的程序集:
<Reference Include="sts2"> <HintPath>$(STS2_GAME_DIR)/sts2.dll</HintPath> <Private>false</Private></Reference>第三类是 Godot 打包能力。pack/ 目录会被 tools/build_pck.gd 打成 .pck。即使当前主要逻辑在 DLL,.pck 仍然是 mod 包的一部分,用来承载 manifest 之外的资源、localization 或后续 Godot 侧内容。
第四类是 Node 24。仓库的 ./sts CLI 是 TypeScript 源码直接运行,依赖 Node 24 的内置 TypeScript stripping;CLI 的检查命令是:
npm run verify:cli游戏怎么加载 mod
Section titled “游戏怎么加载 mod”从当前实践看,STS2 的 mod 加载契约可以理解成一个目录协议:游戏启动时扫描 mods/<mod_id>/,读取 mod_manifest.json,再按 manifest 声明加载同目录下的 .pck 和 .dll。
当前 manifest 是:
{ "id": "taku_agent", "has_pck": true, "has_dll": true, "affects_gameplay": true}DLL 里真正的入口是 src/ModEntry.cs:
[ModInitializer(nameof(Initialize))]public static class ModEntry{ public static void Initialize() { _harmony ??= new Harmony("io.retr0.sts2.taku_agent"); _harmony.PatchAll(typeof(ModEntry).Assembly); ObservationServer.Start(); }}这里有三个关键点。
第一,入口用游戏 modding API 的 [ModInitializer] 标记,而不是自己去找进程注入点。
第二,初始化只做一次,并把启动过程写入 ~/Library/Application Support/STS2TakuAgent/bootstrap.log。这类 marker 很重要,因为 mod 没启动、Harmony patch 没打上、HTTP server 没开,外部看起来都可能只是“CLI 连不上”。
第三,真正扩展游戏行为靠 Harmony patch。当前仓库 patch 了三个战斗生命周期点:
CombatManager.SetUpCombat的 postfix:战斗建立后采样。CombatManager.SetupPlayerTurn的 postfix 包装异步返回:玩家回合开始后采样。Hook.AfterCardPlayed的 postfix 包装异步返回:卡牌打出并结算后采样和记录。
这三个点不追求覆盖全游戏,而是先把“战斗状态能不能稳定读”打穿。后面的地图、奖励、商店、事件、选牌等状态,更多依赖运行时对象和 Godot UI 节点实时构建快照,而不是每个场景都先 patch 一个事件。
工程目录怎么组织
Section titled “工程目录怎么组织”一个可维护的 STS2 mod 工程至少要分出这些边界:
src/ C# mod 源码src/Patches/ Harmony patch,接游戏生命周期src/State/Builders/ 从运行时对象和 UI 节点构建快照src/Observation/ localhost HTTP server 和 API catalogsrc/Execution/ 动作执行器与执行日志pack/ 打进 PCK 的资源与 localizationtools/ PCK 打包脚本等辅助工具tools/sts-cli/ 面向 AI/开发者的 CLI这个拆法背后的原则是:patch 只负责接入,builder 负责观察,executor 负责写操作,server 负责协议,CLI 负责外部体验。不要把“读状态、打动作、等转场、输出日志”混在一个 patch 里。
build_and_deploy.sh 是当前仓库最重要的工程化脚本。它做了三步:
- 用
dotnet build src/TakuAgentMod.csproj -c Release编译 DLL。 - 用游戏的 Godot engine headless 执行
tools/build_pck.gd,把pack/打成dist/taku_agent.pck。 - 把
mod_manifest.json、taku_agent.pck、taku_agent.dll复制到游戏的mods/taku_agent/。
默认 macOS 路径已经写进脚本,但仍然保留环境变量覆盖:
STS2_GAME_DIR=... STS2_MODS_DIR=... DOTNET_BIN=... ./build_and_deploy.sh这比手动复制可靠,因为它让 AI 和人类使用同一条安装路径。只要脚本成功,运行时目录里应该总是同时存在 manifest、PCK 和 DLL。
自动重启和 smoke check
Section titled “自动重启和 smoke check”构建安装之后,还需要让游戏重新加载 mod。restart_game.sh --wait-for-server 负责停止并重启本地游戏,然后等待观察服务恢复。dev_cycle.sh 则把完整循环合在一起:
./dev_cycle.sh --smoke它的语义是:
- build + deploy。
- restart game。
- wait for observer server。
- 执行最小 smoke:
./sts ping、./sts context、./sts actions、./sts doctor。
这里的关键不是“省几条命令”,而是给 AI 一个确定的迭代边界。一次改动能不能进入下一轮,不靠肉眼猜,而靠服务是否启动、context 是否可读、actions 是否能稳定返回。
AI 怎么自己测试和迭代
Section titled “AI 怎么自己测试和迭代”AI 自动迭代 mod 的前提,是把游戏运行时转成可验证的外部协议。当前仓库的闭环是:
代码修改 -> ./dev_cycle.sh --smoke -> ./sts context -> ./sts actions -> ./sts exec <action> ... -> ./sts wait <condition> -> ./sts delta 或 ./sts room snapshot -> 日志复盘读状态时,AI 默认从 context 开始,而不是读完整状态。context 返回 stateType、isStable、isTransitioning 和推荐查询。只有当前屏幕稳定时,actions 才暴露合法动作;转场中则刻意返回空动作面,避免 AI 对半稳定 UI 下手。
执行动作时,AI 不直接点击坐标,而是调用:
./sts exec play_card 0 jaw_worm_0./sts exec end_turn./sts exec choose_map_node 1这些命令最终落到 POST /api/v1/actions/execute。服务端会在游戏主线程执行动作,等待 post-action 快照,返回 delta、context、recovery 和 suggestedNext。失败时还会写 debug snapshot。
复盘靠两类日志:
action-execution.jsonl:server 端写动作执行记录,包含 correlation id、执行前后资源、动作面摘要、delta facts、失败原因。cli-command.jsonl:CLI 端写每条命令的参数、耗时、HTTP 请求路径、退出结果。
这个设计让 AI 能把“我打错了”拆成具体问题:是 stale index、状态还没稳定、动作 surface 没暴露、server 执行失败,还是游戏本体已经进入终局 overlay。
可复用的搭建顺序
Section titled “可复用的搭建顺序”从零做类似工程,可以按这个顺序推进:
- 先做最小 mod:manifest、PCK、DLL、
[ModInitializer]、bootstrap log。 - 加 Harmony patch,只验证 1 到 2 个生命周期点,不急着覆盖所有场景。
- 在 mod 内启动 localhost server,并保证所有读写游戏对象的逻辑回到 Godot 主线程执行。
- 建
context,先分类当前处于 menu、combat、map、reward 还是 overlay。 - 建
actions,把“当前能做什么”变成机器可读的 action surface。 - 建
actions/execute,让写接口复用actions的动作名和参数。 - 建 CLI,把 AI 的所有操作限制在
context -> actions -> exec -> wait -> delta这条路径。 - 加 dev cycle、doctor、telemetry,让每次迭代都能自动验证。
真正的重点不是一次做出很多 API,而是让每个 API 都能被 AI 稳定组合。对游戏自动化来说,稳定的状态机比大的全量 JSON 更重要。