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>
116 lines
4.8 KiB
Python
116 lines
4.8 KiB
Python
"""Where the launcher keeps everything.
|
|
|
|
An installed launcher (NX2navi-Setup.exe puts it in %LOCALAPPDATA%\\Programs\\NX2navi) is one folder:
|
|
|
|
NX2navi.exe launcher/native/nx2navi.c: starts python\\pythonw.exe on app\\<current>\\launcher\\start.pyw
|
|
python/ the embeddable Python with tkinter, numpy, lz4 and zstandard (tools/make_launcher.py)
|
|
app/current.txt the version the stub starts; app/previous.txt the one before it, kept for a rollback
|
|
app/<version>/ one release: launcher/, runtime/, recomp/, tools/, bin/, VERSION, toolchain.lock.json
|
|
pydeps/ zbic, downloaded from PyPI on this PC (launcher/toolchain.py ensure_zbic)
|
|
toolchain/msys64/ucrt64 MSYS2's clang and libraries, downloaded from the MSYS2 repository on this PC
|
|
toolchain/pkgs/ the downloaded package files, kept while the lock names them
|
|
data/launcher.json settings; data/logs/ logs; data/cache/ the fonts read from the player's game
|
|
game/ the built game, unless the player picks another folder
|
|
|
|
Run from a checkout of the repository (python -m launcher), the app folder is the repository and everything else goes
|
|
to build/launcher-dev/, which git ignores.
|
|
"""
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
APP = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _is_release_app(app):
|
|
return app.parent.name == 'app' and (app.parent / 'current.txt').is_file()
|
|
|
|
|
|
def ansi_safe(path):
|
|
"""True when the path survives the Windows ANSI code page: the MSYS2 tools and the game's own build read their
|
|
arguments through it, so a folder name outside it breaks the build. Always True elsewhere."""
|
|
if os.name != 'nt':
|
|
return True
|
|
try:
|
|
str(path).encode('mbcs', errors='strict')
|
|
return True
|
|
except (UnicodeEncodeError, LookupError):
|
|
return False
|
|
|
|
|
|
class Layout:
|
|
def __init__(self, root=None, app=None):
|
|
self.app = Path(app or APP)
|
|
self.dev = not _is_release_app(self.app)
|
|
env = os.environ.get('NX2NAVI_LAUNCHER_ROOT')
|
|
if root:
|
|
self.root = Path(root)
|
|
elif env:
|
|
self.root = Path(env)
|
|
elif not self.dev:
|
|
self.root = self.app.parents[1]
|
|
else:
|
|
self.root = self.app / 'build' / 'launcher-dev'
|
|
self.apps = self.root / 'app'
|
|
self.python = self.root / 'python'
|
|
self.stub = self.root / 'NX2navi.exe'
|
|
self.pydeps = self.root / 'pydeps'
|
|
self.data = self.root / 'data'
|
|
self.logs = self.data / 'logs'
|
|
self.cache = self.data / 'cache'
|
|
self.downloads = self.data / 'downloads'
|
|
self.settings = self.data / 'launcher.json'
|
|
tool_root = self.root
|
|
if not ansi_safe(tool_root):
|
|
tool_root = Path(os.environ.get('SystemDrive', 'C:') + '\\') / 'NX2navi'
|
|
self.toolchain = tool_root / 'toolchain'
|
|
self.msys_root = self.toolchain / 'msys64'
|
|
self.msys = self.msys_root / 'ucrt64'
|
|
self.pkgs = self.toolchain / 'pkgs'
|
|
self.tools = self.app / 'tools'
|
|
self.make_game = self.tools / 'make_game.py'
|
|
self.lock_file = self.app / 'toolchain.lock.json'
|
|
self.feed_file = self.app / 'feed.json'
|
|
|
|
@property
|
|
def version(self):
|
|
v = self.app / 'VERSION'
|
|
return v.read_text(encoding='utf-8').strip() if v.is_file() else '0.0.0'
|
|
|
|
def default_game_dir(self):
|
|
"""<root>/game, or a Games folder on the fixed drive with the most free space when the root's drive has
|
|
less than 40 GB free or the root's path would break the build."""
|
|
here = self.root / 'game'
|
|
try:
|
|
import shutil
|
|
if shutil.disk_usage(self.root.anchor or self.root).free >= 40e9 and ansi_safe(here):
|
|
return here
|
|
except OSError:
|
|
return here
|
|
best = None
|
|
if os.name == 'nt':
|
|
import shutil
|
|
import string
|
|
for letter in string.ascii_uppercase[2:]:
|
|
drive = Path(f'{letter}:\\')
|
|
try:
|
|
free = shutil.disk_usage(drive).free
|
|
except OSError:
|
|
continue
|
|
if best is None or free > best[0]:
|
|
best = (free, drive / 'Games' / 'NX2navi')
|
|
return best[1] if best else here
|
|
|
|
def python_for_builds(self):
|
|
"""The console python.exe beside the launcher's pythonw.exe (no window: the build starts it with
|
|
CREATE_NO_WINDOW), or this interpreter in a checkout."""
|
|
exe = Path(sys.executable)
|
|
if exe.name.lower() == 'pythonw.exe' and (exe.parent / 'python.exe').is_file():
|
|
return str(exe.parent / 'python.exe')
|
|
return str(exe)
|
|
|
|
def ensure(self):
|
|
for d in (self.data, self.logs, self.cache, self.downloads):
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return self
|