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:
turquopool committed 2026-11-01 15:22:14 +08:00
1 parent f469c5a774
commit 873ebb7755
14 files changed
+382 -60

No files matched your search

+1
View File
@@ -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
View File
@@ -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/`.
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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'},
+7 -4
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+102
View File
@@ -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:
+3
View File
@@ -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