Dev Log: 2026-09-06¶
The save was not corrupted. The game deleted that progress on purpose, in a branch that only runs when you go back and replay something you already finished.
The report¶
A player on Steam sent in a clear, specific bug report. They had finished Chapters 1 through 3, started Chapter 4, then went back to replay Chapter 1 on a higher difficulty. When they returned to the world map, Chapters 2 and 3 were gone. Not locked behind a paywall, not hidden, gone. The campaign acted as if they had never played past Chapter 1.
They also noticed something smaller in the same message: every difficulty achievement was one rank too high. They had unlocked the "Extreme" clears, but the game had handed them the "Lunatic" achievement for each one.
Two bugs, one report. They turned out to be unrelated in cause but both sat in the same area: how the game records what you have completed.
What actually happened to the save¶
The game tracks campaign completion per chapter, per difficulty. Finishing a chapter's final stage calls one function, advance_chapter(), which moves you to the next chapter and sets up a fresh, empty completion record for it.
That is correct the first time you clear a chapter. The problem is that replaying an already-cleared chapter's final stage calls the exact same function. So when this player re-cleared the Chapter 1 finale, advance_chapter() ran again, moved them to Chapter 2, and overwrote Chapter 2's real completion record with a blank one. Chapters 3 and 4 were still sitting in the save file untouched, but with Chapter 2 showing as incomplete, the progression gate in front of them slammed shut.
The fix is a guard: only create a blank record when the chapter has genuinely never been entered.
var next_key := _chapter_progress_key(current_chapter)
if not stages_completed_by_chapter.has(next_key) \
or _tier_book_is_empty(stages_completed_by_chapter[next_key]):
stages_completed_by_chapter[next_key] = _empty_tier_book()
credits_seen_difficulties = []
That stops the bleeding for everyone going forward. It does nothing for the players who already lost data.
Rebuilding the lost progress from what survived¶
The completion record was wiped, but the game stores per-stage star ratings in a completely separate structure, and that was never touched. Every stage this player had three-starred on every difficulty still had its rating on disk.
Star ratings are only ever written when you clear a stage, and the game already records them per difficulty. That makes them a faithful shadow copy of the completion record. So the save migration that runs on the next load rebuilds any wiped chapter directly from its surviving stars:
func _rebuild_tier_book_from_stars(chapter: int) -> Array:
var book := _empty_tier_book()
for star_key in stage_stars:
var parts := str(star_key).split("_")
if parts.size() != 3 or int(parts[0]) != chapter:
continue
var stage_num := int(parts[1])
var tier := int(parts[2])
if tier >= 0 and tier < book.size() and not (stage_num in book[tier]):
book[tier].append(stage_num)
return book
For this player's save, decoded and tested locally, that recovers Chapter 2's clears on Easy, Normal, and Hard exactly, and puts them back at Chapter 4 where they were. A chapter with no surviving star data at all falls back to marking it cleared on Normal, so the player is never left stranded even in the worst case. The migration only touches saves that show the wipe signature. Healthy saves and fresh saves pass straight through.
The achievement bug was a time bomb from months ago¶
The difficulty list used to be Normal, Hard, Expert, Extreme, Lunatic. A while back I added an Easy tier at the front. Everything that reads a difficulty by its position in that list needed to account for the shift.
The achievement code did not get that update. It still mapped position 1 to Hard, position 2 to Expert, and so on, all shifted by one. Clearing a chapter on Extreme unlocked the Lunatic achievement. Clearing on Normal unlocked the Hard achievement. It had been wrong since the day Easy shipped, and nobody had reported it until now because the achievement it hands you looks like a bonus, not a bug.
The fix is one line, plus the bound that goes with it:
# before
var suffix := ["", "_hard", "_expert", "_extreme", "_lunatic"]
if difficulty >= 1 and difficulty < suffix.size():
# after
var suffix := ["", "", "_hard", "_expert", "_extreme", "_lunatic"]
if difficulty >= 2 and difficulty < suffix.size():
One caveat: Steam and Google Play do not let a game revoke an achievement it already granted. The fix stops the wrong unlocks from here on and grants the correct tier on your next chapter clear, but if you were handed a difficulty achievement early, it stays on your profile. I decided that was a better outcome than adding code whose job is to take achievements away from players.
The review pass caught an incomplete fix¶
I had the change reviewed before shipping, and the review found a real gap. Preventing the save wipe was not enough on its own. In the original report's scenario, the player was still left parked on Chapter 2 after the replay, because two separate things both moved them there and only one was fixed.
The second one was the world map's "next chapter" button. It checked whether you had cleared the current chapter's finale on the difficulty you currently had selected. But chapter unlocking in this game is global: clear a chapter on any difficulty and the next one opens on all of them. The stage-to-stage unlocks already worked that way; the chapter button was the odd one out.
# before: gated on the difficulty you happen to have selected right now
GameManager.is_stage_complete_at(GameManager.difficulty, 10, GameManager.current_chapter)
# after: any difficulty counts, matching how stage unlocks already work
GameManager.is_chapter_cleared_any_tier(GameManager.current_chapter)
With both halves fixed, advance_chapter() also snaps you forward to the furthest chapter you have real progress in, so a replay can never drag your position backward again.
Miscellaneous¶
- Added 30 tests covering the replay wipe, the save migration and every branch in it, the chapter gate, and the corrected achievement mapping.
- GDScript test suite: 1180 of 1183 passing (3 pending: sprite assets unavailable in the headless test environment, not failures).
- Python solver test suite: 595 passing (17 skipped).
- Build 0.1.7 goes out to Steam, Google Play, and itch.io.
The lesson I keep relearning is that a code path which only runs during replays is still a code path that ships. This one had been quietly resetting the next chapter every time someone went back for a better score, and it only became visible when a player happened to have a later chapter worth losing. The achievement bug is the same shape from the other side: a change to one list, months ago, that broke something nobody thought to re-test because the breakage looked like a gift.