Save File Surgery: Rewinding a Finished Stellar Blade Save

Save File Surgery: Rewinding a Finished Stellar Blade Save to Before the Ending

How it started: I picked the wrong ending

The night I finished Stellar Blade, I stood at the final choice for a long time: take Adam's hand, or refuse?

I refused. Then the credits rolled, and I remembered something important — this game has no save history. One autosave slot, and the moment you finish the game it gets overwritten with the "completed" state. Want to see the other ending? Replay 30 hours.

Unacceptable. A save is just a file, and files can be edited. I sent the save to Claude Code with a one-line request:

"I want this save to go back to before the final ending."

What followed turned into a binary surgery spanning 13 patch versions.

First incision: dissecting the GVAS format

Stellar Blade runs on Unreal Engine 4. The save starts with EVAS followed by GVAS — the standard UE4 SaveGame magic. The whole 17MB file is one serialized property tree:

00000000  45 56 41 53 01 00 00 00  47 56 41 53 02 00 00 00  |EVAS....GVAS....|
00000010  0d 02 00 00 04 00 1a 00  ... 2b 2b 55 45 34 2b    |......++UE4+...|

Every property is name + type + size + value; strings are length-prefixed FStrings; containers come as Map / Array / Struct / Set. No off-the-shelf parser handled this game's nesting, so Claude wrote one from scratch — and immediately stepped into UE4 serialization's most classic trap:

BoolProperty stores its value in the property header, before the guid flag byte. When the parser hit a bool that was True, it read the value byte as a guid flag, skipped 16 extra bytes, and the entire tree derailed from there. Once fixed, the full save tree finally walked clean from start to end.

Along the way we found a treasure: a 1.5MB NewGamePlusBaseData blob — the game snapshots your entire progress at completion to seed New Game+. Every story flag exists twice. Patch one copy and miss the other, and you've patched nothing.

Midgame: story progress goes deeper than you think

Naive v1: flip Ach_RealEnding_Result.bCompleted from 1 to 0 in the achievement table. Load the game — nothing. Straight into the final boss fight.

So we dug, layer by layer, each one looking like the last:

Version What we found Result
v2-v3 Quest surgery: removed chapter 13 from "completed"; zeroed the three ending timestamps EndingTimeStamp_KillElder/KillLily/SaveLily Still boss fight
v4 LevelSeqPlayingSet: 937 "already played" cutscene records; deleted 11 including "Encounter Adam" and "Do Not Fusion" Still boss fight
v5 Camp/farewell trigger flags + mission task records (bSuccessTask) Still boss fight
v6 Key insight: the deleted quest must be rebuilt as "in progress" — the game stages the world from the active quest Quest tracker shows "Find the Elder Naytiba"!
v7 Lily's exosuit N_Exosuit had LifeState=5 (destroyed); revived it to 1 Still phase 2
v9 One-shot trigger ZTrigger_020: Disable + DoingCount=1 — fires once, then disables itself Still phase 2

Two technical details worth savoring here.

Equal-length vs variable-length patches. Changing TaskGroup_Complete to TaskGroup_Progress is same-length — overwrite in place, done. But ESBObject_DisableESBObject_Enable is one byte shorter, and GVAS containers all record their own byte sizes. One change means fixing size fields along the entire ancestor chain, six levels deep, from the outermost Map down to the ByteProperty itself. Miss one level and the game reports a corrupted save.

A game world is a big distributed state machine. The quest system says "chapter 13 in progress," achievements say "boss never fought," the cutscene system says "nothing played" — but the environment system still remembers "the mech is wrecked" and "that trigger already burned." Any subsystem out of sync and your loaded world is a Frankenstein.

The turning point: stop guessing, get a reference

By v10 the character could finally walk around the camp, and the farewell voice line even played — but entering the boss arena still launched phase 2 directly. Blind patching had hit diminishing returns.

New approach: Nexus Mods has a player-shared save from right before the final choice. Download it, write a full structural diff tool, and compare everything — every zone's state, all 1600+ achievement records, all 900+ cutscene entries — filtering out collectible noise, keeping only story-relevant differences.

The diff instantly exposed two layers nobody had touched:

Layer one: EnvStateSlotTag. Every environment object carries a "state slot tag" besides its activation state. Four arena switches were On in our save, Off in the reference — the "battle in progress" arena state. The game reads them and drops you straight into phase 2.

Layer two — the actual culprit of the whole case: value semantics vs existence semantics.

Reference save:  Nest_Elder_ChoiceBattle  → record does not exist at all
Our save:        Nest_Elder_ChoiceBattle  → record exists, bCompleted = 0

For twelve versions we had been zeroing flags. But achievement systems are implemented as "append a record when the event first happens" — the game's scripts check HasRecord(alias) and never look at the value. Every zeroed record was still shouting: "this happened!"

v13 physically deleted 15 finale records from both achievement tables (array counts, element bytes, and five levels of ancestor sizes all patched in sync). The save finally matched the reference structurally.

The ending: this time, she takes his hand

Load the save, walk into the depths of the Nest — the cutscene fires. Adam extends his hand.

The choice screen appears.

One last check confirmed the true-ending eligibility in the save: Ach_RealEnding_Result = 1, all seven prerequisites done, all three ending timestamps zero. Taking the hand this time leads to the hidden true ending where Lily survives — the branch missed in the first playthrough.

Postmortem: three things worth keeping

  1. When editing binary saves, structural validation is your lifeline. After every patch, re-walk the full property tree and confirm it terminates cleanly at the file's end. Thirteen versions, zero bricked saves — entirely thanks to this habit.

  2. Blind patching has a low ceiling; a reference sample is a cheat code. Twelve versions of "guess field → patch → boot game → verify," fifteen minutes per loop. With a reference save and a full diff, the culprit fell in two rounds. In reverse engineering, one sample of the correct state is worth a hundred experiments.

  3. Beware existence semantics. Zeroing a flag is not the same as "it never happened" — if the system tests record presence, you need deletion, not zeroing. The same trap lives in game saves, event-sourcing systems, and audit logs.

Final tally: Claude wrote a dozen throwaway Python scripts, a hand-rolled GVAS parser, and a full-save diff tool. My contribution was repeatedly loading the game and reporting "still phase 2."

Human picks the wrong ending; AI rewinds time. Fair division of labor.

Harvey

Full Stack Developer

A full-stack developer passionate about solving real-world business challenges, with expertise in data science and artificial intelligence.

Contact Me