A small, unofficial companion mod for Slay the Spire 2 that patches around bugs I hit
while playing the Steam Workshop mod
New Game ++ (NewGamePlusPlus)
on Ascension 10.
Some of them stopped a run outright. Not all of them are New Game ++'s bugs — a couple are base-game bugs that only become reachable once a run continues past act 3.
I'm publishing this mainly as a reference implementation. New Game ++ has no public repository, so if its author (or MegaCrit) wants any of this, the code is here to copy rather than describe. It is public domain for exactly that reason.
Written against New Game ++ 1.3.4 and game v0.111.0.
| Patch | Fixes | Whose bug |
|---|---|---|
CrowdingDisable |
Crash entering combat, turn 1 never starting, turn 2 never arriving | New Game ++ |
InterludeBoonResetFix |
The Landing running out of boons and stranding the run | New Game ++ |
ChapterOneStrengthFix |
Elites getting +1 Strength in chapter 1 | New Game ++ |
StaleSecondBossFix |
Black map screen entering act 2 of chapter 2 onward | Base game |
AncientRerollFix |
Ancients offering identical choices on every revisit | Base game |
Each source file carries a doc comment with the full root cause and the reasoning behind the approach. The short version follows. (Those comments are in Japanese; the causes are all written up in English in the Workshop discussion thread.)
Read this one before copying it.
New Game ++'s Crowding adds extra enemies pulled from the act's roster. A relocated monster is
written against its own encounter's slot names, so dropping it into a different encounter
breaks it, and there are as many failure modes as there are places a slot name is read. I hit
three: positioning (NCombatRoom.PositionCreaturesWithSlots dereferencing a null slot),
move selection (ConditionalBranchState.GetNextState matching no branch and throwing inside the
turn loop), and summoning (Fogmog.IllusionMove calling CreatureCmd.Add(..., "illusion") in an
encounter that has no scene). The last two don't crash — they kill the turn loop, so combat opens
and simply never proceeds.
I patched the first two individually before concluding I couldn't bound the problem that way, so
this patch no-ops CrowdingPatch.Postfix entirely. That removes a feature. It is the right
call for me and the wrong call for the mod itself.
NgppInterlude._taken is shared with the canonical model. EventRoom holds the canonical
instance and EventSynchronizer.BeginEvent does clone it per visit — but EventModel.ToMutable
is MemberwiseClone and EventModel.DeepCloneFields only rebuilds _dynamicVars, so every
clone points at the canonical's _taken list object. It accumulates for the whole process,
across chapters and across runs. There are six boons, so it empties out, and an empty Bargain
page has no options and no exit.
Prefix on NgppInterlude.GenerateInitialOptions clearing _taken and resetting _picksLeft.
Untested at runtime. The others are confirmed over ~10 chapters of play; I stopped before triggering a carry-over again. The mechanism is verified against the decompiled source, the fix engaging is not.
DifficultyVerdict.StrengthElite is floor(1.25 * D) + 1, so the constant lands +1 Strength on
elites and bosses at D=0 — chapter 1, which the mod's own description calls untouched vanilla.
StrengthNormal has no +1.
Prefix on NgppStrength.OnCombatBegan, skipping it while difficulty is 0. Patching the getter
would be more direct, but it's an expression-bodied one-liner and prone to being inlined.
NMapScreen.SetMap only assigns _secondBossPointNode when the new map has a second boss. When
it doesn't, RemoveAllMapPointsAndPaths queue-frees the node but never nulls the field, so the
next SetMap runs if (_secondBossPointNode != null) … .GetPath() against a disposed Godot
wrapper — which is not C# null. It throws inside EnterNextAct, so the map never generates and
the screen stays black until the game is restarted.
Vanilla can't reach it: act 3 is the last act, so there's no SetMap after the one that sets the
field. Needs Ascension 10, which is what gives a chapter's final act its second boss.
Prefix on NMapScreen.SetMap clearing the field when the incoming map has no second boss.
EventModel.BeginEvent rebuilds the event Rng from run seed + player slot + model id. No act
index, no visit counter, counter back to 0. Every Ancient's GenerateInitialOptions draws only
from base.Rng, so a second visit to the same Ancient in one run deals the same hand. The game
knows about this case — Rng(Player, ModelId, ulong mixin = 0) documents it and asks for a mixin
— but BeginEvent inlines the formula without one.
Vanilla can't reach it either: act Ancient pools don't overlap and the shared Ancient goes to at most one act, so no Ancient repeats in a run. New Game ++ draws again per appended chapter.
Prefix on AncientEventModel.GenerateInitialOptionsWrapper that counts earlier acts using the
same Ancient and swaps in a mixed-in seed only from the second visit on, so unmodded runs keep
identical seeds.
Requires the .NET 9 SDK — the game ships its own Microsoft.NETCore.App 9.0.7 runtime, and a
net8.0 build will not load.
./install.shBuilds and installs to <game>/mods/NgppFixes/. The script gets the SDK via
nix run nixpkgs#dotnet-sdk_9; if you have .NET 9 installed already, dotnet build -c Release
and copying NgppFixes.dll plus NgppFixes.json into that directory does the same thing.
NgppFixes.csproj references sts2.dll, GodotSharp.dll, 0Harmony.dll and
NewGamePlusPlus.dll directly out of the Steam install; adjust SteamDir / WorkshopDir if your
library lives elsewhere. Nothing from the game or from New Game ++ is redistributed here.
The install target sits inside the Steam game directory, so verifying game files or a game update
can remove it. Re-run ./install.sh if the old behaviour comes back.
Every patch that reaches into New Game ++ or game internals by name resolves those names in
TargetMethod(). If an update renames or removes one, PatchAll throws and the mod fails to
load visibly, rather than silently doing nothing. That's deliberate — a patch that quietly stops
applying is worse than one that announces itself.
Not affiliated with MegaCrit or with the author of New Game ++. The bugs were found while playing; the decompiling and most of the code were done with an AI assistant, and every claim above was checked against the decompiled source and against game logs.
The Unlicense — public domain. Copy anything here into New Game ++ or the game itself without asking, crediting, or telling me.