Conversation
Starsector saves are big and they pile up. Archiving compresses a save folder into a single .7z beside the saves folder and removes the folder, which keeps the game's load list short and frees up disk space. Archived saves are listed in a dialog and can be put back at any time. Two ways in: an archive button on each save card for one-offs, and bulk dialogs that start from "keep the newest N" and let every tick be changed by hand. Restoring uses the same dialog pointed the other way. The whole thing is built around one rule: a save folder is only ever removed once there is an archive that provably holds every byte of it. The archive passes 7-Zip's integrity check, then every file in the folder is matched against the archive by name, size, and CRC32 checksum. The archive is written under a .part name and only renamed into place once it has passed, so a crash halfway cannot leave a file that looks finished. Removal goes to the recycle bin. Any failure at any step leaves both copies where they were and says what went wrong in plain words. Restoring is as careful: it unpacks into a temporary folder, checks what came out against what the archive says it holds, rechecks that the target name is still free, and only then moves it into place. It never writes over a save folder that is already there, and it keeps the archive unless asked otherwise. Also refuses to touch anything that is not a folder sitting directly inside the saves folder, and refuses outright while the game is running. Tested against a stand-in archiver that can be told to fail in one exact way — writes nothing, writes a damaged file, loses an entry, reports a wrong size or checksum, unpacks the wrong bytes — checking every time that the save folder survived. Plus a full round trip against the real 7-Zip binary, byte for byte, with an empty file, a non-ASCII filename, random bytes, and nested folders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmU5sKTjNNKJ3TZutfCBzi
Renders each of the save-archiver dialogs to a PNG for the README and for showing the UI on a pull request. The tool is a widget test that loads the repo's own Roboto and the Material icon font so text and icons draw properly, builds the app's theme, and writes the images through the golden mechanism. It is tagged local-only: golden images differ between machines, so CI leaves it alone. Making the images turned up two things. The size summary used "→", which the Roboto in assets/fonts has no glyph for, so anyone on a theme using that font saw an empty box; it now reads "412 MB saved as 38 MB". And the picker's footer row overflowed at dialog width, which the widget test added earlier caught — the count now takes the remaining space and ellipsises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmU5sKTjNNKJ3TZutfCBzi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Starsector saves are big and they pile up. A long modded campaign runs to hundreds of megabytes, and the game writes a new folder every fresh run. People end up with a dozen campaigns they aren't playing, a load list they scroll through, and tens of gigabytes gone.
This adds archiving: a save folder is compressed into a single
.7zbeside the saves folder, and the folder is removed. Archived saves are listed in a dialog and can be put back at any time.Two ways in, because the two jobs are different:
"Restore saves" is the same dialog pointed the other way. An archive whose save folder name is already taken can't be ticked, and says why:
A batch runs one job at a time, showing the step each one is on. "Stop after this one" skips the rest but never interrupts the job in flight:
Each row ends up with its own outcome, so a partial failure is legible rather than one blanket error:
The safety rule
Everything here is built around one rule: a save folder is only ever removed once there is an archive that provably holds every byte of it. "Provably", in order:
Only then does the folder go, and it goes to the recycle bin where the system allows it.
Supporting rules:
.partname and only renamed into place once verified, so a crash halfway can't leave a file that looks finished. The.partfile is deleted on every exit path.(2); an existing save folder blocks a restore outright.Restoring is as careful: unpack into a temporary folder, check what came out against what the archive says it holds, recheck the target name is still free, then move into place. A move that fails partway has its half-copied folder taken back out. The archive is kept unless you tick the box, and that box is off by default.
Any failure at any step leaves both copies exactly where they were, with a plain-English reason.
Tests
109 new tests, all green (suite: 921 → 1030).
save_archiver_test.dart(40) — a stand-in archiver that can be told to fail in one exact way: writes nothing, throws, writes a damaged file, fails its integrity check, loses one entry from the listing, reports a size one byte off, reports a wrong checksum, reports no checksum, unpacks the wrong bytes, loses a file on the way out. Each one asserts the save folder survived. Plus the guards: a folder outside the saves folder, a nested folder, the saves folder itself, an empty folder, a symlink, an archive folder inside the save.save_archiver_seven_zip_test.dart(10) — the real bundled 7-Zip, end to end. Round trip compared byte for byte with an empty file, a non-ASCII filename, 200 KB of random bytes and a nested folder; a bit-flipped archive; a truncated archive; two saves of the same name; reading the descriptor straight out of an archive. Skips cleanly if there's no binary for the platform, so it runs for real on CI.seven_zip_listing_test.dart(12) — the7z l -sltparser, including Windows-shaped output and the archive's own filename never being read as an entry.archived_save_store_test.dart(16) — the note beside an archive, its fallbacks, sorting, and an archive that isn't an archive at all.save_archive_selection_test.dart(14) — the keep-newest-N arithmetic.bulk_save_selection_dialog_test.dart(14) — that the ticks somebody is looking at are exactly what gets acted on. This one caught a real layout overflow, now fixed.save_archive_runner_dialog_test.dart(7) — one job at a time, "stop" never interrupting the job in flight, and the dialog not closing mid-run.The screenshots above come from
screenshots_test.dart, which renders each dialog through the golden mechanism with the repo's own Roboto and the Material icon font loaded. It's taggedlocal-onlyso CI leaves it alone — golden images differ between machines. To redraw them after a UI change:Making them turned up a real bug: the size summary used
→, which the Roboto inassets/fontshas no glyph for, so anyone on a theme using that font saw an empty box. It now reads "412 MB saved as 38 MB".Changes to existing code
SevenZipgainslistEntriesWithDetails(sizes and checksums;listFilesonly returns paths, which isn't enough to tell a complete archive from a truncated one) andcreateArchiveFromFolder.save_reader.dart:parseSaveDescriptorsplit out ofSaveFileNotifier.readSave, so a descriptor read out of an archive is parsed by the same code as one read off disk. Body reindents, hence the large diff on that file.Constants.savesArchiveFolderName=TriOS_Save_Archives.Notes
<archive>.7z.info.json) holds the character name, date and mod list so the list draws without unpacking. It's a convenience, never a source of truth: it falls back to readingdescriptor.xmlout of the archive, and failing that still lists the archive so it can be restored (theold_campaign_backuprow in the first screenshot).ThemeManagersetscheckColor: Colors.transparentapp-wide — that's existing behaviour, not something this PR introduces.openspec/changes/archive-saves/. Two boxes intasks.mdare unticked deliberately — hands-on checks on a real install, which I can't do from here: archive a save, confirm it's gone from the game's load list, restore it, load it; and confirm the archive folder lands correctly on all three platforms, including with a custom saves path.changelog.mduntouched, per CLAUDE.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01VmU5sKTjNNKJ3TZutfCBzi