Files
Valiantsin DzerakhandClaude Opus 5.5 56b3cd1246 Drop the keyboard prompts of #483 in favour of #585
#483 draws the stick glyphs on the wrong code points: U+E056 and U+E057 (the stick presses) get WASD and the mouse,
U+E05E and U+E05F (the sticks) get Ctrl and C, and both left-stick HUD images show Ctrl. #585 maps them by the game's
font and keeps each glyph's advance.

This reverts commit cd35ffa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:35:35 +03:00

114 lines
5.7 KiB
Python

"""Package a built totk.exe into a folder that runs from anywhere: build/<version>-portable by default.
The folder holds the exe, its DLLs, the shader translator (NativeAOT: no .NET), the RomFS index and patches, the
extracted ExeFS (the game code the exe loads), the shared fonts, the prepared shader and pipeline files. There is
no config file: totk.exe finds the exefs, romfs, profile and shared-fonts folders next to itself. Without --with-romfs
the 16 GB RomFS image stays out; copy build/extracted/<version>/romfs/romfs.bin to <folder>/romfs/ or set TOTK_ROMFS
(a path, relative ones to the exe's folder).
The folder contains game code and data derived from the game: for your own machines only, never publish or share it.
Saves go to <folder>/profile.
.venv/Scripts/python tools/package_build.py [--version v121] [--build build/v121] [--exe PATH/totk.exe]
[--out build/v121-portable] [--with-romfs] [--no-shader-cache]
"""
import argparse
import os
import shutil
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
EXE = '.exe' if os.name == 'nt' else ''
# A Linux source build links the system's SDL3, Opus and libvpx: the other machine needs them installed
DLLS = ('SDL3.dll', 'libopus-0.dll', 'libiconv-2.dll') if os.name == 'nt' else ()
# Prepared state next to the exe: translator answers, programs, the prepared list, pipeline cache and its keys, and the
# variant lists `totk.exe --prepare-shaders` reads (the driver keys its library cache by exe path, so each copy runs it).
STATE = ('romfs-index.bin', 'pipeline-cache.bin', 'pipeline-keys.bin', 'shader-answers.bin', 'shader-programs.bin',
'shader-prepared.bin', 'seed-answers.bin', 'seed-programs.bin')
def copy(src, dst):
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def copy_tree(src, dst, workers=16):
jobs = []
for dirpath, _, files in os.walk(src):
rel = Path(dirpath).relative_to(src)
(dst / rel).mkdir(parents=True, exist_ok=True)
jobs += [(Path(dirpath) / f, dst / rel / f) for f in files if not f.endswith('.tmp')]
with ThreadPoolExecutor(workers) as pool:
list(pool.map(lambda j: shutil.copy2(*j), jobs))
return len(jobs)
def size_of(path):
return sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--version', default='v121', help='game version (v121 or v143): the defaults below follow it')
ap.add_argument('--build', type=Path, help='build dir with the prepared state (default build/<version>)')
ap.add_argument('--exe', type=Path, help='totk.exe to package (default: <build>/totk.exe); its DLLs are taken from --build')
ap.add_argument('--xlate', type=Path, default=ROOT / 'build' / 'xlate' / f'totk_xlate{EXE}')
ap.add_argument('--extracted', type=Path, help='default build/extracted/<version>')
ap.add_argument('--out', type=Path, help='default build/<version>-portable')
ap.add_argument('--with-romfs', action='store_true', help='also copy romfs.bin (about 16 GB)')
ap.add_argument('--no-shader-cache', action='store_true', help='leave out shader-cache (about 2 GB): translates again')
a = ap.parse_args()
a.build = a.build or ROOT / 'build' / a.version
a.extracted = a.extracted or ROOT / 'build' / 'extracted' / a.version
a.out = a.out or ROOT / 'build' / f'{a.version}-portable'
build, out = a.build.resolve(), a.out.resolve()
exe = (a.exe or build / f'totk{EXE}').resolve()
fonts = a.extracted.resolve().parent / 'firmware' / 'shared-fonts'
exefs = a.extracted.resolve() / 'exefs'
romfs = a.extracted.resolve() / 'romfs' / 'romfs.bin'
need = [exe, a.xlate, exefs, fonts, build / 'romfs-patches'] + [build / d for d in DLLS] + [build / 'romfs-index.bin']
if a.with_romfs:
need.append(romfs)
missing = [str(p) for p in need if not p.exists()]
if missing:
sys.exit('package_build: missing: ' + ', '.join(missing))
if out == build:
sys.exit('package_build: --out must not be the build dir')
if not str(out).startswith(str(ROOT / 'build')) and not str(out).startswith(str(ROOT / 'generated')):
print('package_build: warning: the folder holds game code; keep it out of the repository')
out.mkdir(parents=True, exist_ok=True)
copy(exe, out / f'totk{EXE}')
for d in DLLS:
copy(build / d, out / d)
copy(a.xlate, out / f'totk_xlate{EXE}')
idf = a.xlate.with_suffix('.id')
if idf.exists():
copy(idf, out / 'totk_xlate.id')
for f in STATE:
if (build / f).exists():
copy(build / f, out / f)
else:
print(f'package_build: {f} not in {build}, skipped')
n = copy_tree(build / 'romfs-patches', out / 'romfs-patches')
for name in ('romfs-patches-xbox', 'romfs-patches-xbox-label'): # prompts = xbox (make_romfs_patches.py --prompts)
if (build / name).is_dir():
n += copy_tree(build / name, out / name)
n += copy_tree(exefs, out / 'exefs')
n += copy_tree(fonts, out / 'shared-fonts')
if not a.no_shader_cache and (build / 'shader-cache').is_dir():
n += copy_tree(build / 'shader-cache', out / 'shader-cache')
(out / 'profile').mkdir(exist_ok=True)
if a.with_romfs:
(out / 'romfs').mkdir(exist_ok=True)
print('package_build: copying romfs.bin (16 GB) ...')
shutil.copyfile(romfs, out / 'romfs' / 'romfs.bin')
print(f'package_build: {out}: {n} files in folders, {size_of(out) / 2**30:.2f} GiB'
+ ('' if a.with_romfs else f'; no RomFS: put romfs.bin into romfs{os.sep} or set TOTK_ROMFS'))
if __name__ == '__main__':
main()