Repository navigation
Save data: export and import one game's save as a file - #642
oliverames wants to merge 7 commits into
Conversation
|
The ruff/pyright job fails on a finding this PR does not touch: |
22a8ce6 to
13ca12a
Compare
|
The two red jobs noted above are fixed on |
13ca12a to
4dc8173
Compare
A save could only leave the device by copying Documents/save in the Files app, and a reinstall that lands in a new data folder lost it unless the player had copied it first. dm_save_export packs the committed saves ($DM_SAVE and, when the game has one, <$DM_SAVE>_device) into one file; dm_save_import validates one whole and swaps it in, keeping the replaced tree as <save>.pre-import. The format is a 32-byte header and one entry per file or folder (kind, guest name, size, bytes, CRC-32), sorted and reproducible byte for byte. Names are the guest's, so a case-collision alias (.dm-case-N with hle_fs.c's xattr) survives a trip through a case-sensitive volume. An import writes nothing until the whole archive has been checked: a short file, a bad checksum, a '..' entry, an unknown version or another game's archive is refused and leaves the save as it was. Both directions refuse while a save is mounted, because the mount holds the working directory the transaction swaps. t_save_archive (62 checks) packs a tree with nested and empty folders, a zero-byte file, a non-ASCII name and a case alias; exports it twice to the same bytes; restores it and exports again to the same bytes; and checks the replacement and its backup, the cleared .work/.new, a damaged checksum, a cut-short file, trailing bytes, an unknown version, '..', unsorted and duplicate entries, a missing parent folder, another game's title, device save data and an empty save.
The iPhone settings screen and the Mac settings window gain Export Save Data… and Import Save Data…. The iPhone writes '<game> <date>.dmsave' to Documents/Save Data, visible in the Files app, and opens the share sheet so the file can go to Files, AirDrop or another device; the Mac uses NSSavePanel and NSOpenPanel. Both call the host's archive, so a save moves between the two apps without the Files app's folder copy, and the work runs off the main thread on the iPhone, where a save can be hundreds of megabytes. The archive's type (org.nxnative.save-archive, extension dmsave) is declared in each app's Info.plist, so the file pickers offer it, and Swift reaches the C through dm/ios/Sources/DmIOS-Bridging-Header.h (iOS, SWIFT_OBJC_BRIDGING_HEADER) and swiftc's -import-objc-header (Mac). The launcher must be the place that imports, because the game's mount owns the working directory once it runs. build_ipa.sh exports the game's base title ID as DMTitleID (pack_app.py already writes it on the Mac), which the archive records so another game's file is refused. docs/playing.md has the player's side: where the export goes, what import keeps, and that the file is nxnative's own format, not a Switch or other emulator save. The Mac pack reuse key now covers save_archive.c/.h and SaveData.swift, and the launcher tests compile SaveData.swift with the C file.
The comment read as if this definition were the weak one; the Mac launcher's is (save_archive.c), and this one is the strong answer the host links.
The smoke build sets every project.yml placeholder it needs; DMTitleID is new (SaveData.swift), and without DM_TITLE_ID xcodegen leaves the app's title ID empty. No finding changes.
The file importer hands back a security-scoped URL that may point at a temporary copy; the import runs in a detached task so a large save does not freeze the launcher, and the scope must stay open until that task is done. SaveData.importArchive still balances its own start/stop inside the task.
…lang-tidy unix.Stream)
4dc8173 to
8e55c49
Compare
What and why
A save can only leave the phone by copying
Documents/savein the Files app, and a reinstall that lands in a new data folder loses it unless the player copied it first. Every other emulator and Switch save tool lets a player carry one file; nxnative had no way to.The launchers now have a Save data section: Export Save Data… writes the game's last committed save to one
.dmsavefile, and Import Save Data… puts one back. On the iPhone the file goes toDocuments/Save Data(the Files app, under the game's name) and opens the share sheet, so it can go to Files, AirDrop or another device; on the Mac both use the normal save and open panels. Both calldm/host/save_archive.c, so a save moves between a Mac app and an iPhone app of the same game.dm_save_exportpacks$DM_SAVEand, when the game has one,<$DM_SAVE>_device. It packs the committed tree only:Commitis what the game asks to persist, and the working directory beside it is what the next mount discards.dm_save_importvalidates the whole archive into<save>.importfirst, then renames it in, keeping the replaced tree as<save>.pre-importand clearing a leftover.work/.new. A failure at any step puts every moved tree back; nothing is written outside the save.save_archive.c): a 32-byte header with a magic, version, the game's base title ID and the entry count, then entries of kind, guest name, size, bytes and CRC-32, sorted and unique. Not a zip, so no compression library or deflate implementation is needed, and the same save always produces the same bytes..dm-case-Nalias carrieshle_fs.c'sorg.d2rmobile.guest-namexattr into the archive, and import rebuilds the alias the same way, so a save written on a case-sensitive volume reads back on APFS.dm_fs_save_in_use, a new strong function inhle_fs.cwith a weak default for the Mac launcher, which links the archive without the host). The launcher is the place that imports, because the game's mount owns the working directory once it runs.build_ipa.shnow exportsDM_TITLE_IDinto the iPhone app'sDMTitleID(pack_app.pyalready wrote it on the Mac)..dmsavetype is declared in both Info.plists so the pickers offer it, and Swift reaches the C through a bridging header on iOS and-import-objc-headeron the Mac.This is 1,048 lines including the 62-check test and both front ends, just over CONTRIBUTING's ~1,000-line line. The commits are separable (host archive; launchers and wiring) if you would rather take it in that order.
The branch is rebased on current
main, so it no longer has a merge commit, and the overlap with #617 (cloud saves, now onmain) is resolved here: both save sources are linked into the Mac launcher (launcher.o,save_sync.o,save_archive.o),hle_fs.ckeepsdm_fs_clone_committedbesidedm_fs_save_in_use, and the bridging header importsCloudSaves.handsave_archive.h. The full host test suite passes on that base, includingt_save_archive,t_fs_caseandt_save_sync, and so dotest_nsp_mac_appandtest_launcher_storage(45 tests).Checked
make -C dm/host GAME=010015100B514000builds, andmake -C dm/host/tests GAME=010015100B514000 runpasses in full, including the newt_save_archive(62 checks). It packs a tree with nested and empty folders, a zero-byte file, a non-ASCII name and a.dm-case-1alias; exports twice to identical bytes; restores it and exports again to identical bytes; then checks the replacement and its.pre-importbackup, cleared.work/.new, a damaged checksum, a cut-short file, trailing bytes, an unknown version,.., a trailing-slash root, an entry before its folder, unsorted and duplicate entries, another game's title, an unknown title, device save data, an empty save and a mounted save. The same test also passes under ASan and UBSan.GAME=010015100B514000 python3 dm/ios/tools/compile_check.py: 181 files, 0 failed.dm/ios/tests: all six test modules pass, includingtest_launcher_storage.py, which now compilesSaveData.swiftandsave_archive.cwith the real model and window.python3 -m unittest dm.tools.tests.test_nsp_mac_app: 44 tests pass, withpack_app.py's reuse key covering the archive sources andSaveData.swift.dm.tools.tests.test_docs,test_env_switches(no new switch) andtest_player_envpass.dm_fs_save_in_use, for the launchers), so no game can behave differently and noscripts/regress.py check --gamesrun was made.DM_SAVEbefore the Mac window now, asmain.malready did on iOS.Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)This is a feature, not a finding: the player's side is a new Save data section in
docs/playing.md, and the format is described at the top ofdm/host/save_archive.c.