diff --git a/README.md b/README.md index 9c417385..47244d5a 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,14 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md). Download `NX2navi-Setup-.exe` from the releases and start it next to your `.nup` dump. It is the only file you need. The launcher it -installs finds the dump, downloads the build tools, builds the game on your PC (10 to 25 minutes, once) and starts +installs finds the dump, downloads the build tools, builds the game on your PC (20 to 45 minutes, once) and starts it; afterwards it keeps the port up to date and rebuilds only what changed ([docs/launcher.md](docs/launcher.md)). [docs/README-BuildKit.md](docs/README-BuildKit.md) describes the same build by hand, [docs/building.md](docs/building.md) building from source. Settings are in the in-game menu and in `nx2navi.ini` ([nx2navi.ini.example](nx2navi.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md). Requirements: Windows 10 or later; an x86-64 CPU with SSE4.2 (AVX2/FMA machines get a faster build); 16 GB of RAM; about -30 GB free for the game and 1.5 GB for the launcher and its tools; a GPU with Vulkan 1.3, `VK_EXT_external_memory_host` +34 GB free for the game and 1.5 GB for the launcher and its tools; a GPU with Vulkan 1.3, `VK_EXT_external_memory_host` and `VK_EXT_mesh_shader`. The setup is not signed, so SmartScreen warns on its first start. ## Known issues diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index bd459a32..432586be 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -13,7 +13,7 @@ downloads included, and keeps the game up to date. This page is the same build b NVIDIA). - Your own dump of the game as a `.nup` file (or its `exefs` and `romfs` folders), with the 1.1.1 update: program `0400486024DEC000`, main build `e0f3ccfa`. Other versions are refused. -- About 30 GB free on the disk you build to, besides the dump. +- About 34 GB free on the disk you build to, besides the dump. ## One-time setup (about 15 minutes) @@ -58,7 +58,7 @@ last draws the Xbox, PlayStation, Steam Deck and keyboard and mouse button promp meanwhile. If it stops, run the same command again: finished steps are skipped. When it says `finished`, start `C:\Games\Ocarina\ocarina.exe`. You can delete `C:\Games\Ocarina\build-cache` (about -5 GB) afterwards. +1.5 GB) afterwards. ## Linux diff --git a/docs/launcher.md b/docs/launcher.md index c388dd22..a5570ba1 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -14,7 +14,7 @@ it. The code is `launcher/`, the release is made by `tools/make_launcher.py`, an drive, and proposes `\game` as the install folder (or `X:\Games\NX2navi` on the drive with the most free space when that drive has less than 40 GB). Both rows can be changed. 3. Selects **Begin Installation**. The launcher downloads the build tools (about 170 MB, once), checks the dump, - unpacks it, translates and compiles the game: 10 to 25 minutes. It keeps the PC awake meanwhile. + unpacks it, translates and compiles the game: 20 to 45 minutes. It keeps the PC awake meanwhile. 4. Selects **Play**. The launcher hides while the game runs and comes back when it exits. From then on the launcher looks for a new version when it opens and every hour while it runs, also while it is @@ -278,7 +278,7 @@ When the new version fails to start (exit code 86 from `start.pyw`, or any error `data/logs/crash.log` and shows it in a message box only when there is no version to go back to, when Steam started it, or when the six seconds are over (`quiet_exit`): a box holds the exit code back until it is closed, and the broken version then stayed the current one. The game is never rebuilt while it -runs. Keep Build Cache off deletes the lifted C and the objects (about 5 GB) after each build but keeps +runs. Keep Build Cache off deletes the lifted C and the objects (about 1.5 GB) after each build but keeps `build-cache/state.json`, so updates still rebuild without the dump (compiling everything). ## Sharing a save @@ -450,7 +450,7 @@ fail when that logic changes, to ask whether the step must run again on every in ## Requirements and limits -- Windows 10 or 11, x86-64; the build's own requirements (README-BuildKit.md): 16 GB of RAM, about 30 GB free for +- Windows 10 or 11, x86-64; the build's own requirements (README-BuildKit.md): 16 GB of RAM, about 34 GB free for the game and 1.5 GB for the launcher and its tools, a Vulkan 1.3 GPU. - The programs are not signed: SmartScreen warns on the first start of the setup. - English only for now. diff --git a/launcher/jobs.py b/launcher/jobs.py index f90206f9..a411236f 100644 --- a/launcher/jobs.py +++ b/launcher/jobs.py @@ -15,7 +15,7 @@ PENDING, RUNNING, DONE, SKIPPED, FAILED = 'pending', 'running', 'done', 'skipped # Failures whose message says all there is to say: the log gets one line for them (failure_line), not a traceback. # A refused update check wrote 22 lines of two tracebacks into launcher.log each time. -PLAIN_FAILURES = (feed.FeedError,) +PLAIN_FAILURES = (feed.FeedError, build.BuildError) def failure_line(e): diff --git a/launcher/ui/app.py b/launcher/ui/app.py index 343d708c..5eea462b 100644 --- a/launcher/ui/app.py +++ b/launcher/ui/app.py @@ -788,12 +788,12 @@ class App: dump_desc = ('Choose your own dump of Ocarina of Time with the 1.1.1 update, as one .nup file. None was ' 'found in Downloads, Desktop or Documents.',) gd = self.game_dir() - need = 30 + need = 34 free = free_gb(gd) + partial_image_gb(gd) # Begin Installation removes the partial image (begin_install) folder_desc = (f'The game is built into this folder. {free:.0f} GB free on {drive_of(gd)}; the game needs ' f'about {need} GB. Select this row to choose another folder.',) ready = bool(dump) - begin_desc = (f'Build Ocarina of Time on this PC from your own dump. It takes 10 to 25 minutes, once, and ' + begin_desc = (f'Build Ocarina of Time on this PC from your own dump. It takes 20 to 45 minutes, once, and ' f'downloads 170 MB of build tools. The game needs about {need} GB on {drive_of(gd)} ' f'({free:.0f} GB free).',) if ready else \ ('Choose your dump first: select Game Dump above.',) @@ -887,8 +887,8 @@ class App: return gd = self.game_dir() drop_partial_image(gd) # before the free space is measured - if free_gb(gd) < 31 and not (Path(gd) / 'romfs' / 'romfs.bin').is_file(): - self.message = f'{drive_of(gd)} has {free_gb(gd):.0f} GB free; the game needs about 30 GB.' + if free_gb(gd) < 34 and not (Path(gd) / 'romfs' / 'romfs.bin').is_file(): + self.message = f'{drive_of(gd)} has {free_gb(gd):.0f} GB free; the game needs about 34 GB.' self.render() return self.settings['game_dir'] = gd @@ -1486,7 +1486,7 @@ class App: self.toggle('Hide Launcher While Playing', 'hide_while_playing', 'Hide this window while the game runs; it comes back when you quit the game.'), self.toggle('Keep Build Cache', 'keep_cache', - 'Keep the translated code and compiled files (about 5 GB) in the game folder, so an update ' + 'Keep the translated code and compiled files (about 1.5 GB) in the game folder, so an update ' 'compiles only what changed. Off: they are deleted after each build.'), self.channel_row(), *([self.discord_row(gd)] if gd and Path(gd).is_dir() else []), @@ -1686,7 +1686,7 @@ class App: self.show('job') def drop_build_cache(self): - """Keep Build Cache off: delete the lifted C and the objects (about 5 GB) but keep state.json, which an + """Keep Build Cache off: delete the lifted C and the objects (about 1.5 GB) but keep state.json, which an update needs to rebuild without the dump; the next build then lifts and compiles everything again.""" gd = self.settings.get('game_dir') if not gd: diff --git a/tests/launcher_test.py b/tests/launcher_test.py index 6d1830cb..27589b5a 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -2862,6 +2862,15 @@ class MakeGameTest(Tmp): self.assertEqual([n for _, n in mg.space_needs(['lift', 'compile', 'shaders'], self.out, carried=True)], [gib]) self.assertEqual(mg.space_needs(['shaders'], self.out, carried=True), []) self.assertEqual(mg.space_needs([], self.out), []) + # a shader cache that was begun is not asked for the whole preparation's space again + (self.out / 'shader-cache').mkdir() + self.assertEqual([n for _, n in mg.space_needs(['shaders'], self.out)], [4 * gib]) + (self.out / 'shader-cache' / '0123.spv').write_bytes(b'') + self.assertEqual(mg.space_needs(['shaders'], self.out), []) + (self.out / 'shader-cache' / '0123.spv').unlink() + (self.out / 'shader-cache' / 'shader-cache.pack').write_bytes(b'') + self.assertEqual(mg.space_needs(['shaders'], self.out), []) + shutil.rmtree(self.out / 'shader-cache') def check(free, needs): err = io.StringIO() @@ -4424,7 +4433,7 @@ def mock_app(test, game): class BeginInstallTest(Tmp): def test_a_cut_romfs_image_does_not_block_the_install(self): """Begin Installation removes the romfs.bin.part an install that was cut short left before it measures the - free space, so the 31 GB it asks for are not short by the part's size.""" + free space, so the 34 GB it asks for are not short by the part's size.""" import collections game = self.tmp / 'game' part = game / 'romfs' / 'romfs.bin.part' diff --git a/tools/make_game.py b/tools/make_game.py index 7d642e82..3e8c0fdd 100644 --- a/tools/make_game.py +++ b/tools/make_game.py @@ -1160,7 +1160,10 @@ SPACE_IMAGE_MARGIN = 1 * GiB # beside the RomFS image, as step_romfs asks def space_needs(runs, out, image_bytes=0, carried=False): """[(what, bytes)] this build will write into the game folder, for the steps it runs (steps_to_run). image_bytes: the size of the RomFS image when that step runs; carried: the shaders step keeps the shaders - prepared before (step_shaders update) and writes none.""" + prepared before (step_shaders update) and writes none. A game folder that already holds translations (a + preparation that was cut short, or one repeated with the same translator) is not asked for the shaders' space: the + preparation then writes only what is missing, which may be nothing, and asking for all of it would stop a build + that has the room it needs.""" needs = [] if 'romfs' in runs: needs.append(('the RomFS image', image_bytes + SPACE_IMAGE_MARGIN)) @@ -1168,11 +1171,23 @@ def space_needs(runs, out, image_bytes=0, carried=False): lifted = (out / 'build-cache' / 'lift' / 'layout.json').is_file() needs.append(('the build cache (the translated code and its compiled files)', SPACE_REBUILD if lifted else SPACE_BUILD_CACHE)) - if 'shaders' in runs and not carried: + if 'shaders' in runs and not carried and not shaders_begun(out): needs.append(('the prepared shaders', SPACE_SHADERS)) return needs +def shaders_begun(out): + """True when the game's shader cache holds a translation already: its pack, or a loose file.""" + cache = out / 'shader-cache' + if (cache / 'shader-cache.pack').is_file(): + return True + try: + with os.scandir(cache) as it: + return any(e.name.endswith('.spv') for e in it) + except OSError: + return False + + def check_space(out, needs): """Stops the build before its first long step when the game folder's disk cannot hold what the build will write (space_needs), naming each part and how much is missing. Only the RomFS image's space was looked at,