Progress reset — design
Progress reset — design
Section titled “Progress reset — design”Brainstormed with Chris 2026-10-04. Status: spec, awaiting review. Next step: implementation plan (writing-plans).
The App Store description will say the game is in active development and that some updates may need to reset progress (wording below; “beta” is avoided because of App Review Guideline 2.2). So we need a reset that is real, announced and recoverable, instead of a silent one.
What happens today [verified in code]: MetaState.SCHEMA_VERSION / MIN_COMPAT_SCHEMA
(sim/meta_state.gd:14-18) were built as the reset lever. Raise them and from_dict() returns a
fresh state, and MetaStore.last_load_compatible goes false. Boot-time writes respect that flag, but
MetaStore.save_state() (meta/meta_store.gd:220) does not check it, and main.gd calls it at run end
and on shop purchases (main.gd:631/647/772/1685/2698). So the first run after the update silently
overwrites the old save, with no notice and no backup. The same happens to a corrupt save and to a
save from a newer schema.
Decisions (Chris, 2026-10-04)
Section titled “Decisions (Chris, 2026-10-04)”- Each release picks a reset kind: required (this build can’t read the old save) or recommended (it can; the player chooses).
- Wipe scope = progress only. Cleared:
banked_gold,gold_spent,levels,selected_ship,drone_loadout,seen_enemies. Kept:tutorial_done,seen_survival_intro,shop_tip_views,install_id,share_diagnostics. Returning players aren’t sent back through the tutorial, and their Diagnostics choice and anonymous id survive. - Manual reset in the pause menu, available at any time.
- Reset offers are bundled in the build, never delivered by remote config. A wipe only ever arrives with a reviewed store build.
Design
Section titled “Design”1. Reset kinds
Section titled “1. Reset kinds”Required. Raise SCHEMA_VERSION and MIN_COMPAT_SCHEMA together (the existing lever). A save
with schema < MIN_COMPAT_SCHEMA gets the required notice at boot (§4). On OK: back up, wipe (§2),
save. Nothing is written to disk before the OK.
Recommended. meta/save_reset.gd (new, class_name SaveReset) holds:
const RESET_EPOCH := 0: the build’s epoch, bumped by a release that recommends a reset.const RESET_NOTICE := "": why this release recommends it, shown in the offer.
MetaState gains reset_epoch: int. The two defaults are deliberately different:
MetaState.new()defaults toSaveReset.RESET_EPOCH, so a fresh install is never offered a reset.from_dict()defaults a missing key to0, so any save from before this feature counts as epoch 0.
At boot, if meta.reset_epoch < SaveReset.RESET_EPOCH, the recommended offer appears with two
choices, Reset now and Keep my progress. Either choice sets reset_epoch = RESET_EPOCH and
saves, so the offer is asked once only. Reset now also runs the wipe (§2).
Other unreadable saves (handled by the same notice, different copy):
- Corrupt / unparseable JSON: “Your save couldn’t be read, so progress has been reset.” The behaviour matches required (back up, then wipe).
- Newer schema than this build (a downgrade; can’t happen through store updates, but can between dev and test installs): “This save is from a newer version of Dark Cosmos. Update the game to keep it. Progress this session won’t be saved.” No wipe; writes stay blocked (§3).
2. MetaStore.wipe_progress(old: Dictionary) -> MetaState
Section titled “2. MetaStore.wipe_progress(old: Dictionary) -> MetaState”One function used by required, recommended and manual resets.
- Backup: copy the current save file byte-for-byte to
meta.backup-<BUILD>.jsonnext to it (sameuser://→ cache-dir fallback assave_state; tvOS uses the cache dir). An existing backup with the same name is overwritten, which is fine: one backup per build. - Build a fresh
MetaState, carry over the kept fields (decision 2) fromold, read leniently from the raw dictionary (the required case can’t usefrom_dict()), and setreset_epoch = SaveReset.RESET_EPOCH. - Save it, and set
last_load_compatible = true. EventQueue.push("save_reset", {"kind": "required"|"recommended"|"manual"|"corrupt", "from_schema": n, "from_epoch": n}). This is gameplay telemetry, not a new kind of personal data. The planner checks whether the privacy page or ASC label enumerates event types, and updates them if so.
main.gd swaps its meta for the returned state.
3. Structural guard: no silent overwrite
Section titled “3. Structural guard: no silent overwrite”save_state() refuses (a no-op that still counts save_count, and sets
last_save_error = "blocked: unreadable save not acknowledged") while last_load_compatible is
false. The only ways to clear the flag are wipe_progress() or a clean load. This turns the
current behaviour (every caller has to remember) into an impossible one, and closes the
silent-overwrite path for corrupt and newer-schema saves too. The existing boot-time
if last_load_compatible: checks become redundant but harmless; leave them.
4. The notice UI
Section titled “4. The notice UI”One new panel, ui/save_reset_notice.gd, in the existing neon-panel style using live tokens, with
MenuNav focus for controller/remote. Modes:
- required / corrupt: title, body text, one button OK.
- recommended: title,
RESET_NOTICEbody, buttons Keep my progress (default focus, the safe choice) and Reset now. Reset now asks a second time (“Reset all progress? This can’t be undone.”) before wiping. - newer-schema: title, body, OK (no wipe).
main._ready decides after MetaStore.load_state() and shows the notice before the start menu is
interactive. On close, the start menu proceeds as normal. Precedence when several apply:
required/corrupt > newer-schema > recommended, with at most one notice per boot. A required reset
also sets reset_epoch, so a recommended offer in the same build doesn’t follow it.
5. Manual reset (pause menu)
Section titled “5. Manual reset (pause menu)”A Reset progress row in ui/pause_menu.gd, next to Diagnostics. Press once and the row changes
to “Press again to reset all progress”; the confirm state times out after a few seconds or on focus
loss. The confirm press emits progress_reset_requested. main calls wipe_progress(meta.to_dict())
with kind manual, abandons the run (no gold banked, no results screen) and returns to the start
menu. Hidden during the tutorial/story lesson track, where the pause menu already trims rows (the
planner confirms the existing gate).
Debug Settings ↔ console parity: this is a player setting, not a dev row, so the console is not
required. (Planning, 2026-10-04: there’s no dev command. Screenshots come from tools/ui_sweep
screens that construct the panel directly and never touch the save, which avoids hand-written
console JS plus a Worker redeploy.)
5b. Undo a reset (Chris, 2026-10-04: “a way that allows a player to restore if they decide after they wished they hadnt wiped”)
Section titled “5b. Undo a reset (Chris, 2026-10-04: “a way that allows a player to restore if they decide after they wished they hadnt wiped”)”This replaces §5’s two-press row. The pause row opens a small progress dialog
(SaveResetNotice in mode manage): Reset progress, Restore previous progress (only when
an undo slot exists) and Cancel (default focus). Reset and Restore each ask a second time inside
the dialog. The row reads “Reset / restore” when an undo slot exists, otherwise “Reset progress”.
- One undo slot,
meta.undo.jsonbeside the save. A manual or recommended wipe copies the current save there before wiping, replacing any earlier slot. A required or corrupt wipe deletes the slot instead: this build can’t read a too-old save, so it can’t be offered back. (The per-buildmeta.backup-<BUILD>.jsonsupport copy is still written for every wipe.) - Restore (second press: “Restore your previous progress? Anything earned since the reset will
be replaced.”): loads the slot (it must be schema-compatible, or the option is hidden), keeps the
current
share_diagnostics/install_id(settings, not progress), setsreset_epoch = max(slot's, RESET_EPOCH)so a recommended offer doesn’t come back, saves it, deletes the slot (one step; a restore isn’t itself undoable), sendssave_resetwithkind: "restore", and returns to the start menu like a manual reset. - The slot doesn’t expire; it lasts until the next reset or a restore.
- The recommended offer’s confirm text adds: “You can undo this from the pause menu.”
6. Release procedure (documented in docs/architecture/meta-and-story.md)
Section titled “6. Release procedure (documented in docs/architecture/meta-and-story.md)”- Recommended reset: bump
SaveReset.RESET_EPOCH, writeRESET_NOTICE, mention it in What’s New. - Required reset: bump both schema consts, and mention it in What’s New.
- Never both in one release unless deliberate (required already covers it).
App Store copy (goes into marketing/appstore/copy.md with this work; listed in 1.0.2)
Section titled “App Store copy (goes into marketing/appstore/copy.md with this work; listed in 1.0.2)”Dark Cosmos is in active development. Expect big updates as we build it out: new weapons, enemies and systems, plus balance changes. While the game is still taking shape, some updates may need to reset your progress. We’ll always tell you in the update notes first.
Testing
Section titled “Testing”GUT (all with disk_io_enabled off, plus temp paths via load_from-style injection, never the real save):
wipe_progressclears exactly the cleared fields and keeps exactly the kept ones, from both a compatible dict and a raw too-old-schema dict.- A backup file is written and byte-identical to the pre-wipe save.
save_stateis blocked whilelast_load_compatibleis false, and unblocked after a wipe.- Epoch: a fresh
MetaStateis never offered; a save missingreset_epochis offered whenRESET_EPOCH > 0; after either choice it’s not offered again; a round-trip throughto_dict/from_dictpreservesreset_epoch. - Notice precedence picks one mode per boot.
save_resetevent pushed with the right kind (injectable push spy;EventQueue.pushis a no-op headless).- Pause-menu confirm: one press doesn’t emit, two presses emit, and the confirm state times out.
Visual: bh-visual-qa on the notice (required + recommended) and the progress dialog (with an undo
slot), via new tools/ui_sweep screens, at iPhone, iPad and ATV sizes. Sim untouched, so no determinism change.
Out of scope
Section titled “Out of scope”- A general in-game “What’s New” screen.
- Restoring the per-build
meta.backup-<BUILD>.jsonsupport copies in the UI (the player-facing undo is the singlemeta.undo.jsonslot, §5b). - Remote-config-triggered resets (decision 4).