mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Let the installer extract the Switch shared fonts from a firmware folder
The setup takes an optional --firmware directory (an unpacked firmware or an emulator's Contents/registered) and, for Chinese, Korean and Traditional Chinese, runs the vendored font extractor into <install>/shared-fonts. Without the folder it warns as before and the install still completes. The fonts step is keyed by the language and a cheap firmware fingerprint; the window gets a folder picker and help text, and the command line takes --firmware.
This commit is contained in:
1 parent
f469c5a774
commit
873ebb7755
14 files changed
+382
-60
No files matched your search
@@ -10,6 +10,7 @@ updates = ['C:\Games\TotK\update-1.4.3.nsp']
|
||||
keys = 'C:\Switch\prod.keys'
|
||||
# Optional: a firmware NCA directory (an unpacked firmware, or an emulator's installed registered/ directory).
|
||||
# tools/extract_fonts.py takes the system shared fonts from it; the game needs them only with Chinese or Korean text.
|
||||
# The installer asks for the same folder (--firmware) when the language needs those fonts.
|
||||
# firmware = 'C:\Switch\firmware'
|
||||
|
||||
[work]
|
||||
|
||||
+17
-8
@@ -11,6 +11,8 @@ holds only the game; the installer and its tools stay outside it.
|
||||
2. Picks:
|
||||
- the base game (XCI or NSP) and the 1.2.1 or 1.4.3 update (NSP);
|
||||
- their `prod.keys`;
|
||||
- optionally the firmware folder (`system/Contents/registered` of an unpacked firmware or an emulator's install),
|
||||
needed for Chinese, Korean and Traditional Chinese text;
|
||||
- the game language and the install folder.
|
||||
3. Waits for the setup, one time. The window shows each step and its progress.
|
||||
4. Plays `totk.exe` from the install folder. The installer and the source NSP or XCI can be deleted.
|
||||
@@ -22,6 +24,7 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
totk.exe, SDL3.dll, libopus-0.dll, totk_xlate.exe
|
||||
totk.ini language; the other keys as commented defaults (totk.ini.example)
|
||||
exefs/, romfs/ extracted from the player's files
|
||||
shared-fonts/ the Switch system fonts, with Chinese, Korean or Traditional Chinese text
|
||||
profile/ saves
|
||||
logs/ totk.log of the last run, totk.1.log to totk.4.log of the runs before it
|
||||
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
|
||||
@@ -31,8 +34,8 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
## Updating the port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the shaders. A newer
|
||||
release that changes nothing game-derived skips the rebuild.
|
||||
`shared-fonts/`, `profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild.
|
||||
|
||||
Given an update file other than the recorded one (1.2.1, then 1.4.3), the installer extracts and builds again from it and
|
||||
keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on load, and 1.2.1 loads the saves 1.4.3 wrote.
|
||||
@@ -43,19 +46,22 @@ keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on loa
|
||||
`tools/extract.py`), the keys are new enough for the update, enough space is free on the target disk (about 36 GiB at the peak of a
|
||||
first install, 20 GiB after it), the CPU has
|
||||
AVX2 (`-march=x86-64-v3`), a Vulkan 1.3 GPU exists. Each failure is one clear message.
|
||||
2. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
|
||||
3. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
2. **Fonts** with Chinese, Korean or Traditional Chinese text and a firmware folder: extract the Switch system fonts
|
||||
from the firmware into `shared-fonts/` (MacNativeGames' `extract_shared_fonts.py`, which verifies every NCA layer
|
||||
and BFTTF header). Without a firmware folder the setup warns and goes on; the game then lacks those glyphs.
|
||||
3. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
|
||||
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
|
||||
to the installer.
|
||||
4. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
|
||||
hashes and the game version's patch page) and link
|
||||
them with the prebuilt runtime library into `totk.exe` (`build_game.py --runtime-lib`; about 3 min on 24 cores).
|
||||
5. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
6. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
|
||||
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
|
||||
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
|
||||
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
|
||||
The patches are written twice: `romfs-patches` and `romfs-patches-xbox` (`--xbox-prompts`, the A/B and X/Y prompts
|
||||
swapped for `pad_layout = xbox_prompts`).
|
||||
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
|
||||
7. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
|
||||
|
||||
A cancelled or failed setup resumes from the last finished step (`totk-install.json`).
|
||||
|
||||
@@ -64,10 +70,13 @@ A cancelled or failed setup resumes from the last finished step (`totk-install.j
|
||||
`installer/` (stdlib only; `installer/layout.py` maps the release folder and, in a repository, the dev toolchain):
|
||||
|
||||
```
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR] [--verify]
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR] [--temp DIR] [--verify]
|
||||
```
|
||||
|
||||
- On a folder with `totk-install.json` only `--dest` is needed; the game files only when the extraction must run again.
|
||||
- `--firmware DIR` is the folder holding the firmware NCAs: an unpacked firmware, or the emulators' installed
|
||||
`Contents/registered` (the parent `Contents` folder works too). It is read only with Chinese, Korean or Traditional
|
||||
Chinese text, and only when `shared-fonts/` is not already in the install folder.
|
||||
- stdout: one JSON event per line, types `start`, `plan`, `step` (started, done, skipped), `progress` (step, phase,
|
||||
done/total/unit), `space` (bytes needed and free on the install disk and the temp disk), `warning`, `error`, `end` (`installer/events.py`). Messages are ids with parameters;
|
||||
`installer/text/en.json` holds their English text. The human-readable log goes to `<temp>/logs/`.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""The TotK installer: builds a playable folder from the player's own game files (docs/installer.md).
|
||||
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR]
|
||||
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR]
|
||||
[--temp DIR]
|
||||
|
||||
Modules: layout (where the bundled tools are, release or repository), events (JSON progress on stdout and the log
|
||||
file), state (totk-install.json), runner (which steps run: resume and update), steps (the six steps), ini (totk.ini),
|
||||
file), state (totk-install.json), runner (which steps run: resume and update), steps (the seven steps), ini (totk.ini),
|
||||
cli (the command line). text/en.json holds the English text of every message id the events carry.
|
||||
|
||||
Standard library only, apart from what the extraction tools import (cryptography). Runs on Python 3.14.
|
||||
|
||||
+10
-6
@@ -1,10 +1,12 @@
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR]
|
||||
[--temp DIR] [--verify]
|
||||
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
|
||||
[--root DIR] [--temp DIR] [--verify]
|
||||
|
||||
A first install needs every option. On a folder with totk-install.json (resume after a cancel or failure, or update
|
||||
with a newer release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed
|
||||
only when the extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed,
|
||||
2 bad command line, 3 cancelled (Ctrl+C or Ctrl+Break).
|
||||
A first install needs every option. --firmware names the Switch firmware folder (an unpacked firmware or an emulator's
|
||||
Contents/registered); the setup extracts the system shared fonts from it when the language is Chinese, Korean or
|
||||
Traditional Chinese. On a folder with totk-install.json (resume after a cancel or failure, or update with a newer
|
||||
release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed only when the
|
||||
extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed, 2 bad command
|
||||
line, 3 cancelled (Ctrl+C or Ctrl+Break).
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -48,6 +50,8 @@ def main(argv=None):
|
||||
ap.add_argument('--base', type=Path, help='base game, XCI or NSP')
|
||||
ap.add_argument('--update', type=Path, help='the update NSP: 1.1.1, 1.2.1 or 1.4.3')
|
||||
ap.add_argument('--keys', type=Path, help='prod.keys')
|
||||
ap.add_argument('--firmware', type=Path, help='Switch firmware folder (Contents/registered) for the shared fonts '
|
||||
'of Chinese, Korean and Traditional Chinese text')
|
||||
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
|
||||
ap.add_argument('--dest', type=Path, required=True, help='install folder')
|
||||
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
|
||||
|
||||
+23
-10
@@ -1,8 +1,9 @@
|
||||
"""The installer window: python -m installer.gui
|
||||
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the game language and the
|
||||
install folder, runs `python -m installer ...` as a subprocess and shows its JSON events. Decisions live in guilogic.py;
|
||||
this file only draws. The window text is in installer/text/<en|ru>.json under "ui.*" and "launcher.*" ids.
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
|
||||
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
|
||||
events. Decisions live in guilogic.py; this file only draws. The window text is in installer/text/<en|ru>.json under
|
||||
"ui.*" and "launcher.*" ids.
|
||||
|
||||
--selftest [--shots DIR] build every screen in both languages and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru window language (default: the Windows display language)
|
||||
@@ -51,7 +52,7 @@ class App:
|
||||
self.run_args = run_args or {} # extra CLI options for tests: temp, verify
|
||||
self.log_entries = [] # events and raw lines of the run, rendered by render_log
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'dest')}
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
|
||||
self.vars['lang'] = tk.StringVar()
|
||||
self.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
@@ -197,6 +198,10 @@ class App:
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
|
||||
@@ -206,6 +211,7 @@ class App:
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
@@ -266,7 +272,7 @@ class App:
|
||||
|
||||
def form(self):
|
||||
return Form(base=self.vars['base'].get(), update=self.vars['update'].get(), keys=self.vars['keys'].get(),
|
||||
lang=self.selected_lang(), dest=self.vars['dest'].get())
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get())
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
@@ -290,6 +296,11 @@ class App:
|
||||
if path:
|
||||
self.vars[name].set(os.path.normpath(path))
|
||||
|
||||
def pick_firmware(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_firmware'), mustexist=True)
|
||||
if path:
|
||||
self.vars['firmware'].set(os.path.normpath(path))
|
||||
|
||||
def pick_dest(self):
|
||||
path = filedialog.askdirectory(title=self.cat('ui.pick_dest'), mustexist=False)
|
||||
if path:
|
||||
@@ -306,7 +317,7 @@ class App:
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
command = form.command(self.layout.python, root=self.layout.root if self.layout.dev else None,
|
||||
temp=self.run_args.get('temp'), verify=self.run_args.get('verify', False))
|
||||
temp=self.run_args.get('temp'), verify_hashes=self.run_args.get('verify', False))
|
||||
self.begin(command, self.layout.root)
|
||||
|
||||
def begin(self, command, cwd):
|
||||
@@ -495,10 +506,10 @@ class App:
|
||||
# ---- self test ---------------------------------------------------------------------------------------------------
|
||||
|
||||
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/TotK', 'log': 'D:\\log.txt',
|
||||
'steps': ['check', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
SAMPLE_RUN = [
|
||||
SAMPLE_START,
|
||||
{'type': 'plan', 'run': ['check', 'extract'], 'skip': {}},
|
||||
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'progress', 'step': 'check', 'phase': 'inputs'},
|
||||
{'type': 'space', 'dest': 'Z:/selftest/TotK', 'need': 36_400_000_000, 'free': 210_000_000_000, 'temp': 'C:\\t',
|
||||
@@ -511,12 +522,14 @@ SAMPLE_RUN = [
|
||||
]
|
||||
|
||||
|
||||
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'lift': 'current', 'compile': 'current',
|
||||
'prepare': 'current', 'cleanup': 'not_needed'}},
|
||||
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'fonts': 'current', 'lift': 'current',
|
||||
'compile': 'current', 'prepare': 'current',
|
||||
'cleanup': 'not_needed'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'started'},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'done', 'seconds': 1.0},
|
||||
{'type': 'step', 'step': 'fonts', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'lift', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'compile', 'state': 'skipped', 'reason': 'current'},
|
||||
{'type': 'step', 'step': 'prepare', 'state': 'skipped', 'reason': 'current'},
|
||||
|
||||
@@ -25,7 +25,7 @@ from .events import render
|
||||
|
||||
TEXT = Path(__file__).resolve().parent / 'text'
|
||||
UI_LANGUAGES = ('en', 'ru')
|
||||
STEP_ORDER = ('check', 'extract', 'lift', 'compile', 'prepare', 'cleanup')
|
||||
STEP_ORDER = ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup')
|
||||
# message parameter -> catalog id prefix of the names that replace its raw values
|
||||
PARAM_NAMES = {'lang': 'lang.', 'known': 'lang.', 'step': 'step.', 'input': 'param.input.'}
|
||||
LANG_RU = 0x19 # primary language id of Russian in a Windows LANGID
|
||||
@@ -111,6 +111,7 @@ class Form:
|
||||
base: str = ''
|
||||
update: str = ''
|
||||
keys: str = ''
|
||||
firmware: str = ''
|
||||
lang: str = ''
|
||||
dest: str = ''
|
||||
|
||||
@@ -147,19 +148,21 @@ class Form:
|
||||
value = getattr(self, name).strip()
|
||||
if value and not Path(value).is_file():
|
||||
found.append(f'ui.missing_{name}')
|
||||
if self.firmware.strip() and not Path(self.firmware.strip()).is_dir():
|
||||
found.append('ui.missing_firmware')
|
||||
return found
|
||||
|
||||
def command(self, python, root=None, temp=None, verify=False):
|
||||
def command(self, python, root=None, temp=None, verify_hashes=False):
|
||||
cmd = [str(python), '-m', 'installer']
|
||||
for flag, value in (('--base', self.base), ('--update', self.update), ('--keys', self.keys),
|
||||
('--lang', self.lang), ('--dest', self.dest)):
|
||||
('--firmware', self.firmware), ('--lang', self.lang), ('--dest', self.dest)):
|
||||
if value.strip():
|
||||
cmd += [flag, value.strip()]
|
||||
if root:
|
||||
cmd += ['--root', str(root)]
|
||||
if temp:
|
||||
cmd += ['--temp', str(temp)]
|
||||
if verify:
|
||||
if verify_hashes:
|
||||
cmd.append('--verify')
|
||||
return cmd
|
||||
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@ LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419',
|
||||
'zh-Hans', 'zh-Hant')
|
||||
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
|
||||
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the installer does not extract.
|
||||
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the fonts step extracts from the
|
||||
# firmware folder when the player gives one.
|
||||
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
|
||||
|
||||
ACTIVE = re.compile(r'^(\s*)(?:totk_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
|
||||
|
||||
@@ -81,6 +81,7 @@ class Layout:
|
||||
"""Paths the steps need that do not exist."""
|
||||
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
|
||||
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
|
||||
self.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py',
|
||||
self.recomp / 'gen.py',
|
||||
self.clang,
|
||||
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, self.runtime_lib, self.ninja, self.hactool,
|
||||
|
||||
+160
-17
@@ -1,13 +1,14 @@
|
||||
"""The six setup steps of docs/installer.md: check, extract, lift, compile, prepare, cleanup.
|
||||
"""The seven setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, cleanup.
|
||||
|
||||
Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); lift,
|
||||
compile and prepare run the bundled Python on recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and
|
||||
tools/prepare_shaders.py, whose output goes to <temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call
|
||||
here: the host check is a small C program built with the bundled clang (hostcheck.c).
|
||||
Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); fonts
|
||||
runs the vendored extract_shared_fonts.py on the firmware folder; lift, compile and prepare run the bundled Python on
|
||||
recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepare_shaders.py, whose output goes to
|
||||
<temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call here: the host check is a small C program built
|
||||
with the bundled clang (hostcheck.c).
|
||||
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, totk.exe and its DLLs, romfs-index.bin,
|
||||
romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini,
|
||||
totk-install.json.
|
||||
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
|
||||
and its DLLs, romfs-index.bin, romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of
|
||||
totk.exe --prepare-shaders, totk.ini, totk-install.json.
|
||||
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), gen/ (C), build/ (objects),
|
||||
logs/ (kept by the cleanup).
|
||||
"""
|
||||
@@ -217,6 +218,64 @@ def size_of(paths):
|
||||
return sum(p.stat().st_size for p in paths if p.is_file())
|
||||
|
||||
|
||||
# The Switch system shared fonts (runtime/ov_system.c): TotK falls back to them for Chinese, Korean and Traditional
|
||||
# Chinese text. tools/vendor/macnativegames/extract_shared_fonts.py writes these names.
|
||||
SHARED_FONTS = 'shared-fonts'
|
||||
SHARED_FONT_FILES = ('0-standard.bfttf', '1-chinese-simplified.bfttf', '2-chinese-simplified-extension.bfttf',
|
||||
'3-chinese-traditional.bfttf', '4-korean.bfttf', '5-nintendo-extension.bfttf')
|
||||
FONTS_NEED = 32 << 20 # the six BFTTF files and their manifest
|
||||
|
||||
|
||||
def requested_lang(ctx):
|
||||
"""The language this run uses: --lang, else the one recorded for the install (Check canonicalizes it)."""
|
||||
return ini.canonical_language(getattr(ctx.options, 'lang', None) or ctx.state.get('lang'))
|
||||
|
||||
|
||||
def firmware_dir(value):
|
||||
"""The firmware NCA folder behind a --firmware path: the folder itself, or its registered/ child (the emulators'
|
||||
layout, Contents/registered)."""
|
||||
if not value:
|
||||
return None
|
||||
path = Path(value)
|
||||
return path / 'registered' if (path / 'registered').is_dir() else path
|
||||
|
||||
|
||||
def firmware_has_ncas(folder):
|
||||
"""Whether folder holds firmware NCAs: .nca files, or the hashed directories with a 00 file (registered/)."""
|
||||
try:
|
||||
entries = list(Path(folder).iterdir())
|
||||
except OSError:
|
||||
return False
|
||||
return any((e.is_file() and e.suffix.lower() == '.nca') or (e.is_dir() and (e / '00').is_file()) for e in entries)
|
||||
|
||||
|
||||
def firmware_stamp(folder):
|
||||
"""A cheap identity of the firmware NCAs: names, sizes and mtimes, not their contents (the folder is ~326 MB)."""
|
||||
try:
|
||||
entries = sorted(Path(folder).iterdir())
|
||||
except OSError:
|
||||
return 'missing'
|
||||
rows = []
|
||||
for entry in entries:
|
||||
if entry.is_dir():
|
||||
file = entry / '00'
|
||||
if not file.is_file():
|
||||
continue
|
||||
elif not entry.is_file() or entry.suffix.lower() != '.nca':
|
||||
continue
|
||||
else:
|
||||
file = entry
|
||||
st = file.stat()
|
||||
rows.append((entry.name, st.st_size, st.st_mtime_ns))
|
||||
return rows
|
||||
|
||||
|
||||
def shared_fonts_ready(dest):
|
||||
"""Whether dest/shared-fonts holds a complete set that the game can load."""
|
||||
folder = Path(dest) / SHARED_FONTS
|
||||
return (folder / 'manifest.json').is_file() and all((folder / name).is_file() for name in SHARED_FONT_FILES)
|
||||
|
||||
|
||||
# ---- 1. check
|
||||
|
||||
class Check(Step):
|
||||
@@ -233,13 +292,30 @@ class Check(Step):
|
||||
own = (ctx.temp, *ctx.temp.parents) # a temp folder inside the install folder (installer/cli.py) is ours
|
||||
if ctx.mode == 'install' and dest.is_dir() and any(p not in own for p in dest.iterdir()):
|
||||
raise InstallError('check.dest_not_empty', path=str(dest))
|
||||
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
|
||||
lang = requested_lang(ctx)
|
||||
if not lang:
|
||||
raise InstallError('check.lang_missing')
|
||||
if lang not in ini.LANGUAGES:
|
||||
raise InstallError('check.lang_unknown', lang=lang, known=', '.join(ini.LANGUAGES))
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is not None:
|
||||
if not firmware.is_dir():
|
||||
raise InstallError('check.firmware_missing', path=str(firmware))
|
||||
if not firmware_has_ncas(firmware):
|
||||
raise InstallError('check.firmware_empty', path=str(firmware))
|
||||
if lang in ini.NEED_SHARED_FONTS:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
if firmware is None:
|
||||
rep.warning('warn.lang_needs_fonts', lang=lang)
|
||||
elif not shared_fonts_ready(dest):
|
||||
keys = getattr(ctx.options, 'keys', None)
|
||||
if not keys or not Path(keys).is_file():
|
||||
raise InstallError('check.fonts_keys_needed')
|
||||
try:
|
||||
key_values = load_keys(Path(keys))
|
||||
except (ValueError, OSError, UnicodeDecodeError):
|
||||
raise InstallError('check.keys_unreadable') from None
|
||||
if 'header_key' not in key_values:
|
||||
raise InstallError('check.keys_no_header_key')
|
||||
ctx.lang = lang
|
||||
ctx.temp.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -368,6 +444,8 @@ class Check(Step):
|
||||
romfs = ctx.dest / 'romfs' / 'romfs.bin'
|
||||
if romfs.is_file(): # a resumed extraction replaces it
|
||||
dest_need -= romfs.stat().st_size
|
||||
if ctx.will_run('fonts'):
|
||||
dest_need += FONTS_NEED
|
||||
if ctx.will_run('prepare') or ctx.will_run('extract'):
|
||||
dest_need += DEST_EXTRA
|
||||
if ctx.will_run('lift') or ctx.will_run('compile'):
|
||||
@@ -419,7 +497,72 @@ class Check(Step):
|
||||
ctx.state['version'] = found
|
||||
|
||||
|
||||
# ---- 2. extract
|
||||
# ---- 2. fonts
|
||||
|
||||
class Fonts(Step):
|
||||
"""The Switch system fonts for Chinese, Korean and Traditional Chinese text (tools/extract_fonts.py, which runs the
|
||||
vendored extract_shared_fonts.py). The firmware folder is optional: without it the setup warns and the game runs
|
||||
with those glyphs missing. The extraction verifies the firmware NCAs and writes the six BFTTF files the console
|
||||
puts in shared memory."""
|
||||
|
||||
id = 'fonts'
|
||||
|
||||
def key(self, ctx):
|
||||
lang = requested_lang(ctx)
|
||||
if lang not in ini.NEED_SHARED_FONTS:
|
||||
return fingerprint('fonts-1', 'not-needed')
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is None:
|
||||
return fingerprint('fonts-1', 'no-firmware')
|
||||
return fingerprint('fonts-1', lang, str(firmware), firmware_stamp(firmware))
|
||||
|
||||
def current(self, ctx, record):
|
||||
if requested_lang(ctx) not in ini.NEED_SHARED_FONTS or not getattr(ctx.options, 'firmware', None):
|
||||
return True
|
||||
return shared_fonts_ready(ctx.dest)
|
||||
|
||||
def verify(self, ctx, record):
|
||||
# An update run may omit --firmware; the installed set still has to be there for these languages.
|
||||
if requested_lang(ctx) in ini.NEED_SHARED_FONTS:
|
||||
return shared_fonts_ready(ctx.dest)
|
||||
return True
|
||||
|
||||
def run(self, ctx):
|
||||
lang = getattr(ctx, 'lang', None) or requested_lang(ctx)
|
||||
if lang not in ini.NEED_SHARED_FONTS:
|
||||
return {}
|
||||
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
|
||||
if firmware is None:
|
||||
return {} # Check warned: the game runs, some text may not show
|
||||
keys = getattr(ctx.options, 'keys', None)
|
||||
if not keys:
|
||||
raise InstallError('check.fonts_keys_needed')
|
||||
out = ctx.temp / 'shared-fonts'
|
||||
remove(out)
|
||||
ctx.reporter.progress('fonts', 'extract', force=True)
|
||||
tool = ctx.layout.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py'
|
||||
code, log = run_tool(ctx, 'fonts', tool, ['--keys', keys, '--firmware', firmware, '--output', out])
|
||||
if code:
|
||||
detail = ''
|
||||
try:
|
||||
detail = log.read_text(encoding='utf-8', errors='replace').strip().splitlines()[-1][:300]
|
||||
except (OSError, IndexError):
|
||||
pass
|
||||
raise InstallError('fonts.failed', code=code, detail=detail, log=str(log))
|
||||
missing = [name for name in SHARED_FONT_FILES if not (out / name).is_file()]
|
||||
if missing or not (out / 'manifest.json').is_file():
|
||||
raise InstallError('fonts.incomplete', files=', '.join(missing) or 'manifest.json')
|
||||
ctx.reporter.progress('fonts', 'copy', force=True)
|
||||
try:
|
||||
ctx.dest.mkdir(parents=True, exist_ok=True)
|
||||
remove(ctx.dest / SHARED_FONTS)
|
||||
shutil.move(str(out), str(ctx.dest / SHARED_FONTS))
|
||||
except OSError as e:
|
||||
raise InstallError('fonts.copy_failed', path=str(ctx.dest / SHARED_FONTS), detail=str(e)) from None
|
||||
return {'files': len(SHARED_FONT_FILES)}
|
||||
|
||||
|
||||
# ---- 3. extract
|
||||
|
||||
class Extract(Step):
|
||||
id = 'extract'
|
||||
@@ -527,7 +670,7 @@ class Extract(Step):
|
||||
watch.stop()
|
||||
|
||||
|
||||
# ---- 3. lift
|
||||
# ---- 4. lift
|
||||
|
||||
EMITTED = re.compile(r'^emitted (\d+)/(\d+) chunks')
|
||||
DISCOVERED = re.compile(r'^(\w+): \d+ functions \(.*discovery')
|
||||
@@ -563,7 +706,7 @@ class Lift(Step):
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 4. compile
|
||||
# ---- 5. compile
|
||||
|
||||
NINJA_LINE = re.compile(r'^\[(\d+)/(\d+)\] ')
|
||||
|
||||
@@ -620,7 +763,7 @@ class Compile(Step):
|
||||
return {'exe_sha256': file_sha256(dest / exe.name)}
|
||||
|
||||
|
||||
# ---- 5. prepare
|
||||
# ---- 6. prepare
|
||||
|
||||
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
|
||||
|
||||
@@ -718,7 +861,7 @@ class Prepare(Step):
|
||||
return {'xlate_sha256': file_sha256(xlate)}
|
||||
|
||||
|
||||
# ---- 6. clean up
|
||||
# ---- 7. clean up
|
||||
|
||||
class Cleanup(Step):
|
||||
id = 'cleanup'
|
||||
@@ -736,7 +879,7 @@ class Cleanup(Step):
|
||||
rep.progress('cleanup', 'temp', force=True)
|
||||
for stale in dest.glob('update-merge-*'):
|
||||
remove(stale)
|
||||
for name in ('gen', 'build', 'check', 'shader_list.tsv'):
|
||||
for name in ('gen', 'build', 'check', 'shader_list.tsv', 'shared-fonts'):
|
||||
remove(ctx.temp / name)
|
||||
romfs_problem(dest / 'romfs', expected_update(ctx))
|
||||
return {}
|
||||
@@ -758,4 +901,4 @@ def romfs_problem(romfs, expected):
|
||||
|
||||
|
||||
def all_steps():
|
||||
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"step.check": "Checking the computer and the game files",
|
||||
"step.fonts": "Extracting the system fonts",
|
||||
"step.extract": "Extracting the game",
|
||||
"step.lift": "Translating the game code to C",
|
||||
"step.compile": "Compiling {game_exe}",
|
||||
@@ -11,6 +12,8 @@
|
||||
"phase.check.verify_romfs": "Checking the installed game data",
|
||||
"phase.check.inputs": "Opening the game files with the keys",
|
||||
"phase.check.versions": "Checking the game versions",
|
||||
"phase.fonts.extract": "Extracting the shared fonts from the firmware",
|
||||
"phase.fonts.copy": "Copying the shared fonts into the install folder",
|
||||
"phase.extract.hash_base": "Computing the checksum of the base game",
|
||||
"phase.extract.hash_update": "Computing the checksum of the update",
|
||||
"phase.extract.exefs": "Extracting the game code",
|
||||
@@ -67,10 +70,16 @@
|
||||
"check.base_wrong_version": "The base game is not version 1.0.0 ({modules} differ).",
|
||||
"check.update_wrong_version": "The update is version {found}; this release takes {wanted}.",
|
||||
"check.disk_space": "Not enough free space for {path}: {need_gb} GB needed, {free_gb} GB free.",
|
||||
"check.firmware_missing": "The firmware folder {path} does not exist. Choose the firmware's Contents/registered folder.",
|
||||
"check.firmware_empty": "The firmware folder {path} holds no NCA files. Choose the firmware's Contents/registered folder.",
|
||||
"check.fonts_keys_needed": "The shared fonts for this language are not installed yet; choose prod.keys so the setup can extract them from the firmware.",
|
||||
|
||||
"extract.exefs_mismatch": "The extracted game code does not match version {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Applying the update to the game data failed: {detail}. Details: {log}",
|
||||
"extract.romfs_mismatch": "The extracted game data does not match version {version} (size {size}, checksum {sha256}...).",
|
||||
"fonts.failed": "Extracting the shared fonts failed (exit code {code}): {detail}. Details: {log}",
|
||||
"fonts.incomplete": "The shared font extraction did not write every file ({files}).",
|
||||
"fonts.copy_failed": "The shared fonts cannot be copied into {path}: {detail}",
|
||||
"lift.failed": "Translating the game code failed (exit code {code}). Details: {log}",
|
||||
"compile.failed": "Compiling {game_exe} failed (exit code {code}). Details: {log}",
|
||||
"compile.exe_in_use": "{path} is in use. Close the game and run the installer again.",
|
||||
@@ -81,7 +90,7 @@
|
||||
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
|
||||
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
|
||||
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts. Choose the firmware folder (Contents/registered) to install them; without it some text may not show.",
|
||||
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
|
||||
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
|
||||
"warn.kernel_old": "Linux {version} lacks the write tracking the game relies on (6.7 or later); the game may run very slowly. Updating the system fixes this.",
|
||||
@@ -94,13 +103,16 @@
|
||||
"ui.base": "Base game (NSP or XCI)",
|
||||
"ui.update": "Update 1.1.1 or 1.2.1 or 1.4.3 (NSP)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Firmware folder (optional)",
|
||||
"ui.game_language": "Game language",
|
||||
"ui.dest": "Install folder",
|
||||
"ui.browse": "Browse...",
|
||||
"ui.pick_game": "Choose the base game file",
|
||||
"ui.pick_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file",
|
||||
"ui.pick_keys": "Choose prod.keys",
|
||||
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
|
||||
"ui.pick_dest": "Choose the install folder",
|
||||
"ui.firmware_help": "For Chinese, Korean and Traditional Chinese text: the firmware's Contents/registered folder, for example from Ryujinx or Yuzu. The setup copies the Switch system fonts from it into the install folder.",
|
||||
"ui.type_game": "Game files (NSP, XCI)",
|
||||
"ui.type_nsp": "NSP files",
|
||||
"ui.type_keys": "Key files (*.keys)",
|
||||
@@ -122,6 +134,7 @@
|
||||
"ui.missing_base": "The base game file does not exist.",
|
||||
"ui.missing_update": "The update file does not exist.",
|
||||
"ui.missing_keys": "The keys file does not exist.",
|
||||
"ui.missing_firmware": "The firmware folder does not exist.",
|
||||
"ui.running_install": "Installing",
|
||||
"ui.running_resume": "Continuing the earlier setup",
|
||||
"ui.running_update": "Updating the installation",
|
||||
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"step.check": "Проверка компьютера и игровых файлов",
|
||||
"step.fonts": "Извлечение системных шрифтов",
|
||||
"step.extract": "Распаковка игры",
|
||||
"step.lift": "Перевод кода игры в C",
|
||||
"step.compile": "Сборка {game_exe}",
|
||||
@@ -11,6 +12,8 @@
|
||||
"phase.check.verify_romfs": "Проверка установленных данных игры",
|
||||
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
|
||||
"phase.check.versions": "Проверка версий игры",
|
||||
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
|
||||
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
|
||||
"phase.extract.hash_base": "Подсчёт контрольной суммы базовой игры",
|
||||
"phase.extract.hash_update": "Подсчёт контрольной суммы обновления",
|
||||
"phase.extract.exefs": "Распаковка кода игры",
|
||||
@@ -67,10 +70,16 @@
|
||||
"check.base_wrong_version": "Базовая игра не версии 1.0.0 (отличаются модули: {modules}).",
|
||||
"check.update_wrong_version": "Версия обновления {found}; этот выпуск принимает {wanted}.",
|
||||
"check.disk_space": "Не хватает свободного места для {path}: нужно {need_gb} ГБ, свободно {free_gb} ГБ.",
|
||||
"check.firmware_missing": "Папки прошивки {path} нет. Выберите папку Contents/registered прошивки.",
|
||||
"check.firmware_empty": "В папке прошивки {path} нет файлов NCA. Выберите папку Contents/registered прошивки.",
|
||||
"check.fonts_keys_needed": "Общие шрифты для этого языка ещё не установлены; выберите prod.keys, чтобы установка извлекла их из прошивки.",
|
||||
|
||||
"extract.exefs_mismatch": "Распакованный код игры не соответствует версии {version} ({modules}).",
|
||||
"extract.romfs_merge_failed": "Не удалось применить обновление к данным игры: {detail}. Подробности: {log}",
|
||||
"extract.romfs_mismatch": "Распакованные данные игры не соответствуют версии {version} (размер {size}, контрольная сумма {sha256}...).",
|
||||
"fonts.failed": "Не удалось извлечь общие шрифты (код выхода {code}): {detail}. Подробности: {log}",
|
||||
"fonts.incomplete": "При извлечении общих шрифтов записаны не все файлы ({files}).",
|
||||
"fonts.copy_failed": "Не удаётся скопировать общие шрифты в {path}: {detail}",
|
||||
"lift.failed": "Не удалось перевести код игры (код выхода {code}). Подробности: {log}",
|
||||
"compile.failed": "Не удалось собрать {game_exe} (код выхода {code}). Подробности: {log}",
|
||||
"compile.exe_in_use": "Файл {path} занят. Закройте игру и запустите установку снова.",
|
||||
@@ -81,7 +90,7 @@
|
||||
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
|
||||
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
|
||||
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
|
||||
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch. Выберите папку прошивки (Contents/registered), чтобы установить их; без неё часть текста может не отображаться.",
|
||||
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
|
||||
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
|
||||
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
|
||||
@@ -94,13 +103,16 @@
|
||||
"ui.base": "Базовая игра (NSP или XCI)",
|
||||
"ui.update": "Обновление 1.1.1 или 1.2.1 или 1.4.3 (NSP)",
|
||||
"ui.keys": "prod.keys",
|
||||
"ui.firmware": "Папка прошивки (необязательно)",
|
||||
"ui.game_language": "Язык игры",
|
||||
"ui.dest": "Папка установки",
|
||||
"ui.browse": "Обзор...",
|
||||
"ui.pick_game": "Выберите файл базовой игры",
|
||||
"ui.pick_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3",
|
||||
"ui.pick_keys": "Выберите prod.keys",
|
||||
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
|
||||
"ui.pick_dest": "Выберите папку установки",
|
||||
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
|
||||
"ui.type_game": "Файлы игры (NSP, XCI)",
|
||||
"ui.type_nsp": "Файлы NSP",
|
||||
"ui.type_keys": "Файлы ключей (*.keys)",
|
||||
@@ -122,6 +134,7 @@
|
||||
"ui.missing_base": "Файл базовой игры не найден.",
|
||||
"ui.missing_update": "Файл обновления не найден.",
|
||||
"ui.missing_keys": "Файл ключей не найден.",
|
||||
"ui.missing_firmware": "Папки прошивки нет.",
|
||||
"ui.running_install": "Установка",
|
||||
"ui.running_resume": "Продолжение прерванной установки",
|
||||
"ui.running_update": "Обновление установки",
|
||||
|
||||
+25
-10
@@ -30,7 +30,7 @@ from installer import layout as layouts # noqa: E402
|
||||
from installer import state as statefile # noqa: E402
|
||||
from installer.events import Reporter # noqa: E402
|
||||
from installer.runner import Context # noqa: E402
|
||||
from installer.steps import all_steps # noqa: E402
|
||||
from installer.steps import Fonts, all_steps # noqa: E402
|
||||
from installer.guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, Catalog, Form, Model, Process, # noqa: E402
|
||||
amount, describe_event, parse_line, windows_ui_language)
|
||||
|
||||
@@ -38,7 +38,7 @@ INSTALLED = ROOT / 'build' / 'install-test'
|
||||
FIXTURE = ROOT / 'build' / 'inst-gui' / 'e2e-install'
|
||||
COPIED = ('totk.ini', statefile.NAME) # written by a run, so never linked
|
||||
START = {'type': 'start', 'release': 'r1', 'mode': 'install', 'dest': 'D:\\T', 'log': 'D:\\T.log',
|
||||
'steps': ['check', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
|
||||
|
||||
|
||||
def fold(*events):
|
||||
@@ -66,7 +66,7 @@ class ModelTest(unittest.TestCase):
|
||||
m = fold(START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'lift': 'current', 'compile': 'current'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'})
|
||||
self.assertEqual([m.states[s] for s in m.steps],
|
||||
[RUNNING, PENDING, SKIPPED, SKIPPED, PENDING, PENDING])
|
||||
[RUNNING, PENDING, PENDING, SKIPPED, SKIPPED, PENDING, PENDING])
|
||||
self.assertEqual(m.current, 'check')
|
||||
m.apply({'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 1.0})
|
||||
self.assertEqual(m.states['check'], DONE)
|
||||
@@ -137,10 +137,12 @@ class ModelTest(unittest.TestCase):
|
||||
|
||||
|
||||
class RunStatesTest(unittest.TestCase):
|
||||
OK = [START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'lift': 'current', 'compile': 'current',
|
||||
'prepare': 'current', 'cleanup': 'not_needed'}},
|
||||
OK = [START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'fonts': 'current', 'lift': 'current',
|
||||
'compile': 'current', 'prepare': 'current',
|
||||
'cleanup': 'not_needed'}},
|
||||
{'type': 'step', 'step': 'check', 'state': 'started'}, {'type': 'step', 'step': 'check', 'state': 'done'},
|
||||
{'type': 'step', 'step': 'extract', 'state': 'started'}, {'type': 'step', 'step': 'extract', 'state': 'done'},
|
||||
{'type': 'step', 'step': 'fonts', 'state': 'skipped'},
|
||||
{'type': 'step', 'step': 'lift', 'state': 'skipped'}, {'type': 'step', 'step': 'compile', 'state': 'skipped'},
|
||||
{'type': 'step', 'step': 'prepare', 'state': 'skipped'}, {'type': 'step', 'step': 'cleanup', 'state': 'done'},
|
||||
{'type': 'end', 'result': 'ok', 'seconds': 1}]
|
||||
@@ -200,11 +202,20 @@ class FormTest(unittest.TestCase):
|
||||
def test_unknown_language(self):
|
||||
self.assertEqual(self.form(lang='xx').problems(), ['ui.need_lang'])
|
||||
|
||||
def test_firmware_folder(self):
|
||||
firmware = self.dir / 'firmware'
|
||||
firmware.mkdir()
|
||||
self.assertEqual(self.form(firmware=str(firmware)).problems(), [])
|
||||
self.assertEqual(self.form(firmware=str(self.dir / 'no-firmware')).problems(), ['ui.missing_firmware'])
|
||||
|
||||
def test_command(self):
|
||||
cmd = self.form().command('py.exe', root=ROOT, temp=self.dir / 't', verify=True)
|
||||
firmware = self.dir / 'firmware'
|
||||
firmware.mkdir()
|
||||
cmd = self.form(firmware=str(firmware)).command('py.exe', root=ROOT, temp=self.dir / 't', verify_hashes=True)
|
||||
self.assertEqual(cmd[:3], ['py.exe', '-m', 'installer'])
|
||||
for flag in ('--base', '--update', '--keys', '--lang', '--dest', '--root', '--temp', '--verify'):
|
||||
for flag in ('--base', '--update', '--keys', '--firmware', '--lang', '--dest', '--root', '--temp', '--verify'):
|
||||
self.assertIn(flag, cmd)
|
||||
self.assertEqual(cmd[cmd.index('--firmware') + 1], str(firmware))
|
||||
self.assertEqual(cmd[cmd.index('--lang') + 1], 'en-US')
|
||||
|
||||
def test_installed_folder_needs_only_itself(self):
|
||||
@@ -244,7 +255,7 @@ class CatalogTest(unittest.TestCase):
|
||||
cat = Catalog('en')
|
||||
ids = ['ui.' + n for n in ('title', 'start', 'cancel', 'open_folder', 'notice', 'space_one', 'space_two',
|
||||
'process_died', 'result_ok', 'result_failed', 'result_cancelled')]
|
||||
ids += [f'step.{s}' for s in ('check', 'extract', 'lift', 'compile', 'prepare', 'cleanup')]
|
||||
ids += [f'step.{s}' for s in ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup')]
|
||||
ids += ['ui.state_' + n for n in ('started', 'done', 'skipped', 'pending', 'running', 'failed')]
|
||||
for lang in ('en', 'ru'):
|
||||
cat.set_language(lang)
|
||||
@@ -334,6 +345,10 @@ def make_fixture():
|
||||
ctx = Context(layout, FIXTURE, ROOT / 'build' / 'inst-gui' / 'e2e-temp', Reporter(out=io.StringIO()), options, state=state)
|
||||
ctx.steps = all_steps()
|
||||
ctx.plan()
|
||||
# An install made before the fonts step has no record for it: record it as current when its output is (the
|
||||
# languages that need the fonts have them in shared-fonts/).
|
||||
if Fonts().current(ctx, {}):
|
||||
state['steps'].setdefault('fonts', {'release': state.get('release', ''), 'key': ctx.keys['fonts']})
|
||||
for step_id, record in state['steps'].items():
|
||||
record['key'] = ctx.keys[step_id]
|
||||
statefile.save(FIXTURE, state)
|
||||
@@ -346,7 +361,7 @@ class EndToEndTest(unittest.TestCase):
|
||||
def run_cli(form, temp, tries=1):
|
||||
"""Runs the CLI in --verify mode through Process, up to `tries` times while the RomFS hash reads wrong."""
|
||||
for attempt in range(tries):
|
||||
p = Process(form.command(sys.executable, root=ROOT, temp=temp, verify=True), cwd=ROOT)
|
||||
p = Process(form.command(sys.executable, root=ROOT, temp=temp, verify_hashes=True), cwd=ROOT)
|
||||
model, stray = Model(), []
|
||||
t0, code = time.monotonic(), None
|
||||
while code is None and time.monotonic() - t0 < 600:
|
||||
@@ -384,7 +399,7 @@ class EndToEndTest(unittest.TestCase):
|
||||
|
||||
def test_cancel_during_the_check(self):
|
||||
cmd = Form(dest=str(make_fixture())).command(sys.executable, root=ROOT, temp=ROOT / 'build' / 'inst-gui' / 'e2e-temp',
|
||||
verify=True)
|
||||
verify_hashes=True)
|
||||
p = Process(cmd, cwd=ROOT)
|
||||
model, t0, code, cancelled = Model(), time.monotonic(), None, False
|
||||
while code is None and time.monotonic() - t0 < 120:
|
||||
|
||||
@@ -291,6 +291,108 @@ class CleanupTest(unittest.TestCase):
|
||||
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
|
||||
|
||||
|
||||
class FontsTest(unittest.TestCase):
|
||||
"""The fonts step: keyed by language and firmware, a no-op without firmware, writes dest/shared-fonts."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
base = Path(self.tmp.name)
|
||||
self.dest, temp, self.firmware = base / 'dest', base / 'temp', base / 'firmware'
|
||||
temp.mkdir()
|
||||
self.firmware.mkdir()
|
||||
(self.firmware / 'a.nca').write_bytes(b'nca')
|
||||
self.keys = base / 'prod.keys'
|
||||
self.keys.write_text('header_key = 00\n')
|
||||
self.ctx = SimpleNamespace(dest=self.dest, temp=temp, lang='zh-Hans', state={},
|
||||
layout=SimpleNamespace(tools=ROOT / 'tools', root=ROOT),
|
||||
reporter=Reporter(out=io.StringIO()),
|
||||
options=SimpleNamespace(lang='zh-Hans', firmware=self.firmware, keys=str(self.keys)))
|
||||
self.saved_run_tool = steps.run_tool
|
||||
|
||||
def tearDown(self):
|
||||
steps.run_tool = self.saved_run_tool
|
||||
self.ctx.reporter.close()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def fake_tool(self, files=None, code=0):
|
||||
"""Stand-in for extract_shared_fonts.py: writes the requested files where --output points."""
|
||||
def run(ctx, step, script, args):
|
||||
out = Path(args[args.index('--output') + 1])
|
||||
out.mkdir(parents=True)
|
||||
for name in steps.SHARED_FONT_FILES if files is None else files:
|
||||
(out / name).write_bytes(b'bfttf')
|
||||
(out / 'manifest.json').write_text('{}')
|
||||
log = ctx.temp / 'fonts.log'
|
||||
log.write_text('error line\n')
|
||||
return code, log
|
||||
steps.run_tool = run
|
||||
|
||||
def test_all_steps_order(self):
|
||||
self.assertEqual([s.id for s in steps.all_steps()],
|
||||
['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup'])
|
||||
|
||||
def test_extracts_into_the_install_folder(self):
|
||||
self.fake_tool()
|
||||
self.assertEqual(steps.Fonts().run(self.ctx), {'files': 6})
|
||||
self.assertTrue(steps.shared_fonts_ready(self.dest))
|
||||
self.assertEqual(sorted(p.name for p in (self.dest / 'shared-fonts').iterdir()),
|
||||
sorted((*steps.SHARED_FONT_FILES, 'manifest.json')))
|
||||
|
||||
def test_noop_without_firmware(self):
|
||||
self.ctx.options.firmware = None
|
||||
self.fake_tool()
|
||||
self.assertEqual(steps.Fonts().run(self.ctx), {})
|
||||
self.assertFalse((self.dest / 'shared-fonts').exists())
|
||||
|
||||
def test_noop_for_other_languages(self):
|
||||
self.ctx.lang, self.ctx.options.lang = 'en-US', 'en-US'
|
||||
self.fake_tool()
|
||||
self.assertEqual(steps.Fonts().run(self.ctx), {})
|
||||
self.assertFalse((self.dest / 'shared-fonts').exists())
|
||||
|
||||
def test_failed_extraction_keeps_the_old_folder(self):
|
||||
self.fake_tool()
|
||||
steps.Fonts().run(self.ctx)
|
||||
self.fake_tool(code=1)
|
||||
with self.assertRaises(steps.InstallError) as e:
|
||||
steps.Fonts().run(self.ctx)
|
||||
self.assertEqual(e.exception.id, 'fonts.failed')
|
||||
self.assertTrue(steps.shared_fonts_ready(self.dest))
|
||||
|
||||
def test_incomplete_output_is_an_error(self):
|
||||
self.fake_tool(files=steps.SHARED_FONT_FILES[:1])
|
||||
with self.assertRaises(steps.InstallError) as e:
|
||||
steps.Fonts().run(self.ctx)
|
||||
self.assertEqual(e.exception.id, 'fonts.incomplete')
|
||||
|
||||
def test_current_tracks_language_and_output(self):
|
||||
fonts = steps.Fonts()
|
||||
self.assertFalse(fonts.current(self.ctx, {}))
|
||||
self.fake_tool()
|
||||
fonts.run(self.ctx)
|
||||
self.assertTrue(fonts.current(self.ctx, {}))
|
||||
self.ctx.options.lang = 'en-US'
|
||||
self.assertTrue(fonts.current(self.ctx, {}))
|
||||
self.ctx.options.lang = 'zh-Hans'
|
||||
(self.dest / 'shared-fonts' / '0-standard.bfttf').unlink()
|
||||
self.assertFalse(fonts.current(self.ctx, {}))
|
||||
|
||||
def test_key_changes_with_language_and_firmware(self):
|
||||
fonts = steps.Fonts()
|
||||
zh = fonts.key(self.ctx)
|
||||
self.ctx.options.lang = 'en-US'
|
||||
self.assertNotEqual(zh, fonts.key(self.ctx))
|
||||
self.ctx.options.lang = 'zh-Hans'
|
||||
(self.firmware / 'b.nca').write_bytes(b'other')
|
||||
self.assertNotEqual(zh, fonts.key(self.ctx))
|
||||
|
||||
def test_registered_child_is_used(self):
|
||||
nested = self.firmware / 'registered'
|
||||
nested.mkdir()
|
||||
(nested / 'c.nca').write_bytes(b'nca')
|
||||
self.assertEqual(steps.firmware_dir(self.firmware), nested)
|
||||
|
||||
|
||||
class EventsTest(unittest.TestCase):
|
||||
def test_lines_are_json_and_the_log_is_english(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
[files]
|
||||
# Folder with RomFS mod files that replace or add game files. Default: none.
|
||||
# romfs_mods =
|
||||
# Folder with the Switch shared fonts (BFTTF files, tools/extract_fonts.py output), for Chinese and Korean text.
|
||||
# Default: shared-fonts next to totk.exe. TOTK_SHARED_FONTS overrides it.
|
||||
# shared_fonts =
|
||||
|
||||
[diagnostics]
|
||||
# Extra lines in the log, comma separated: svc, host, ipc, fs, gfx, perf, sched, audio, present or all. The menu's
|
||||
|
||||
Reference in new issue
Block a user