fix(build): a game built from a romfs folder gets its damaged textures replaced, as one from a .nup does #546
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!546
Loading…
Reference in a new issue
No description provided.
Delete branch "jarynclouatre/Longshot:fix/romfs-folder-stand-ins"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: medium · Tested on: Linux x86-64 (game-free checks only) · Needs: any OS, an incomplete dump extracted to folders
Why
An incomplete dump (about 23.3 GB instead of 23.8) has texture files whose start is zeros, and the game then leaves out every model that uses one: most of the Water Temple's walls and floors, the Great Fairy Fountain's floor (#1002).
stand_in_damaged_filesinnx2/make_game.pyreplaces them with whole files of the same kind and says so, but only in a romfs the builder unpacked from a .nup (its.longshot_romfsstamp) and on--updateof such a folder. A game built from folders (--romfswith--exefs, or--gamewith a folder) has the damaged files linked in as they are, and nothing is said.The folder way was left out so the player's own folder is never written to (
bd527024: "--romfs folders are not touched"). The game folder's romfs is a set of hard links to that folder, andnup.stand_in_damagedwrites each stand-in to<file>.partand then replaces the name with it, so the stand-in can go in the game folder without writing the player's file.How to reproduce
Seen on: found reading the code; not seen in the game
Settings and switches:
--romfswith--exefs, or--gamewith a folder; an incomplete dump of about 23.3 GB instead of 23.8.Steps: Build from the incomplete dump's folders, check the build's messages, then look at the Water Temple's walls and floors or the Great Fairy Fountain's floor.
Expected: Damaged files are reported and stand-ins are used where available without writing the player's folder.
Actual: The damaged files are linked in as they are and nothing is said; the game leaves out every model that uses one.
What changed
Builds from folders now check for damaged texture files and use stand-ins in the game folder without writing the player's files. Refused replacements are listed and tried again on the next build; a romfs that is the player's folder itself, or holds it, is only reported.
nx2/make_game.py,nx2/nup.py): the folder way callsstand_in_damaged_files(out, romfs, a.romfs)after linking. For a folder its stamp is the folder's path and file count, kept in.longshot_romfs_checkedas for an unpack, so the roughly 100,000 files are opened once per folder and not at every build. A stand-in replaces the game folder's name of the damaged file; the player's file and its other names keep their bytes. A file thatlink_or_copyputs in afresh (one deleted from the game folder, a stand-in among them) clears that stamp, so it is looked at again; a rescan of a folder build that finds nothing keeps the list of what was replaced.nup.stand_in_damagedremoves an existing<file>.partbefore copying, so a.partlinked in from the player's folder is not written through. A replacement Windows refuses (a read-only file: through a hard link the attribute is the player's file's own, so it is not cleared; or a file another program has open, such as a virus scanner right after the linking) leaves that file as linked, removes its.part, and lists it as "not replaced, Windows refused it (read-only in your folder, or open in another program); the next build tries again"; the build goes on, as it did before, and the check is not marked done, so the next build tries that file again. The .nup way's wording and its stop for a file with no stand-in are unchanged. When the game folder's romfs is the player's folder itself, or holds it, the damaged files are listed (printed and inlongshot_damaged_files.txt) and not replaced, and the build goes on. The .nup and--updateways are unchanged; the scan's progress line says "once per unpack or folder".tests/test_make_game_cache.py): a chosen romfs with a .zs that starts with zeros beside a whole one of the same kind, and a second hard link to the damaged file: the game folder's file is the stand-in, the player's file and its other name still hold their bytes (link count 2), the note names it, and a second build from the same folder opens no file again. A leftover.partin the player's folder keeps its bytes, and the list stays after the next build. A stand-in deleted from the game folder is put back on the next build. A replacement refused with PermissionError (os.replacemade to refuse one file) leaves that file as linked and listed, while the other damaged file still gets its stand-in; the note names both; the next build, with the refusal gone, puts the stand-in in. A romfs that is the game folder's own, or inside it, is only reported. The harness letsnx2/unpack.pythrough for a chosen exefs.tests/test_damaged_check_progress.pyreads the scan call, which now has a second argument, andnx2/test_nup_stand_in.py's stand-in stub takes the newrefusedargument (that check runs only with zstandard installed).Measurements
Verdict: the new checks fail on main and pass here; the suite has no new failures.
Hardware: none used (game-free checks on a Linux x86-64 PC, Python 3.14, gcc 15.2)
Scene: none, the game was not run
Method: 2026-11-14.
python3 -I -B tests/run_all.py, two runs each, interleaved (main, PR, main, PR). The new checks also ran against main'snx2/make_game.py(LONGSHOT_CACHE_TEST_SOURCE, in a full checkout of main). Branch:fix/romfs-folder-stand-ins.How to reproduce: the commands above in a checkout of each commit
543846eb602ad901tests/run_all.py(run 1, run 2)test_bake_portability.py,test_walk_key.pytests/run_all.pyin a venv with zstandard (one run)test_lift_cache.py(no numpy in that venv),test_walk_key.pytests/test_make_game_cache.py, folder stand-in casetests/test_make_game_cache.py, own folder casetest_bake_portability.pyfails here for a missing Python module (zstandard).test_walk_key.py's fixture doesn't link on main (undefined reference to hos_oot_autoload_input).Flags and settings
None.
Testing
Tested: the game-free suite on a Linux PC, as in Measurements.
Not tested: a real incomplete dump (none here), and the game's Water Temple after such a build. Windows: neither a real read-only attribute nor the copy a failed hard link falls back to (the stand-in replaces that copy the same way).
Must be tested:
Risks
What could break: A build from folders now opens every .zs file once per folder, which takes minutes where a virus scanner looks at each one, as an unpack's check does. While a file stays refused, the check is not marked done, so every build from that folder opens every .zs again (minutes) until the file can be replaced. A check that runs again (after a refusal, or a file linked in afresh) and finds damaged files lists only those, so that build's note no longer names files an earlier build already replaced. #534 adds a check of the chosen romfs in
mainand the sameunpack.pyline in the test harness; both merge cleanly together and the cache tests pass on the merge. #360 (open) copies these marks between game folders and callslink_or_copy, which now returns how many files it put in: whichever lands second needs a small rebase.Turning it off: Reverting the commit takes the check off the folder way.
Compatibility: A damaged file with no stand-in now stops a build from folders, as it does on the .nup way. As before, a game folder built again keeps the files it already holds (
link_or_copyskips names that exist), so a whole dump put at the same path later does not replace stand-ins already in the game folder.Issues
For #464: walls missing in the Water Temple, as with a damaged texture. The reporter's log does not say how their game folder was made or whether their dump is complete, so this may not be their case; they were not asked.
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
mainand passing here.tests/that reads changed source updated in this PR, not weakened.WIP: fix(build): a game built from a romfs folder gets its damaged textures replaced, as one from a .nup doesto fix(build): a game built from a romfs folder gets its damaged textures replaced, as one from a .nup does359b340f1f602ad901c0Author of #464 here. I did use romfs+exefs to build the game, dumped folders total 24.4 GB, original nup file totaled 23.3 GB. Will attempt a complete dump and report back.
My issue was fixed by using a complete dump.
Code review only (no tests run, no game run). The approach holds up:
os.replaceof<file>.partswaps the game folder's directory entry, so the player's file and its other hard links keep their bytes; the.partpre-removal avoids writing through a linked leftover;own(same folder, or romfs holding the source) is only reported; a refused replace is listed and the stamp is not written, so it is retried.Things I'd like answered before merge, none a clear bug:
fail(...)), where it used to build a game that left those models out. The PR says so under Compatibility; is that intended for the folder way, given the player can't re-extract as easily as re-picking a .nup? An error saying what to do is fine, I just want the decision to be explicit.known(the.longshot_headslist) is read fromoutfor the folder way too. Ifoutholds a heads file left by an earlier .nup extraction, the folder check would use that list instead of opening files. Please confirm it is removed or cannot be stale there (the end of the function removes it, but only when the function gets that far, andfail(...)returns earlier).The PR is not run on a real incomplete dump and lists that under Must be tested, so it should stay open for a person or tester with one. Commenting rather than approving for that reason and the questions above.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.