Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sts2-ngpp-fixes

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.


What it patches

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.)

CrowdingDisable — disables a feature, does not fix it

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.

InterludeBoonResetFix

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.

ChapterOneStrengthFix

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.

StaleSecondBossFix — base game

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.

AncientRerollFix — base game

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.


Building

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.sh

Builds 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.

Failure mode

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.

Notes

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.

License

The Unlicense — public domain. Copy anything here into New Game ++ or the game itself without asking, crediting, or telling me.

About

Patches around bugs in the Slay the Spire 2 mod New Game ++, including base-game bugs it exposes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages