sts2-bot/docs/research/01-game-engine-and-mod-surface.md
0xrsydn fb32822468 Add design doc and research notes
DESIGN.md covers the three-layer architecture (facts in code, Jev for
tactics, gated escalation for macro). research/ documents the engine and
mod surface, the Jev classifier's measured behavior, the STS2MCP HTTP
interface, state shapes, failure modes, decision architecture, and a
run log of the first four sessions.
2026-09-22 00:01:22 +07:00

174 lines
6.3 KiB
Markdown

# 01 — Game engine and mod surface
Measured on game build `v0.107.1` (commit `59260271`), macOS arm64, Steam.
## Engine
Slay the Spire 2 is a **Godot 4.5.1 (.NET)** game, not Java like the original.
This matters: the gameplay code is plain managed C# IL, so it can be read
directly with no decompiler.
| Item | Value |
|---|---|
| Engine | Godot 4.5.1 (.NET build) |
| Runtime | .NET 9.0.7, self-contained `osx-arm64` |
| Root namespace | `MegaCrit.Sts2.*` (2,751 types) |
| Bundle id | `com.megacrit.SlayTheSpire2` |
## Layout inside the app bundle
```
SlayTheSpire2.app/Contents/
MacOS/
Slay the Spire 2 179 MB Mach-O universal (x86_64 + arm64)
mods/ <- mods load from here
Frameworks/
libfmod.dylib, libfmodstudio.dylib
libGodotFmod.macos.template_release.framework
libspine_godot.macos.template_release.framework
libSentry.dylib
Resources/
Slay the Spire 2.pck 1.9 GB Godot pack
data_sts2_macos_arm64/ .NET assemblies + runtime
sts2.dll 9.3 MB main game assembly
sts2.xml 5.3 MB XML docs, 19,635 members
GodotSharp.dll, 0Harmony.dll, MonoMod.Backports.dll,
Steamworks.NET.dll, Sentry.dll, ...
data_sts2_macos_x86_64/ same, other arch
```
### The free win: `sts2.xml`
`sts2.xml` is the .NET XML documentation file, shipped un-stripped. It gives
**19,635 documented members** with prose summaries. Example, verbatim:
> `ModInitializerAttribute` — *"Declares a class as the main entry point for
> the mod. If this is present, then upon loading the mod, we'll call the method
> named `initializerMethod` within the class. Otherwise, we'll create a harmony
> instance for the mod and call `Harmony.PatchAll`."*
This removes most of the guesswork from modding. Query it with
`~/sts2-re/docdump.py` or any XML parser.
### Harmony and MonoMod ship in the game
`0Harmony.dll` and `MonoMod.*.dll` are present in the shipping build. That is
a strong signal that runtime patching is a supported path, not an accident.
## Official mod loader
The game has first-class mod support. Namespace `MegaCrit.Sts2.Core.Modding`.
| Type | Purpose |
|---|---|
| `ModManager` | Discovers and loads mods |
| `ModInitializerAttribute` | Declares a mod entry point |
| `ModManifest` | The JSON manifest |
| `ModHelper` | Content registration and hook subscription |
| `ModSource` | `ModsDirectory` or `SteamWorkshop` |
| `SettingsSaveMod` | Per-mod enable/disable and manual load order |
`ModManager.Initialize` is documented as loading mods *"from the 'mods'
directory next to the executable, as well as Steam workshop files"* and is
*"called as early as possible in the game's initialization process"*.
**Consequence:** mods load at process start. Changing a mod requires a full
restart. "Save and quit" to the main menu is not enough.
### Mod directories
| Source | Path |
|---|---|
| Local (macOS) | `SlayTheSpire2.app/Contents/MacOS/mods/` |
| Steam Workshop | `Steam/steamapps/workshop/content/2868840/` |
### Manifest schema (verified from a working mod)
```json
{
"id": "STS2_MCP",
"name": "STS2 MCP",
"author": "kunology",
"description": "MCP server bridge for Slay the Spire 2",
"version": "0.4.0",
"has_pck": false,
"has_dll": true,
"affects_gameplay": false
}
```
Note **snake_case** in JSON (`affects_gameplay`, `has_dll`, `has_pck`), which
differs from the C# property names (`affectsGameplay`). The manifest filename
must match the mod id: `STS2_MCP.json` for id `STS2_MCP`.
### Mod loading, from the live log
```
Found mod manifest file .../mods/STS2_MCP.json
Mods have been re-sorted because we detected a change or dependency order was broken.
[WARN] Mod STS2_MCP does not declare min game version. Assuming that it is supported.
Loading assembly DLL .../mods/STS2_MCP.dll
Calling initializer method of type STS2_MCP.McpMod for STS2_MCP, Version=1.0.0.0
Finished mod initialization for 'STS2 MCP' (STS2_MCP).
--- RUNNING MODDED! --- Loaded 1 mods (1 total)
[Sentry.NET] Is running modded
```
`minGameVersion` is optional but its absence produces a warning. It is worth
declaring in any mod we write.
### Consent flow
`NConfirmModLoadingPopup` is documented as *"Vertical popup used to get player
confirmation before loading mods. Renders above the capstone screens (above top
bar)."* It appears **over the main menu, at launch**, and only when mods are
present. Consent persists as `mod_settings: {"mods_enabled": true}` in
`settings.save`.
There is also a `nomods` command-line argument that makes `ModManager` skip
initialization entirely (`ModManagerState.Skipped`).
## Save layout
Modded and vanilla progress are **isolated**:
```
~/Library/Application Support/SlayTheSpire2/steam/<steamid>/
settings.save {"mod_settings": {"mods_enabled": true}, ...}
profile.save
profile1/saves/ vanilla progress
modded/profile1/saves/ MODDED progress
prefs.save
progress.save unlocks, card/enemy/encounter stats
current_run.save the run in progress
history/<ts>.run completed run records
```
Modded runs never touch vanilla saves. This was confirmed by the log line
`Profile-scoped data path initialized: user://steam/<id>/modded/profile1`.
## Bundle signing
The app is **ad-hoc signed** (`Signature=adhoc`, `flags=0x10002(adhoc,runtime)`)
and `spctl -a` already returns `rejected`. There is no quarantine attribute
(only `com.apple.provenance`). Adding files to `Contents/MacOS/` breaks the
sealed-resource count but there is no enforced signature, and Steam launches
the binary directly.
**Do not run Steam's "Verify integrity of game files"** — it deletes the mods
directory, because those files are not in the depot manifest.
## Useful namespaces for a bot
| Namespace | Why it matters |
|---|---|
| `Core.Modding` | Official mod API |
| `Core.AutoSlay` | MegaCrit's own auto-play bot, shipped in the build |
| `Core.DevConsole.ConsoleCommands` | ~40 built-in debug commands |
| `Core.GameActions` | Command pattern for game actions |
| `Core.Runs` / `Core.Combat` | Run and combat state |
| `Core.Saves` | Source-generated JSON serialization |
`AutoSlayer.Start(seed, ...)` runs a complete automated game with handlers for
every screen and room. It is MegaCrit's smoke-test harness and is the best
available reference for driving the game programmatically.