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

6.3 KiB

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)

{
  "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.

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.