mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
NX2navi-Setup.exe installs the launcher into %LOCALAPPDATA%\Programs\NX2navi without administrator rights, with shortcuts and an Apps entry. The launcher finds the player's .nup, downloads the build tools itself (the MSYS2 UCRT64 packages toolchain.lock.json pins, checked by sha256 and unpacked without pacman, and zbic from PyPI) and runs tools/make_game.py, whose steps it shows with their progress. Its window follows the game's options menu: colours sampled from it, the menu's filigree, the white glowing cursor, the Triforce in the title band and as the programs' icon, and all text in the game's Cetra Display, decoded from the player's own dump. Mouse, keyboard and an XInput pad drive it. Updates come from GitHub releases (NX2navi-update.json): the new version is downloaded, checked and staged beside the running one, the stub restarts into it and falls back to the previous version when it fails to start, and the game is rebuilt without the dump. make_game.py now keys its exefs and romfs steps on the game files alone, has --rebuild and --events, compiles only the files whose source, headers, flags or compiler changed, and replaces ocarina.exe only once the new one links. tools/make_launcher.py builds the release on Windows or Linux. Tested: tests/launcher_test.py (19 tests), the real toolchain download and install, and under Wine 11 the setup, the stub, the window on the bundled Windows Python, an update from a local feed by clicking Updates, the rollback of a version that fails to start, and the downloaded clang compiling and linking a program against the game's libraries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Merged onto the current port: make_game.py keeps the launcher's step keys, cached link and in-place swap, and its own runtime manifest (windres), AMD's FidelityFX library and the shader preparation, which becomes a step of its own ("shaders") that the launcher shows in its install, rebuild and update jobs. make_launcher.py ships the shader tools and, with --ffx, AMD's library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.5 KiB
Python
132 lines
5.5 KiB
Python
"""The launcher without its window, for scripts, tests and anyone who prefers a console:
|
|
|
|
python -m launcher.cli check is there a newer release?
|
|
python -m launcher.cli update download and stage it, then finish in the new version
|
|
python -m launcher.cli tools install the build tools and zbic
|
|
python -m launcher.cli install --dump F --game D build the game (the window's Begin Installation)
|
|
python -m launcher.cli rebuild [--game D] rebuild after an update (the window's Rebuild the Game)
|
|
python -m launcher.cli fonts [--dump F] decode the game's font for the window
|
|
python -m launcher.cli continue-update OLD what the new version runs after `update`
|
|
|
|
In an installed launcher: python\\python.exe app\\<version>\\launcher\\cli.py <command> (this file puts the release
|
|
folder on sys.path itself, as start.pyw does). Exit code 0 on success, 1 on failure, 3 when cancelled.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
if __name__ == '__main__' and not __package__:
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
__package__ = 'launcher' # noqa: A001 — lets the relative imports below work when run as a script
|
|
|
|
from . import build, gamefiles, jobs, versions # noqa: E402
|
|
from .context import Context # noqa: E402
|
|
from .paths import Layout # noqa: E402
|
|
|
|
|
|
def follow(job):
|
|
"""Run a job here, printing each step's state as it changes."""
|
|
seen = {}
|
|
job.start()
|
|
while job.result is None:
|
|
for s in job.steps:
|
|
key = (s.state, s.value if s.state != jobs.RUNNING else int(s.fraction * 20))
|
|
if seen.get(s.id) != key:
|
|
seen[s.id] = key
|
|
print(f'{s.label}: {s.value}' + (f' ({s.detail})' if s.detail and s.state == jobs.RUNNING else ''),
|
|
flush=True)
|
|
time.sleep(0.2)
|
|
for s in job.steps:
|
|
if seen.get(s.id, (None,))[0] != s.state:
|
|
print(f'{s.label}: {s.value}', flush=True)
|
|
if job.result == 'failed':
|
|
print(f'error: {job.error}', file=sys.stderr, flush=True)
|
|
return {'ok': 0, 'failed': 1, 'cancelled': 3}[job.result]
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(prog='launcher.cli', description=__doc__.splitlines()[0])
|
|
ap.add_argument('--root', help='the launcher folder (default: this release\'s, or build/launcher-dev)')
|
|
sub = ap.add_subparsers(dest='cmd', required=True)
|
|
sub.add_parser('check')
|
|
sub.add_parser('update')
|
|
sub.add_parser('tools')
|
|
p = sub.add_parser('install')
|
|
p.add_argument('--dump', required=True)
|
|
p.add_argument('--game', required=True)
|
|
p = sub.add_parser('rebuild')
|
|
p.add_argument('--game')
|
|
p = sub.add_parser('fonts')
|
|
p.add_argument('--dump', default='')
|
|
p.add_argument('--game', default='')
|
|
p = sub.add_parser('continue-update')
|
|
p.add_argument('old')
|
|
a = ap.parse_args(argv)
|
|
ctx = Context(Layout(root=a.root) if a.root else None, echo=False)
|
|
s = ctx.settings
|
|
|
|
if a.cmd == 'check':
|
|
job = jobs.CheckJob(ctx).run_inline()
|
|
if job.result != 'ok':
|
|
print(f'error: {job.error}', file=sys.stderr)
|
|
return 1
|
|
r = job.release
|
|
if not r:
|
|
print('no release found')
|
|
elif jobs.update_available(ctx, r):
|
|
print(f'update available: {versions.display(r.version)} (you have {ctx.version})')
|
|
else:
|
|
print(f'up to date: {ctx.version} (newest release {versions.display(r.version)})')
|
|
return 0
|
|
|
|
if a.cmd == 'update':
|
|
check = jobs.CheckJob(ctx).run_inline()
|
|
if check.result != 'ok' or not jobs.update_available(ctx, check.release):
|
|
print(check.error or f'up to date: {ctx.version}')
|
|
return 0 if check.result == 'ok' else 1
|
|
job = jobs.UpdateDownloadJob(ctx, check.release)
|
|
code = follow(job)
|
|
if code or job.after != 'restart':
|
|
return code
|
|
new = ctx.layout.apps / versions.display(check.release.version)
|
|
cli = new / 'launcher' / 'cli.py'
|
|
print(f'finishing in {versions.display(check.release.version)}', flush=True)
|
|
return subprocess.call([ctx.layout.python_for_builds(), str(cli), '--root', str(ctx.layout.root),
|
|
'continue-update', ctx.version])
|
|
|
|
if a.cmd == 'continue-update':
|
|
gd = s.get('game_dir')
|
|
return follow(jobs.ContinueUpdateJob(ctx, gd if gd and build.built_version(gd) else '', a.old,
|
|
s.get('dump')))
|
|
|
|
if a.cmd == 'tools':
|
|
job = jobs.RebuildJob(ctx, '')
|
|
job.steps = job.steps[:1]
|
|
job.run = lambda: job.ensure_tools('tools_update')
|
|
return follow(job)
|
|
|
|
if a.cmd == 'install':
|
|
s['dump'], s['game_dir'] = os.path.abspath(a.dump), os.path.abspath(a.game)
|
|
s.save()
|
|
return follow(jobs.InstallJob(ctx, s['dump'], s['game_dir']))
|
|
|
|
if a.cmd == 'rebuild':
|
|
gd = os.path.abspath(a.game) if a.game else s.get('game_dir')
|
|
if not gd:
|
|
print('error: no game folder (give --game)', file=sys.stderr)
|
|
return 1
|
|
return follow(jobs.RebuildJob(ctx, gd, s.get('dump')))
|
|
|
|
if a.cmd == 'fonts':
|
|
got = gamefiles.extract_fonts(ctx.layout, a.game or s.get('game_dir'), a.dump or s.get('dump'))
|
|
for role, (path, fam) in got.items():
|
|
print(f'{role}: {fam} ({path})')
|
|
return 0 if got else 1
|
|
return 2
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|