Stop a damaged save from being destroyed, and say so on screen - #169
Conversation
Save and load failures were reported with print(), which no front-end renders: pygame and both browser front-ends never show stdout at all, and the console clears it on the very next screen. Both now go through showDialogue, which all four front-ends implement. A failed load also fell back to a fresh game that save() then wrote straight over the file that failed - and save() opened each path with mode "w", truncating it before a byte was written, which is how a slot becomes unreadable in the first place. Saves are now written to a temporary file and swapped in with os.replace, and a slot that would not load is copied into a dated subdirectory of itself before the fallback game overwrites anything. Closes #142 Closes #143 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-reviewThe full diff was read against this repo's own conventions (front-end parity through Acted on
Considered and deliberately not changed
Front-end parityNo per-front-end code was needed and none is missing. Every new message goes through Schema contractNo field was added, renamed or retyped on Tests and docs781 tests pass. This review comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). |
Summary
Two failure modes around save files were addressed, both of which cost a player their whole run without a word on screen.
Save and load errors are now shown through the front-end contract (#142). They were reported with
print(), which reaches nobody: stdout is not rendered at all by the pygame front-end or by either browser front-end, and on the console it is wiped by thelotsOfSpace()at the top of the next screen.showDialogueis used instead — an abstract primitive onBaseUserInterface, so all four front-ends were served by the one change rather than one of them.A save that will not load is no longer overwritten by the fresh game that replaces it (#143). Two independent changes:
os.replace(atomic on POSIX and Windows) once complete. Previously each path was opened with mode"w", which truncates it before a single byte is written — so a crash, a full disk or a kill mid-dump left a partial file and no intact copy anywhere, which is how a slot became unreadable to begin with.damaged-<date>-<time>subdirectory of itself before the fallback game is allowed to write. The whole slot is copied rather than only the file that failed, because restoring a run needsplayer.json,stats.jsonandtimeService.jsontogether. Under the Pyodide front-end the copy is flushed to IndexedDB, for the same reasondelete_save_slotflushes.Supporting details:
jsonschema'sValidationErrorrenders as a multi-paragraph dump of the whole instance and schema; its.messageis the one-line reason, and no dialogue box could show the former.README.md's "Multiple Save Files" section was extended to describe the atomic write and what adamaged-...folder in a slot is.One residual risk is recorded rather than hidden:
os.replaceover an existing file was not exercised under Pyodide in this environment. Emscripten'sFS.renameunlinks an existing destination, so it is expected to behave, but it was verified by reading rather than by running.Closes #142
Closes #143
Test plan
python3 -m compileall -q src testspython3 -m pytest --cov=src --cov-report=term-missing --cov-report=xml:cov.xml— 781 passed,src/fishE.pyat 99% (the only uncovered lines being the pre-existingif __name__ == "__main__"block)blackandautoflakerun over the changed files onlyBaseUserInterface.showDialogue, whichUserInterface(console),PygameUserInterface,WebUserInterfaceandPyodideUserInterface(a subclass ofWebUserInterface) all implement. The rendered text was printed and checked for length and line breaks; pygame's_wrapText/_splitLongWordhandles the long backup path..tmpfile surviving a save, a failed save leaving the previous one intact, the load-failure dialogue reaching the front-end and not stdout, several failures folded into one dialogue, a schema failure described in one short line, an over-long reason being trimmed, the whole slot being copied aside before the fallback game overwrites it, a clean slot not being copied aside, and a copy that itself fails saying so.tests/test_saveFileManager.pygained a regression test that the new nesteddamaged-...directory stays invisible to the save menu and does not disturbget_next_available_slot().This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).