A hand-edited "pace_smooth = 0 ; off" kept the comment in the value, which the readers took as on, "fps_limit = 144 #
monitor" gave 60, and "fullscreen = false" or "rtss = off" were on, since every reader took anything but "0" as on;
the log only named these lines (the check added for F-7). Now, for a key whose row has a set of values, cfg_load cuts
an unquoted value at the first blank followed by ';' or '#' after the '=' ("key = ; note" is unset) and logs the line
once; free text keeps its whole value (window_size, monitor, audio_device and keys without a row such as mods_dir,
pipeline_cache and the bindings, where a path or name may hold " #"), as do a value in double quotes and the
environment's values. A switch (a row whose set is 0|1) written as false, off or no reads as 0 and as true, on or yes
as 1 through cfg_get itself, so every reader agrees (cfg_bool, rt_knob, the readers that compare with "0" or parse a
number); cfg_bool and rt_knob also take those words for keys of other rows, and rt_knob no longer logs them. F-7's
check now names a switch value only when it is none of those, and a comment kept in a value only where it stays
(free text, the environment). The launcher's reader (launcher/gameini.py) cuts the same way for keys the caller
says have a set of values; its mods keys stay free text. The menu's save still writes a changed key's line again
without its comment.
This changes what existing files with such lines do, which is what was approved. tests/cfg_parse_check.py,
tests/rt_knob_check.py and launcher_test PlayerIniTest cover the cut, the words, free text, quotes and the
environment, and pass; cfg_rows_check reports 0 problems; the runtime compile check and the -Wall comparison of cfg.c
and perf.c show no new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`launcher.cli update` downloaded the release and switched to it without asking whether the game ran. The new version's continue-update then failed in its rebuild with "the game is running", which left the new launcher with the old build until Rebuild the Game. The window's App.start_update refuses first; the command now does the same before its update check, and ends with exit code 1 and "error: the game is running: close it first, the update rebuilds it".
Not reproduced before the change: found by reading the code. Checks that passed: tests/launcher_test.py (217 tests, a new one in which a running game stops `update` before its check and its download, which fails before the change) and tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Steam shortcut's --play runs before the window's single-instance lock and checked only whether the game was running. Started while the window rebuilt the game, installed an update or built the mods' code, it ran mods.prepare into the folder the job was reading and started the old ocarina.exe, and the job then failed at the swap with "ocarina.exe is in use" (an update's install left the new launcher on the old build); during the shaders step, make_game.py's own run of the exe made the shortcut log "already running" and do nothing visible. play_only now tries the launcher folder's lock and lets go of it at once: when another process holds it and data/job-running.json names a job (jobs.running_job, which leaves the marker in place), it logs that the launcher is busy, says which job to wait for in a message box and does not start the game. A marker that a stopped launcher left does not stop it, since its lock went with its process, and neither does the window open without a job.
Not reproduced before the change in a game run: found by reading the code. Checks that passed: tests/launcher_test.py (216 tests, a new one for a held lock with the job's marker, a leftover marker with the lock free, and the window open without a job, which fails before the change) and tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build.game_running opened the exe for writing and took every PermissionError that the read-only attribute did not explain for a running game. Python reports a sharing violation (a running image) and access denied (an access list that denies the player writes, or Controlled Folder Access switched on for a game folder under Documents) as the same PermissionError, so an exe that nothing ran but the player could not write kept Play answering "The game is already running", the Steam shortcut doing nothing and a rebuild failing with "the game is running". The open now goes through CreateFileW, with every share mode granted, and reads its error: 32 (ERROR_SHARING_VIOLATION) is a running game; 5 on a read-only exe clears the attribute and tries again as before; any other access denied is no running game, and the rebuild's check logs that the exe cannot be opened for writing, so that make_game.py's later failure to replace it has its cause beside it in launcher.log. The game's own profile lock is not probed instead: a handle on it, even for a moment once a second, makes a starting game end with "another copy of the game is running".
Not reproduced before the change in a game run: the audit's probes showed it on a scratch exe with a deny-write access entry. Checks that passed: tests/launcher_test.py (215 tests, a new one for errors 32, 5 and 0, a read-only exe and the real CreateFileW call on a free file and on a running sleeper compiled with the ucrt64 clang) and tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
play_only, the --play path of the Steam shortcut, logged only "the game ended (code N)" in launcher.log: it read no FATAL line, added no note to the game's log and showed nothing, so a game that ended with 70, 71, 72 or a Windows status left the player with no word of why (as when Steam starts it while the window replaces a save, and it ends at once with "another copy of the game is running"). The 7b0dbeb1 change that logs every end touched the window's watch only. play_only now follows the run with GameRun, the Restart's copies too, and takes the same game_end as the window: the line in launcher.log, the note in the game's log, and a message box only for an end worth one (a FATAL line, 70, 71 or 72, a Windows status; not 0, nor 1 from Task Manager's End task). The logs are written before the box, which waits for OK while Steam still counts the game as running.
Not reproduced before the change in a game run: the audit's probe showed it with a stand-in process. Checks that passed: tests/launcher_test.py (214 tests, a new one for the Steam shortcut's ends with codes 70, 0xC0000005, 0 and 1, which fails before the change) and tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher read the game's FATAL line only after codes 70 and 71, so a run that ended through vk_device.c vk_device_lost_check (code 72, as in the AMD device losses of 2026-11-12) showed a bare "code 72" with no cause. 72 now joins 70 and 71 in FATAL_CODES and has a text of its own, which stands without a FATAL line because the loss's watchdog (vk_device.c lost_wait_exit) can end the run before that line is written.
The game's Restart (gfx_present.c gfx_window_closed) starts the exe again and ends the process the launcher started with code 0, and the launcher took that 0 as the run's code. A restarted copy that ended with 70, 71 or 72 was logged as an end with code 0 and its FATAL line was never shown; one that was killed or hung got a note saying code 0 in its log. Once the process it follows has ended with 0 while the exe still runs, the launcher now finds the copy by its image file (winsys.game_copy, compared as a file so that another install's game is not taken), holds a handle to it and takes its exit code; a further Restart is followed the same way. A copy it could not find ends with its code logged as not known. After code 0 or an unknown code, a FATAL line in a log written since the start can only be such a copy's, and it is shown as one; a log older than the start (a run that ended before it opened its own) is not read. A log cut at its 64 MiB cap (main.c log_sink) wrote its end lines to stderr only, and no longer gets a note saying it has no line of its own end. The end's handling is one function, game_end, for the Steam shortcut's play to share.
Not reproduced before the change in a game run: the audit's probes showed each of these ends with stand-in processes and logs. Checks that passed: tests/launcher_test.py (213 tests, new ones for code 72 with and without its FATAL line, the followed copy, a copy not found, a FATAL line after code 0 and a log older than the start, the cut log, and game_copy on a sleeper compiled with the ucrt64 clang) and tools/check_content.py --staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session of 2026-11-13 ended 8 s after its window lost the focus, after 7 minutes of play, with none of the game's exit lines: no frame-time summary of its last period, no stutter map of its last minute and no growth ranking at all, as the ranking came every 10 minutes. When the window loses the focus in play, the present thread now writes the frame-time summary so far (starting a new period), the stutter map and the growth ranking at its next present, at most once a minute; formatting only, nothing asks the device. The growth ranking comes every 2 minutes of play instead of 10, the stutter map every 5 instead of 10.
A process that ends through exit() rather than plat_exit now writes the log thread's queue last, after the frame-time summary's exit handler, instead of losing it.
The launcher logged a game's end only for a nonzero code, so the session's end with code 0 left no line anywhere. It now logs every end of a game it started, and when the game's log has no line of the game's own end (the window's close, the game's ExitProcess, a timed run's end, a FATAL or exit line), it notes the end there too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This takes back the three commits that added it (cd2f9f1, e60677e, a01910d): its runtime layer and Steam binding, the Co-op tab and what it drew over the game, the launcher's copy of Steam's library file, its settings, tests and docs. The tree is as it was before them, with the bug capture that came after kept: its note on screen no longer makes room for co-op's lines, and dbg_area, which the co-op had brought and the capture uses, stays.
The four settings of that co-op join the retired names, so a settings file that still has them logs no unknown key. A steam_api64.dll that the launcher put beside the game is left where it is: nothing loads it any more.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Main renamed the switch so that an old "coop = 1" turns nothing on; the launcher's check for Steam's library still read the old name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Play ran steamlib.ensure on the Tk thread, co-op on or off. Without the file that walks up to 60,000 folders
of the player's Steam libraries and checks signatures, and the window stood still meanwhile. Play and the Steam
shortcut's start now call it only when co-op is on in the game's settings (steamlib.wanted, read as the game reads
them), and Play runs it on a thread of its own and starts the game from the window's thread when it is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-op loads steam_api64.dll from beside ocarina.exe, and the repository may not hold that file. Before Play and before a start from the Steam shortcut, when the file is not there, launcher/steamlib.py now copies one: one a release ships in its bin folder if it has one, else the newest one in the games of the player's own Steam libraries that Valve signed (WinVerifyTrust and the signer's name) and that has every call runtime/steam.c uses. Nothing is downloaded, the search takes a second or two once, and with no Steam or no suitable file the game starts as before and the Co-op tab says what is missing. The tab's two texts about the file say so in the four languages.
Checked: four new tests on made-up library folders (too old, unsigned, too deep, a second library, a shipped file first, none said once) and the launcher's 190 tests pass; on this PC the search found 40 files in 1.2 s, 25 suitable, and copied a Valve-signed one; that file's library version, an older one than the game was run with, signs in as app 480 and makes a lobby in a program without the game. Not run: Play in the launcher itself with this change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The hidden beta shared story progress through a relay a player had to run and never showed the other players. A new co-op replaces it. Gone are runtime/coop*.c and their headers, the menu's Co-op tab and its texts in four languages, tools/coop_relay.py, the co-op tests, docs/coop.md, the [coop] section of nx2navi.ini.example, the --probe-coop mode, the coop trace item, and Monocypher, which only this layer used.
The old settings stay known as retired keys, so an nx2navi.ini that still sets them logs no unknown key, and they stay on the list of private settings, so a bug report keeps a room's secret, a relay's address and a name out. The platform layer's UDP sockets and fs_save_snapshot stay for the new co-op; cfg_ini_now and fs_profile_path, which nothing else called, are removed.
Checked: the game builds and starts, the menu shows its four tabs, --probe-cfg and --probe-fs pass, and the launcher's private-settings test passes. tests/save_share_check.py builds without its co-op stub; it cannot run from this worktree because the folder's path is longer than Windows allows, on main too.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The worker, the thread that reads the build's output and the window all change a job while the window draws it. A
step could be read with a new state and the value before it, the output's lines could change while they were
copied, and the change counter was incremented without a lock although the window learns from it that the job
ended. Every change and the counter now take the job's lock, and the job screen and the command line copy the steps
under it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A part left by a download of other bytes under the same name was resumed, and the result failed its sha256 check
as if the download were damaged. A note beside the part now names the file it belongs to by the expected sha256;
a part of another file is deleted with a log line and the download starts over, and the same file is still resumed
from another server.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A file called after a device, ending in a dot or a space, or holding a colon was unpacked into the device, under
another name or as a hidden stream, and the game missed it without a word. On Windows the archive is now refused
with the file's name and the reason; elsewhere such names pass as before.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Windows and the build tools stop at 260 characters for a file's whole path, and a built game needs up to 128 below
its folder. A deeper folder made the build stop in the middle with a file "not found"; Begin Installation and the
install job now refuse it first and name both lengths.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The window asked whether the game runs before the player picked the save, so the game could start during the dialog
or during the backup's copy and mount a save that was half replaced. The import now takes profile/instance.lock as
the game does: a running game refuses it, and a game started in the middle is turned away by its own lock.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A disk that filled in the middle ended a download after four tries with an error that named no cause, and an unpack
with half a folder. The rest of a download, the build tools with their unpacked size, an update's archive and a
mod's zip are now measured against the free space first, and the refusal says how much is needed and how much is
free; a disk that fills during a download is named as full at once.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The total was "length + have or size or 0", which is the part file's size when a resumed answer names no length:
the bar ran past its end. It is now the expected size, else the server's length plus the part, else unknown.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game and the launcher each read nx2navi.ini, change their own lines and put a new file in place, so of two
changes in the same moment the second dropped the first. The mods' switches already refused while the game runs;
the Discord Status switch of the Settings screen now does too, and says to use the game's menu.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A job that writes into the game folder leaves data/job-running.json while it runs and removes it when it ends. A
launcher closed or killed in the middle leaves it, and the next start logs it and tells the player which job did not
finish and that Rebuild the Game or Begin Installation goes on from the steps that were done.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Add a Mod copied straight into the mods folder, so a copy that stopped halfway left a half mod there, switched on
at the game's next start. It now goes into a folder beside the mods folder and moves in with one rename; a failed or
interrupted copy removes what it made.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The window already locked data/launcher.lock, but a second one closed without a word, the command line did not take
the lock, and nothing kept two jobs apart. The lock is now launcher/locks.py: the window, every command but check and
every job that changes files hold it, the one refused says who holds it, and a job names what it changes so that a
second one on the same thing fails before its first step.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The 0.9.1 notes keep the release branch's text and take main's sections: New, the edge line under Fixed, and Changed.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Mods, on the home screen, opens the game's mods folder, adds a downloaded mod from its .zip, .7z or .rar file or its
folder, switches each mod on or off, and sets the load order (mods_disabled and mods_order in nx2navi.ini, written as
the game writes them). Each mod's description says what it changes, which other mod changes the same files and which
of the two wins, and why the game leaves it off when it does.
Before the game starts, Play puts the mods folder in order (launcher/mods.py prepare): an archive in it is unpacked into
a folder of its name and kept, a mod whose files sit a folder or more down has them moved up, and a romfs, exefs or
cheats folder put straight into the mods folder becomes a mod of its own. An archive that would write outside its
folder is refused, and nothing is deleted.
The launcher's build (tools/make_game.py) left the mods' code cheats and ExeFS patch files out, so those mods stayed
off. The lift now turns them into patch sets first, as the setup does, with the files in the lift's key and
build-cache/mod-code.json recording what it took up. Play builds the game again before it starts when an enabled mod
brings code no build has seen. The shaders' key names the lift and the compile without the mods' code, so such a build
compiles only what the patches change and leaves the shaders prepared. tools/mod_files.py is the one place the tools,
the build and the launcher find a mod's parts.
Tested: a zipped mod holding its own folder, put into a mods folder, was unpacked and moved up by the launcher's code,
and the game served its file in place of the game's. The lifter took a code cheat and a pchtxt patch made for the game's
main as patch sets. The launcher's and the setup's tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run from a checkout, the launcher uses the Python that started it. A Python older than 3.14 without the zstandard
package got through the first-run screen and stopped at Gather Build Tools with only "No module named 'zstandard'",
after the packages were downloaded. toolchain.zstd_missing now looks for compression.zstd or zstandard when the
launcher starts. Without either, the log and the first screen name the Python and give the pip line for
tools/requirements-launcher.txt, and Gather Build Tools stops with the same message before it unpacks anything. A
release's own Python always has the package; if it ever lacks it, the message says to reinstall NX2navi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Hide Launcher While Playing off, the launcher stayed in its normal state behind the game and kept stepping its
animations: about 0.6 of a CPU core went to a window nobody saw, beside the game's render thread. The animations now
also stand still while the window is not in front, the same check the gamepad polling uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brought onto the line with FSR 4, its frame generation and the debug menu. Where both sides had added to the same
place, both stay: the patch table keeps the pickup count and the free camera, the settings list keeps both sides'
names, the menu includes the co-op page and the debug menu, and the launcher builder takes the pipeline list and the
FSR 4 folder. Both sides had added the menu's typed-character event; one definition stays, with the window code that
passes characters outside the basic plane.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Button Color recoloured the list panel, the description box and the title band, and left the orange choices in the rows as they were, so the buttons themselves seemed not to change. It now takes the rows' label cells, their value cells and the orange choices, which come out a lighter shade of the picked colour; the new Panel Color takes the panels, the dividers and the title band. Text on a light button or panel turns dark, and so does the text in choices that come out bright. The colour is kept as custom_panel in launcher.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changing the scheme redrew the window but kept the rows the screen was opened with, so the two rows came and went only when the screen was opened again, as after choosing a picture. The rows are now made again each time the scheme changes, with the cursor staying on Color Scheme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Custom is the last scheme in Customize Launcher. Its Background Color and Button Color rows open a picker in the description box's place: a square of the hue's colours (saturation across, brightness up and down) with a hue bar under it to click or drag on, Hue, Saturation and Brightness rows for the arrows and the gamepad, and a Hex Code row to type a code into. Each picked colour takes the place of one colour of the first scheme, the backdrop's middle tint or the buttons' wood, and the rest of its group follows it, lighter and darker as in the first scheme; light buttons get dark text. The colours are kept as custom_background and custom_buttons in launcher.json.
The background picture used to fill the whole window, so its top was hidden under the title band. It now fills the window from where the band ends, cut to that shape around its middle and scaled evenly. A copy an older launcher kept for the whole window is cut, never squeezed, and made again from the picture file while it is there.
Add to Steam's description and result are now one plain sentence each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This line logs every setting nx2navi.ini sets, and a bug report takes a copy of nx2navi.ini along: with co-op both would have carried a room's secret, the relay's address and the player's name to whoever reads the report, and the secret lets anyone into the room. One list (runtime/cfg_private.h: the room's secret, the offer number, the relay, the name, the key a maker removes, the menu's test hook) now serves the log's line, "Copy system info" and the report's copy of the file, in the game and in the launcher: such a key reads "(set, not shown)", every other byte of the file stays.
Also: the last leftovers of the restorations that were left out with the puppets (their settings prefix and section), main.c's byte order mark as it was, and two comments that cited files this tree does not have.
Checked: with a room, a relay and a name in nx2navi.ini the log's line shows all three as set and none of their values; the same text gives the same bytes through the game's scrub and the launcher's (a launcher test, and the C function built alone); the launcher tests, the co-op probe, the relay tests and the tab check pass.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The launcher asked for 31 GB where the build's own check now asks for about 34, so a disk between the two passed the first check and was refused seconds later. Both say 34 GB now. The build no longer asks for the whole shader preparation's space when the game folder already holds translations. The texts said 10 to 25 minutes for an install that took players 27 to 41, and 5 GB for a build cache that measures 1.6. A build error's message goes into the launcher's log as one line.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Every selection of the Updates row started a request to GitHub, which
allows an address 60 an hour: a player who selected it 25 times in 90
seconds had every check of the next hour refused. The row and F5 now
start no check within a minute of the last one.
A refused check said "try again in an hour" whatever the answer held. It
now names the time from the answer's X-RateLimit-Reset or Retry-After.
A failed check wrote two tracebacks, 22 lines, into launcher.log each
time; a FeedError is now one line with its message and its cause.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
build.Run read the build's output on a thread that ended on the first
exception of a line or event handler, such as the launcher's log on a
full disk. Nobody read the pipe after that, the build stopped at its next
line once the pipe was full, and the job waited for it for good. The
thread now counts the line as dropped and reads on, and Context.log loses
a line it cannot write instead of raising.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The build compiles on every processor for ten minutes and more while the
job screen invites the player to keep using the PC. build.Run now starts
make_game.py with Windows' below-normal priority class, which the
compilers and the other processes the build starts take over from it, so
the build gives way to what the player does and loses nothing on an idle
PC.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game exits with 70 after a FATAL line of its own and with 71 after
its crash report, and the launcher said only that the line near the end
of the game's log names the error. It now reads the last FATAL line of
the run from logs\nx2navi.log and puts it into the home screen's message
and into launcher.log, so a launcher log a player sends says what the
error was.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The check printed its counts into the build's lines only, so a player
whose build ended with "4 damaged, 3 given a stand-in, 1 left as they
are" saw nothing of it. A job now reads romfs-check.json after the build:
the count goes into the step's row, and when the check ran in that job, a
sentence goes onto the home screen and into launcher.log.
The same four files are flagged on every copy of the game, and three of
them get a stand-in. For those the sentence says so and that nothing
needs doing; other files are counted and romfs-check.json is named.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The Updates row promised that an update rebuilds the game "in a few
minutes". The code cannot keep that: a version that changes the lifter
compiles the game's code again, and one that changes what the shaders are
built with builds all of them into the driver's cache again. Players'
updates took 24, 34 and 45 minutes. The row now says it takes a few
minutes when little changed and half an hour or more otherwise.
The warm-up of the driver's cache after an update only builds, but its
count was drawn in the second half of the bar as in a full preparation,
so a 20 minute run read 50 % in its first second. Its count now fills the
whole bar, and the row says so while the shader cache is packed before it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A RomFS step that was cut short (Cancel, a full disk, a power cut) leaves
romfs.bin.part, up to 26 GB. The next run writes the image anew, but
make_game.py's step_romfs and the launcher's Begin Installation measured
the free space first, with that file counted as used: on a nearly full
disk the retry was refused although it needs no more room than the first
try. Both remove the file before they measure, and the first-run screen
counts its size as free.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
start.pyw showed the import failure in a message box before it ended with
code 86, and the stub waits six seconds for that code: unless the player
closed the box in time, the broken version stayed the current one. When
app/previous.txt names another complete release, the failure now goes to
crash.log only and the process ends at once, so the stub rolls back and
the previous version says on its home screen which version did not start.
The box stays when there is nothing to go back to, when Steam started the
launcher without the stub, and when the stub's wait is already over.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
An update whose lift ran again drops the compile step's record before the
compile runs, so a compile that then stopped left state.json without it,
build.built_version said '' and the launcher's next start showed the
first-run screen and asked for the dump. build.installed now also counts a
folder that holds the exe of the build before and game files a rebuild can
start from: the window opens on home, where Rebuild the Game reads "Not
Finished" and Play says which build it starts, and an update's rebuild and
the command line's go on with the game. Begin Installation into a folder
whose game files are already unpacked builds on from them instead of
reading the dump again.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A game that stops before its first save is the one its player most wants to report, and Export Save refused it: "the game has no save yet". With Add Log and Settings on (the default) the launcher now writes NX2navi-log-<date>_<time>-bug-report.zip with the game's latest log, nx2navi.ini and a manifest that counts no save files, and says so on the Saves screen. Without a log there is still nothing to export, an export without the log and settings still needs a save, no account is made for the export, and an import refuses such a zip as one without a save.
This is the first of the two faults pull request #364 names, written anew inside the existing export; its Export Diagnostics screen and its separate crash log are not taken.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The new row, Add to Steam (Windows), puts a non-Steam shortcut named "The Legend of Zelda Ocarina of Time" into the shortcuts.vdf of every Steam account that signed in on the PC (launcher/steam.py): the binary key-value file is read and written back as it was, with ours added or updated in place, and the file from before the first change kept beside it. Steam reads that file only at its start and writes it back when it closes, so a running Steam is asked to close (-shutdown) and started again after.
The pictures come from the player's game when the row is selected (launcher/gameart.py), as the launcher's fonts and sounds do: Navi from the HUD's icon and its glow, the logo with the Master Sword and the Hylian Shield from the title screen's texture archive (decoded with runtime/tex_astc.c, built into a library with the toolchain's clang), and a frame 12 seconds into the first title movie, the field under the full moon, decoded from its AV1 with the toolchain's dav1d.exe, as the background, the cover and the wide tile. They go into config/grid named by the shortcut's id, CRC-32 of Exe and AppName with the top bit set, which matches the id Steam gave an existing shortcut on the test PC.
Steam starts the launcher's pythonw.exe on data/steam_play.pyw, which runs the launcher of app/current.txt with the new --play: the game starts from its folder with its console hidden, as Play starts it, and the process waits for it and for the copies its own Restart starts, so Steam counts the game as running while it is. ocarina.exe is a console program, so starting it directly would open a console window beside the game, and NX2navi.exe ends six seconds after it started the launcher. Started that way on an RX 7900 XT PC, the game ran, the Python process stayed until the game was closed and ended with code 0. Tested against copies of that PC's Steam files; the real Steam library is not changed until the player selects the row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>