# 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// 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/.run completed run records ``` Modded runs never touch vanilla saves. This was confirmed by the log line `Profile-scoped data path initialized: user://steam//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.