03. Project Skeleton, Build, And Manifest¶
Recommended Repository Shape¶
FieldNotes/
FieldNotes.csproj
ModEntry.cs
Cards/
Potions/
Relics/
Events/
Gui/
Patches/
godot/
FieldNotes/
localization/
images/
images/
atlases/
build/
mods/
FieldNotes/
The source tree separates managed models from Godot resources. The staging directory mirrors the final installation and should contain only files that will be shipped.
Project File¶
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GameDir Condition="'$(GameDir)' == '' and Exists('C:\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2\data_sts2_windows_x86_64\sts2.dll')">C:\Program Files (x86)\Steam\steamapps\common\Slay the Spire 2</GameDir>
<Sts2DataDir Condition="'$(Sts2DataDir)' == '' and '$(GameDir)' != ''">$(GameDir)\data_sts2_windows_x86_64</Sts2DataDir>
</PropertyGroup>
<Target Name="ValidateGameReferences" BeforeTargets="ResolveReferences">
<Error Condition="'$(Sts2DataDir)' == '' or !Exists('$(Sts2DataDir)\sts2.dll')"
Text="Set GameDir or Sts2DataDir to the current Slay the Spire 2 installation." />
</Target>
<ItemGroup>
<Reference Include="sts2">
<HintPath>$(Sts2DataDir)\sts2.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(Sts2DataDir)\0Harmony.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="GodotSharp">
<HintPath>$(Sts2DataDir)\GodotSharp.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
Private=false prevents copies of game-owned assemblies from being staged beside the mod DLL. The running game already supplies them.
One External Manifest¶
v0.107.1 uses one manifest file discovered below <STS2>/mods. Name it <ModId>.json and place it beside the declared payloads:
{
"id": "FieldNotes",
"name": "Field Notes",
"author": "Example Author",
"description": "Adds an example card, potion, relic, event, and overlay.",
"version": "0.1.0",
"min_game_version": "0.107.1",
"has_pck": true,
"has_dll": true,
"dependencies": [],
"affects_gameplay": true
}
Current fields:
| Field | Meaning |
|---|---|
id |
Required loader identity and payload basename |
name |
Display name |
author |
Display metadata and fallback Harmony ID prefix |
description |
Display metadata |
version |
Mod version shown to the loader and metrics |
has_pck |
Require and load <id>.pck |
has_dll |
Require and load <id>.dll |
dependencies |
Objects with a dependency id and optional min_version |
min_game_version |
Minimum supported game semantic version |
affects_gameplay |
Whether the mod is included in gameplay-relevant mod reporting |
There is no pck_name field in the current ModManifest, and an embedded mod_manifest.json is not the active loader contract.
Identity Contract¶
For a manifest with "id": "FieldNotes", the loader resolves:
The external manifest filename does not technically define identity, but matching it to the ID removes ambiguity and avoids accidental parsing of unrelated JSON files. Because every .json below mods/ is treated as a possible manifest, do not store settings or arbitrary JSON beside installed mods unless they are outside the scanned tree or use a different extension.
Initializer And Registration¶
Use an explicit initializer when the mod registers content, subscribes hooks, or needs startup logging:
using FieldNotes.Cards;
using FieldNotes.Potions;
using FieldNotes.Relics;
using HarmonyLib;
using MegaCrit.Sts2.Core.Modding;
using MegaCrit.Sts2.Core.Models.CardPools;
using MegaCrit.Sts2.Core.Models.PotionPools;
using MegaCrit.Sts2.Core.Models.RelicPools;
[ModInitializer(nameof(Initialize))]
public static class ModEntry
{
public static void Initialize()
{
ModHelper.AddModelToPool<ColorlessCardPool, FieldNotesCard>();
ModHelper.AddModelToPool<SharedPotionPool, BottledInsightPotion>();
ModHelper.AddModelToPool<SharedRelicPool, SurveyorLensRelic>();
new Harmony("example.fieldnotes").PatchAll(typeof(ModEntry).Assembly);
}
}
Pool registration must happen before the game freezes pool contents. ModHelper throws when a model is added after initialization.
The three model classes are defined in chapters 10-12. Add those classes before building this combined initializer; the bootstrap in chapter 5 can be used independently for a behavior-only mod.
Build And Stage¶
The expected output is:
A code-only mod sets has_pck to false and omits the PCK. A data-only pack sets has_dll to false, although most custom model examples require a DLL.
Build scripts should fail when a declared payload is absent. Silent partial staging produces manifests that the loader can detect but cannot satisfy.
Installation¶
Copy the whole staged directory to:
Root-level manifests and payloads still work because discovery is recursive from mods/, but per-mod directories isolate filenames, simplify removal, and match the loader's directory ownership model.
Versioned Dependencies¶
Use dependency objects for new releases:
This fragment illustrates the dependency field; the standalone FieldNotes example needs no library and retains dependencies: []. Use the dependency's manifest ID, not its display name or DLL filename.
min_version may be omitted when no minimum is required. If specified, the dependency must declare a parseable semantic version at least that high. Missing, invalid, or lower dependency versions produce distinct loader errors. Keep the mod's own version parseable even when it has no dependencies.
min_game_version checks the installed game version. An absent value currently logs a warning and assumes compatibility; an invalid value or a requirement above the installed version prevents loading. This minimum is not proof of compatibility with every later release.
The older array of strings is temporarily migrated by ModManifest.ReadFromStream, but produces a migration diagnostic and is scheduled for removal. Publish object entries instead.
See release validation and version errors.