Repository navigation
Setup Wizard
The setup wizard (./setup.sh, implemented by tools/setup_halo.py) turns a user's own retail Halo: Combat Evolved PC media or an existing PC 1.10 installation into a private, buildable Halo Vision checkout. It is the front door of the whole pipeline: it verifies the exact executable that the Static Translation Pipeline is written against, imports only the game assets and the private installation registry values the runtime needs, fetches the checksum-pinned visual packs, runs engine generation, stages a manifest-verified game payload, and finally hands off to Xcode (or to an unsigned direct build, see Build System). It is deliberately a guided setup, not an unattended installer: product-key entry, the PC 1.10 updater window, Apple sign-in and headset trust are always human steps.
This page documents every stage in execution order, every command-line flag, the files the wizard reads and writes, the resumability rules, and how a coding agent is expected to drive it.
| File | Role |
|---|---|
setup.sh |
POSIX shell launcher. Finds a Python >= 3.10 (python3.12 first, then python3) and execs tools/setup_halo.py with all arguments. Exits 2 with an install hint if none is found. |
tools/setup_halo.py |
The wizard: argument parsing, readiness doctor, ISO mount, Wine installer, 1.10 updater, game import, payload staging, Python environment, generation, build/Xcode handoff. |
tools/halo_setup_registry.py |
Converts a Windows .reg export, a Wine prefix (system.reg/user.reg) or an existing seed into the private halo-vision-registry.txt seed, allow-listing only Halo values and never echoing private input. |
tools/halo-vision-registry.template.txt |
Placeholder template documenting the seed format for the manual route (see BUILDING.md). |
tools/prepare_engine_vision_device.py |
Imported by the wizard for HALO_SHA256, REQUIRED_GAME_FILES, clone_copy, sha256_file, validate_game, manifest_rows and validate_bundled_game_payload. See Device Preparation and Signing. |
tools/visual_assets.py |
ensure_visual_assets() fetches/verifies the texture and shader packs. Called at the start of the generation stage. See Visual Mods Pipeline. |
tools/test_setup_halo.py |
24 synthetic regressions (no ISO, key, Wine, signing or device). |
docs/SETUP.md, docs/AGENT_SETUP.md, AGENTS.md
|
User guide, agent procedure and agent ground rules. |
flowchart TD
A["./setup.sh args"] --> B["parse_args: validate flag combinations"]
B -->|"--stage check"| C["doctor(): readiness report, exit 0 or 2"]
B -->|"other stages"| D["platform + toolchain gate (Apple Silicon, Python 3.12, XcodeGen, xros SDK)"]
D --> E["need_space(): at least 12 GiB free"]
E --> F["mkdir .setup (0700), take .setup/setup.lock"]
F --> G{"Which game source?"}
G -->|"--game-dir / --bundled / --wine-prefix"| H["import_game(source, registry)"]
G -->|"game/ already exists"| I["check_game(game/) and reuse"]
G -->|"ISO (argument or file picker)"| J["iso_install(): mount read-only, Wine installer, 1.10 updater, hash check"]
J --> H
H --> K["status.json: game-ready"]
I --> K
K -->|"--stage prepare"| L["Stop: game preparation complete"]
K -->|"xcode / build"| M["build_engine()"]
M --> N["ensure_visual_assets(): download or reuse, verify SHA-256"]
N --> O[".venv (Python 3.12) + pinned requirements"]
O --> P["generate_engine_reuse.py + export_engine_imports.py (fingerprinted)"]
P --> Q["stage_payload(): .setup/GamePayload + GamePayloadManifest.json"]
Q -->|"--stage build"| R["build_engine_vision.py --configuration Release --direct, copy payload into app"]
Q -->|"--stage xcode (default)"| S["Generate or keep Xcode project, print signing steps, open Xcode"]
R --> T["status.json: build-ready, signed false, installed false"]
S --> T2["status.json: xcode-ready, signed false, installed false"]
All flags are defined in parse_args() (setup_halo.py:464-492).
| Flag | Type / default | Effect |
|---|---|---|
iso (positional, optional) |
path | The user's retail Halo PC .iso. If omitted on the ISO route, an osascript "choose file" dialog asks for it. |
--stage |
check, prepare, xcode, build; default xcode
|
How far to go (see table below). |
--game-dir |
path | Import an existing owned PC 1.10 installation instead of running the ISO installer. |
--bundled |
flag | Use the Complete release's game data at Release/HaloVision.app/GamePayload (sets --game-dir to that path). Requires --registry or --wine-prefix; cannot be combined with an ISO or --game-dir. |
--registry |
path | A Halo registry seed, a Windows Registry Editor .reg export, or a directory containing Wine system.reg/user.reg. "Never a raw product key." |
--wine-prefix |
path | Read the game and/or registry from an existing Wine prefix (read-only; "never modified"). |
--wine |
string | Path (or PATH name) of a macOS wine binary for the ISO installer. |
--patch |
path | The user's retail halopc-patch-1.0.10.exe; otherwise a file picker asks for it when needed. |
--non-interactive |
flag | Never start an installer UI or show a prompt; fail with an actionable message instead. Also implies --no-open. |
--no-open |
flag | Prepare the Xcode project without running open on it. |
--json |
flag | Machine-readable report. Only valid with --stage check. |
Validation rules enforced by argparse errors (exit status 2 from argparse):
-
--bundledwith an ISO or--game-diris rejected;--bundledwithout--registry/--wine-prefixis rejected (setup_halo.py:478-483). -
--jsonwith any stage other thancheckis rejected (setup_halo.py:484-485). - An ISO together with
--game-diris rejected (setup_halo.py:486-487); covered bytest_conflicting_sources_are_rejected. - All path options are
expanduser().resolve()d, so paths with spaces and~work;setup.shitself is written to be run from any working directory.
--stage |
Tool gate in main()
|
What runs | Final .setup/status.json
|
|---|---|---|---|
check |
none (runs before the platform gate) |
doctor() only; nothing is installed, imported or built. Exit 0 when every check passes, 2 otherwise. |
not written |
prepare |
Apple Silicon macOS only | Disk check, lock, game import (ISO, --game-dir, --wine-prefix, --bundled or reuse of game/). Stops after game-ready. |
{"stage": "game-ready", "executableSHA256": ...} |
xcode (default) |
Apple Silicon, Python 3.12, xcodegen, xcrun --sdk xros --show-sdk-path
|
Everything in prepare, then visual packs, .venv, generation, payload staging, Xcode project generation/handoff. |
{"stage": "xcode-ready", "signed": false, "installed": false} |
build |
Apple Silicon, Python 3.12, xcrun xros SDK (no XcodeGen) |
Everything in prepare, then visual packs, .venv, generation, payload staging, an unsigned direct Release build, and the payload copied into the app. |
{"stage": "build-ready", "signed": false, "installed": false} |
The gate is in main() (setup_halo.py:507-515). status.json records the last completed setup stage; it explicitly says signed: false, installed: false and, per AGENT_SETUP.md, "does not prove device installation".
| Code | Meaning | Source |
|---|---|---|
| 0 | Stage completed, or --stage check reported ready |
setup_halo.py:506, 547 |
| 2 |
--stage check reported missing prerequisites; or any SetupError, PreparationError, RegistryError, OSError, ValueError or subprocess.SubprocessError stopped setup; or an argparse usage error; or setup.sh found no Python >= 3.10 |
setup_halo.py:550-557, setup.sh:11-12 |
| 130 | Ctrl-C (KeyboardInterrupt); message "Setup cancelled. Rerun the same command to resume." |
setup_halo.py:558-560 |
On any handled failure the wizard prints Setup stopped: <message> followed by "Your ISO and existing installation were preserved. See docs/SETUP.md." The process sets umask 077 before anything else (setup_halo.py:551), so every file the wizard creates is owner-only by default.
Defined at setup_halo.py:25-32 and in the device-preparation module.
| Name | Value | Use |
|---|---|---|
ROOT |
repository root | All paths are anchored here, independent of the working directory. |
LOCAL |
ROOT/.setup |
Private state directory (mode 0700). |
GAME |
ROOT/game |
Imported, verified owned game files. |
GEN |
ROOT/native/build/engine-reuse/whole-exe |
Generated engine sources. |
PROJECT |
ROOT/native/EngineVision/EngineVision.xcodeproj |
Generated Xcode project. |
GIB |
1024**3 |
Disk arithmetic. |
CAMPAIGN |
a10 a30 a50 b30 b40 c10 c20 c40 d20 d40 |
All ten campaign maps must be present and at least 1,000,000 bytes. |
PATCH_INFO |
Bungie forum post URL | Printed when the 1.10 update is required; nothing is downloaded from it. |
HALO_SHA256 |
c9acf0c469543283cfed6d7dc04ade976dbdfc7cb4532cf070386de169c19545 |
The only supported halo.exe (retail PC 1.10). prepare_engine_vision_device.py:35
|
REQUIRED_GAME_FILES |
see table below | Minimum sizes of required files. prepare_engine_vision_device.py:36-46 |
REQUIRED_GAME_FILES (mirrored exactly by EngineAssetCatalog.required in EngineAssets.swift:13-23, so the app applies the same rule on the headset):
| Relative path | Minimum bytes |
|---|---|
halo.exe |
2,000,000 |
maps/ui.map |
2,000,000 |
maps/a10.map |
90,000,000 |
maps/bitmaps.map |
300,000,000 |
maps/sounds.map |
200,000,000 |
shaders/vsh.bin |
30,000 |
shaders/fx.bin |
900,000 |
strings.dll |
1,000,000 |
halo-vision-registry.txt |
100 |
setup.sh resolves its own directory with CDPATH= cd -- "$(dirname -- "$0")" && pwd, then tries python3.12 and python3, accepting the first whose sys.version_info >= (3, 10). Note that the launcher accepts 3.10+, but every stage other than check/prepare additionally requires a Python 3.12 interpreter (see python312() below) because the .venv must be 3.12.
doctor() (setup_halo.py:414-461) builds a list of {"check", "ok", "action"} rows. action is empty when the row passes. Rows, in order:
| Check name | Passes when | Action text when it fails |
|---|---|---|
Apple Silicon macOS |
sys.platform == 'darwin' and platform.machine() == 'arm64'
|
Use an Apple Silicon Mac for visionOS builds. |
Python 3.12 |
the running interpreter is 3.12, or python3.12 is on PATH
|
brew install python@3.12 |
visionOS SDK |
xcrun --sdk xros --show-sdk-version succeeds |
Install Xcode and its visionOS platform; select Xcode in Settings, Locations. |
XcodeGen |
xcodegen on PATH
|
brew install xcodegen (not needed for --stage build). |
12 GiB free disk |
shutil.disk_usage(ROOT).free >= 12 GiB |
Free disk space before running the installer or building. |
Wine for original installer |
only evaluated when there is no --game-dir, no existing game/, and no --wine-prefix; passes when find_wine() locates a binary |
Install a compatible macOS Wine distribution, or use the Windows fallback. |
Existing game installation + Private Halo registry
|
a source exists (--game-dir, else the default Halo path in --wine-prefix, else game/), every REQUIRED_GAME_FILES entry except the registry meets its size, all ten campaign maps exist, halo.exe matches HALO_SHA256, and the registry (--registry, else --wine-prefix, else <source>/halo-vision-registry.txt) parses |
On any failure a single row Existing game and private registry with the error text. |
Existing Wine installation |
emitted as a failure only when --wine-prefix has no default Halo directory and no --game-dir was given |
No default Halo path found; supply --game-dir. |
Retail ISO |
when an ISO is given: on macOS, the ISO mounts read-only and inspect_disc() recognises the retail layout |
the error text (or "ISO inspection requires macOS hdiutil") |
PC 1.10 update input |
the disc executable already matches HALO_SHA256, or --patch names an existing file |
Supply the retail PC 1.10 update installer with --patch when prompted. |
The report adds "ready" (all rows ok), "next" (./setup.sh <your ISO> (or --game-dir <your installed copy>)) and a "note" reminding that a valid product key must be entered in the original installer and Apple signing is completed in Xcode. With --json the dict is printed as JSON; otherwise each row prints OK: <check> or NEEDS ACTION: <check> plus an indented action. Exit status is 0 when ready and 2 otherwise (setup_halo.py:497-506).
The doctor really mounts the ISO (and detaches it again) when one is supplied; VALIDATION.md records that this path was exercised against a real owned ISO.
For every stage except check, main():
- Refuses to run unless on Apple Silicon macOS ("See docs/SETUP.md for preparing your game on Windows").
- For
xcode/build: requirespython312(), requiresxcodegenforxcodeonly, and requiresxcrun --sdk xros --show-sdk-pathto succeed. - Calls
need_space()(setup_halo.py:51-55): free space on the volume holdingROOTmust be at least 12 GiB, otherwise "Free disk space is X GiB; setup needs at least 12 GiB ... No existing data was deleted." No automatic cleanup is ever attempted (test_disk_guard). - Rejects a symlinked
.setup/orgame/(prevents the wizard from writing private data through a link to an unexpected location). - Creates
.setup/with mode 0700 (and re-chmods it to 0700 if it already existed). - Opens
.setup/setup.lockand takes a non-blocking exclusivefcntl.flock. A second concurrent setup in the same checkout fails with "Another setup process is using this checkout; wait for it to finish." The lock is held for the remainder of the run (setup_halo.py:521-525).
Inside the lock (setup_halo.py:526-540):
| Condition (first match wins) | Source | Registry input |
|---|---|---|
--game-dir given (including the implicit one from --bundled) |
that directory |
--registry, else --wine-prefix, else <source>/halo-vision-registry.txt
|
--wine-prefix given without --game-dir
|
prefix_game(prefix): exactly one of drive_c/Program Files/Microsoft Games/Halo or drive_c/Program Files (x86)/Microsoft Games/Halo containing a halo.exe (case-insensitive). None found is an error; two found asks for --game-dir. |
--registry, else the prefix |
game/ already exists as a directory |
reuse after check_game(game/) ("Reusing verified game/. The ISO and any previous installation are unchanged.") |
the seed already in game/
|
| otherwise | ISO route: iso_install(args)
|
--registry, else the managed Wine prefix |
prefix_game() (setup_halo.py:154-160) implements the default-path search.
mounted_iso() (setup_halo.py:75-104) is a context manager:
- The path must resolve strictly, be a regular file, and end in
.iso(case-insensitive). - It creates a private temporary directory
halo-iso-*, makesdisc/inside it, and runshdiutil attach -readonly -nobrowse -plist -mountpoint <tmp>/disc <iso>with a 120 s timeout. - The plist output must list the requested mount point among
system-entities[].mount-point; otherwise setup refuses ("did not mount at the requested temporary location"). - In
finally, if this invocation attached the image (or the directory is still a mount point), it runshdiutil detach <mount>and, if that fails,hdiutil detach -force <mount>withcheck=True(30 s timeouts). It detaches only its own temporary mount point, "never a user's existing disc", and avoids recursive temporary-directory cleanup into a still-mounted disk.
test_iso_detaches_after_bad_disc and test_iso_detaches_on_cancellation assert that -readonly is passed and that the final hdiutil call is a detach of the same mount point, after both a rejected disc layout and a KeyboardInterrupt.
inspect_disc() (setup_halo.py:107-116) requires the retail layout, using child_named() (exactly one case-insensitive, non-symlink child):
-
Setup.exe(file),Files/halo.exe(file) andFilesCab/(directory) with at least one*.cab. - Returns
retailLayout, the disc executable's SHA-256, andneedsPC110Update = (sha != HALO_SHA256).
iso_install() (setup_halo.py:163-212):
-
find_wine()(setup_halo.py:58-65):--wineas a file path or aPATHname; otherwise the first ofwhich wine,/Applications/Wine Stable.app/Contents/Resources/wine/bin/wine,/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine. None found is aSetupErrorpointing to the--game-dirroute. - If
--non-interactivewas passed or stdin is not a TTY, setup refuses to start the installer: "The original installer needs a human to enter their key ... No installer was started." (test_noninteractive_never_starts_installerassertsPopenis never called.) - A dedicated prefix
.setup/wine-prefix(mode 0700) is used withWINEPREFIX=<that>,WINEDEBUG=-all, and any inheritedWINEARCHremoved: "Never inherit a global Wine architecture choice or mutate ~/.wine." - The ISO comes from the positional argument or a file picker (
choose_file()usesosascript"choose file"; the code comment explains that a file picker "avoids shell escaping and putting product keys in a terminal"). - Inside the mount, if the prefix does not already contain an installation, it prints the human instructions (enter your own key, accept the terms yourself, keep the default location, do not launch Halo or install DirectX/GameSpy extras) and runs
wine start /wait /unix <disc>/Setup.exethroughrun_logged(..., 'retail-installer', env, cwd=<mount>, timeout=45 min). Afterwardswineserver -kis run against the prefix to flush its registry and release the disc. - If the prefix still has no installation: "Installer did not create a Halo installation ... No successful install is claimed."
Still inside iso_install():
- If the installed
halo.exedoes not hash toHALO_SHA256, the wizard prints that the retail PC patch (not Custom Edition) is required plus the publisher announcement URL, and takes--patchor a file picker result. The patch must be an existing local.exe. - It runs
wine start /wait /unix <patch>viarun_logged(..., 'pc110-update', env, cwd=<installed game>, timeout=20 min), thenwineserver -k. - The executable is hashed again. Anything other than
c9acf0c469543283cfed6d7dc04ade976dbdfc7cb4532cf070386de169c19545fails: "Setup will not generate code from an unknown executable."
The wizard never downloads or substitutes a patch or executable. The same digest gate is applied again in validate_game() during import, by doctor(), in every payload manifest (executableSHA256), and on the headset by EngineBundledManifest.check().
The returned (game, prefix) pair is fed to import_game(game, args.registry or prefix), so by default the registry is read from the managed prefix's system.reg and user.reg.
run_logged() (setup_halo.py:131-151) is used for every long-running child:
- Log file:
.setup/logs/<label>.log, opened withO_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOWand mode 0600 (test_failed_child_is_not_successasserts 0600). - The child runs in a new session (
start_new_session=True); stdout and stderr go to the log only. The console shows just<label>… (private log: .setup/logs/<label>.log). - On timeout or Ctrl-C the whole process group gets
SIGTERM, thenSIGKILLafter 5 s; setup raises "<label>stopped. Private files are retained; rerun to resume." - A nonzero exit raises "
<label>failed (exit N). Review its private local log; do not upload it unredacted."
| Label | Command |
|---|---|
retail-installer |
wine start /wait /unix Setup.exe |
pc110-update |
wine start /wait /unix <patch> |
python-environment |
python3.12 -m venv .venv |
python-dependencies |
.venv/bin/python -m pip install --no-cache-dir -r requirements-development.txt |
engine-generation |
tools/generate_engine_reuse.py ... |
engine-imports |
tools/export_engine_imports.py |
unsigned-build |
tools/build_engine_vision.py --configuration Release --direct |
xcode-project |
tools/build_engine_vision.py --generate-only |
selected_game_files() (setup_halo.py:215-247) builds the exact allow-list of files copied out of a source installation, normalised to Windows-insensitive lowercase destination names:
| Selected | Rule |
|---|---|
halo.exe, strings.dll
|
required; exactly one case-insensitive regular file each |
config.txt, bungie.bik, gearbox.bik, mgs.bik, ending.bik
|
optional; copied when present, must be regular files ("game assets, unlike saved profiles and installation registry data") |
maps/**/*.map |
every .map under the (case-insensitive) maps directory; other suffixes ignored |
shaders/**/*.bin |
every .bin under shaders; other suffixes ignored |
Any symbolic link inside maps/shaders is rejected ("Game asset trees must not contain symbolic links"); two files that collide after lowercasing are rejected. Everything else in the installation (saves, profiles, notes, other registry data) is ignored. test_game_content_selection_preserves_configuration_and_movies_only (in test_visual_assets.py) asserts that personal.sav and halo-vision-registry.txt in the source are not selected, while the configuration and four movies are.
check_game() (setup_halo.py:250-263) validates a prepared directory without ever printing registry values:
- The set of files under the root must equal exactly the selected files plus
halo-vision-registry.txt, with no symlinks anywhere. Extra files fail with "game/ contains unexpected files" (test_unexpected_files_in_game_not_bundled). - The registry seed must parse (
read_registry). -
validate_game()from the device tool checks everyREQUIRED_GAME_FILESminimum size, thehalo.exeSHA-256, and duplicate destinations after canonicalisation; it also returns the per-file rows, total bytes and the canonical tree hash. - All ten campaign maps must exist and be at least 1,000,000 bytes.
import_game() (setup_halo.py:266-300):
flowchart TD
A["read_registry(registry input) -> seed text"] --> B{"source is game/ itself?"}
B -->|yes| C{"game/halo-vision-registry.txt exists?"}
C -->|no| D["private_write seed"] --> E["check_game(game/)"]
C -->|"yes, same seed"| E
C -->|"yes, different seed"| F["SetupError: existing private values preserved"]
B -->|no| G["selected_game_files(source)"]
G --> H{"game/ exists?"}
H -->|yes| I{"same files (size+sha256) and same seed?"}
I -->|yes| J["Resume: nothing rewritten"]
I -->|no| K["SetupError: game/ differs, left unchanged"]
H -->|no| L["Stage in .setup/import-*/game (0700): clone_copy each file, private_write seed"]
L --> M["check_game(stage)"]
M -->|ok| N["rename stage -> game/ (atomic promote)"]
M -->|fail| O["temp dir removed; game/ never created"]
Key properties (each covered by a test in GameImportTests):
- The source installation is never modified (
test_import_normalizes_and_only_copies_assetsassertsHALO.EXEstill exists in the source and thatprivate-note.txtwas not copied). - Copies use
clone_copy()(APFSclonefile(2)copy-on-write when possible,shutil.copy2otherwise), so multi-GB maps do not double disk usage on APFS. - An existing
game/is never overwritten. A matching import resumes without rewriting files (test_matching_import_can_resumecomparesst_mtime_ns); a different one is refused (test_existing_game_not_overwritten,test_different_registry_never_replaces_existing). - A failed validation never promotes the staging directory and leaves no
import-*directory behind (test_failed_import_never_promoted). - A symlink planted in
Maps/is rejected (test_symlink_escape_rejected); a wrong executable raisesPreparationError(test_wrong_executable_rejected). - The seed is written with
private_write()(setup_halo.py:39-48): parent created 0700, data written to aNamedTemporaryFilein the same directory,chmod 0600, then atomicallyreplaced into place.test_private_write_permissionsasserts 0600.
After a successful import (or reuse), main() writes .setup/status.json = {"stage": "game-ready", "executableSHA256": HALO_SHA256}. With --stage prepare the wizard prints "Game preparation complete. Rerun ./setup.sh to generate the engine and open Xcode." and exits.
The original engine reads its installation values (CD path, product ID, version, and so on) through ADVAPI32 registry calls. On the Mac/headset these are served by a small in-memory registry seeded from <game root>/halo-vision-registry.txt (shims_misc.c:273-300, see Win32 Compatibility Layer). RegSetValueExA updates the in-memory table and rewrites the same file through reg_save() (shims_misc.c:331-336), so any value the engine writes is persisted back into that same seed file in the runtime's game root. The wizard produces the initial seed from the user's own installation.
One value per line, five |-separated fields:
ROOT|KEY|NAME|TYPE|VALUE
| Field | Allowed values |
|---|---|
ROOT |
HKLM or HKCU
|
KEY |
always Software\Microsoft\Microsoft Games\Halo (a Wow6432Node segment is normalised away) |
NAME |
one of CDPath, DigitalProductID, DistID, EXE Path, InstalledGroup, LangID, Launched, PendingVersion, PID, Version, VersionType, default, ExitFlag, FIRSTRUN, gamma (halo_setup_registry.py:9-11) |
TYPE |
1 (REG_SZ string), 3 (REG_BINARY, lowercase or uppercase hex digits without separators), 4 (REG_DWORD, decimal) |
VALUE |
string, hex bytes, or decimal DWORD. Must not contain \r, \n, ` |
The checked-in template shows the shape with placeholders only; the two private values (DigitalProductID, the encoded binary product data, and PID, the product ID string) are marked <...>. The encoded DigitalProductID is not the printed product key, and the docs warn never to paste a key into it. The runtime parser stores at most 256 values with 512-byte data buffers (shims_misc.c:274-275); this is why the converter refuses string values of 512 bytes or more rather than letting the runtime silently truncate them.
read_registry(path) (halo_setup_registry.py:118-126):
-
A directory (a Wine prefix): parses
system.regasHKLManduser.regasHKCUif present. -
A file: read via
read_text(), limited to 32 MiB, decoded as UTF-16 when it starts with a BOM (Windows Registry Editor exports) or UTF-8 with optional BOM otherwise.
parse_export() (halo_setup_registry.py:59-105) recognises two syntaxes:
-
Existing seed (text starts with
HKLM|orHKCU|): every line must have exactly five fields; only rows whose normalised key equals the Halo key (case-insensitively) and whose name is allow-listed are kept. -
.regsyntax (Windows "Registry Editor Version 5.00" and Wine "WINE REGISTRY Version 2"): backslash-newline continuations of wrapped hex values are joined first;[HKEY_LOCAL_MACHINE\...]/[HKEY_CURRENT_USER\...]headers map toHKLM/HKCU(Wine section headers without a hive use the per-file default root); doubled backslashes andWow6432Nodeare normalised bynormalize_key(); only"Name"=lines inside the Halo section with allow-listed names are kept."..."values become type 1 (decoded with JSON string semantics; malformed escapes raise "Malformed string" without echoing input),dword:becomes type 4 (hex to decimal),hex:becomes type 3 (commas and spaces stripped). Other value kinds are skipped.
finish(rows) (halo_setup_registry.py:32-56) enforces, without ever placing a value in an error message:
| Rule | Error |
|---|---|
HKLM DigitalProductID exists, is type 3, is 32 to 1024 hex digits, has even length, and is not all zeros |
"Missing or invalid DigitalProductID; use your own completed Halo installation" |
HKLM PID exists, is type 1, is non-blank, and contains no < or > (rejects unfilled template placeholders) |
"Missing PID; use your own completed Halo installation" |
Every kept value: type in {1, 3, 4} and no \r, \n, ` |
`, NUL |
| Type 3: even-length hex | "Malformed binary value ..." |
| Type 4: decimal in 0..0xFFFFFFFF | "Malformed DWORD ..." |
| Type 1: UTF-8 length < 512 bytes | "Oversized value ..." |
Rows outside HKLM/HKCU, the Halo key, or the allow-list are dropped. The function then forces HKCU ... ExitFlag = clean (type 1) so the engine does not treat the first launch as a recovery from a crash, sorts the rows by (root, name), and returns newline-terminated seed text. Duplicate (root, name) rows keep the last occurrence.
Tests in RegistryTests cover: the forced clean exit flag and exclusion of foreign keys (DO-NOT-EXPORT must not appear), UTF-16 Windows exports with wrapped hex and Wow6432Node, Wine registry files with unrelated sections, invalid license values never echoed (placeholders, non-hex, all zeros, odd length, arbitrary private text), a missing PID, malformed private strings not echoed, and 0600 output permissions. All fixtures are synthetic and "never usable as a product key".
build_engine() (setup_halo.py:340-411) begins by calling ensure_visual_assets() before any Python environment work. In summary (details on Visual Mods Pipeline):
- If
.setup/VisualMods/exists, every pack is re-verified againstmods/visual-assets.json(size and SHA-256) and reused with no network access. - Else, if
Release/HaloVision.appexists (Complete bundle), the packs are verified there and copied (copy-on-write) into.setup/VisualMods/. - Else the pinned archive (
MasterChef-v1.0.3-visual-assets.zip, 963,216,022 bytes) is downloaded from the project's GitHub release URL only, its size and SHA-256 are checked, its entries are extracted with strict allow-listing and verified, and the directory is promoted atomically.
A modified or unknown pack is never silently used: a changed .setup/VisualMods file raises instead of being replaced.
Still in build_engine():
-
python312()returns the running interpreter if it is 3.12, elsewhich python3.12. - If
.venv/is missing, it is created (python-environmentlog). If.venv/bin/pythonis missing or not 3.12, setup stops and asks the user to preserve and recreate it; it never deletes an existing.venv. - Dependencies are (re)installed (
pip install --no-cache-dir -r requirements-development.txt,python-dependencieslog) whenimport capstone, pefile, numpy, PIL, unicornfails, or when.setup/requirements.sha256is missing or differs from the SHA-256 ofrequirements-development.txt. The stamp is then rewritten.
Pinned requirements: capstone==5.0.6, pefile==2024.8.26, numpy==2.4.2, Pillow==11.3.0, unicorn==2.1.4.
setup_halo.py:362-380. The child environment adds HALO_EXE=<ROOT>/game/halo.exe (read by tools/engine_reuse/upstream.py:21 to locate the PE image; default game/halo.exe).
The generation fingerprint is a SHA-256 over:
-
HALO_SHA256 + <requirements digest>, then - for each input file, its repository-relative path followed by its bytes, in this order: all
tools/**/*.py(sorted), allthird_party/xwa/**/*.pyand*.h(sorted), alldecompilation/**/*.txt(sorted).
Generation is skipped when the output is complete (exactly 32 chunk_*.c plus engine_bundle.c, engine_imports.c, engine_functions.h, generation.json in GEN) and .setup/generation.sha256 equals the fingerprint. Otherwise it runs:
.venv/bin/python tools/generate_engine_reuse.py \
@decompilation/c9acf0c46954/function-addresses.txt \
@decompilation/c9acf0c46954/extra-function-entries.txt \
--label whole-exe --max-functions 10000 --trap-unsupported --discover --chunks 32
.venv/bin/python tools/export_engine_imports.pyand rewrites the stamp. See Static Translation Pipeline and Function Address Lists for what these produce. Any change to a translator source, the vendored decoder, or the address lists therefore forces regeneration on the next setup run.
stage_payload() (setup_halo.py:303-331) converts game/ into an app-ready resource tree:
-
check_game(game/)returns the canonical rows, total bytes and tree digest. - If
.setup/GamePayloador.setup/GamePayloadManifest.jsonalready exists, it is validated withvalidate_bundled_game_payload(.setup); if itspayload_idequals the digest, it is reused ("Verified existing bundled game payload; reusing it."). Anything else is refused: "Existing .setup/GamePayload differs or is incomplete; preserve it and use a fresh checkout." (test_modified_payload_is_not_overwritten). - Otherwise each file is
clone_copy'd into.setup/payload-*/GamePayload/<canonical path>, and a manifest is written withprivate_write:
{
"formatVersion": 1,
"payloadID": "<sha256 of canonical [{path,bytes,sha256}] JSON>",
"fileCount": 0,
"totalBytes": 0,
"executableSHA256": "c9acf0c4...",
"files": [{"path": "halo.exe", "bytes": 0, "sha256": "..."}]
}- The staged pair is validated, then renamed to
.setup/GamePayloadand.setup/GamePayloadManifest.json.
payloadID is computed by inventory_digest(): the SHA-256 of the compact, key-sorted JSON array of {path, bytes, sha256} rows, sorted case-insensitively by path (prepare_engine_vision_device.py:97-104). The headset uses the same value to choose a per-payload install directory (Application Support/HaloVision/PackagedGames/<payloadID>), see Device Preparation and Signing. test_payload_manifest_and_resume asserts the manifest validates, counts five fixture files, and is byte-identical after a second run.
- If
native/EngineVision/EngineVision.xcodeprojdoes not exist,build_engine_vision.py --generate-onlycreates it (xcode-projectlog) fromproject.yml. That spec references../../.setup/GamePayload(folder reference) and../../.setup/GamePayloadManifest.jsonas optional resources and the four.setup/VisualModsfiles as resources, and setsCODE_SIGNING_ALLOWED: YESwithCODE_SIGN_STYLE: Automatic.test_public_project_includes_optional_payload_and_signingasserts those strings are present. - If the project already exists, it is kept (preserving the user's Team and bundle identifier), but only if its
project.pbxprojmentionsGamePayloadManifest.json,TextureMods.hvtandShaderMods.hvs. An older project that predates these resources stops setup with instructions to preserve signing choices and regenerate with--generate-only. - The wizard prints the remaining human steps (select the EngineVision target, Signing & Capabilities, choose a Team and unique bundle identifier with automatic signing, choose the paired unlocked Vision Pro, press Run) and runs
open EngineVision.xcodeprojunless--no-openor--non-interactive.
- Runs
build_engine_vision.py --configuration Release --direct(unsigned-buildlog), producingnative/EngineVision/.build/DirectXROS/HaloVision.app. - If the app already contains
GamePayload, itspayload_idmust equal the staged one, otherwise "Existing app payload differs; use a clean unsigned build before repeating setup." - Otherwise
.setup/GamePayloadiscopytree'd (withclone_copy) into the app and the manifest copied beside it, then the app's payload is validated. - The code comment states the design: "Explicit setup invocation adds owned data; the ordinary build command stays source-only." The plain
build_engine_vision.pynever adds game data.
The result is unsigned; signing is done in Xcode or with prepare_engine_vision_device.py.
| Path | Created by | Mode | Purpose / resumability |
|---|---|---|---|
.setup/ |
main() |
0700 | All private state; must not be a symlink. Ignored by Git. |
.setup/setup.lock |
main() |
umask |
flock mutual exclusion per checkout. |
.setup/logs/<label>.log |
run_logged() |
0600 | Private child output; can contain local paths and installer output. |
.setup/wine-prefix/ |
iso_install() |
0700 | Dedicated Wine prefix for the original installer. Reused on rerun (if it already contains Halo, the installer is skipped). Also usable later as --wine-prefix "$PWD/.setup/wine-prefix". |
.setup/import-*, .setup/payload-*, .setup/visual-assets-*
|
temp directories | 0700 | Staging; removed automatically. |
.setup/requirements.sha256 |
build_engine() |
0600 | Requirements digest stamp. |
.setup/generation.sha256 |
build_engine() |
0600 | Generation fingerprint stamp. |
.setup/status.json |
main() |
0600 | Last completed stage (game-ready, xcode-ready, build-ready). |
.setup/GamePayload/, .setup/GamePayloadManifest.json
|
stage_payload() |
0700/0600 | Manifest-verified payload included by Xcode and the build stage. |
.setup/VisualMods/ |
ensure_visual_assets() |
0700, files 0600 | Verified texture/shader packs plus VisualModsManifest.json. |
.setup/visual-assets.lock |
ensure_visual_assets() |
umask | Blocking flock around pack installation. |
game/ |
import_game() |
0700 dir | Canonical owned game files plus the private seed (0600). Never overwritten. |
.venv/ |
build_engine() |
default | Python 3.12 environment. Never deleted by setup. |
native/build/engine-reuse/whole-exe/ |
generator | default | Generated engine sources. |
native/EngineVision/EngineVision.xcodeproj |
--generate-only |
default | Kept across reruns to preserve signing choices. |
All of these are listed in .gitignore and are flagged by the hygiene audit if they ever appear in a distribution tree.
Rerunning the same command after fixing a prerequisite or completing a human step is the intended recovery for every failure:
- Valid
game/is reused; different inputs are refused instead of overwriting. - A completed Wine install in
.setup/wine-prefixis detected and the installer is not shown again. - Requirements and generation are skipped when their stamps match.
-
.setup/GamePayloadand.setup/VisualModsare reused only when they verify exactly; modified copies are preserved and reported. - An existing Xcode project (and its signing choices) is kept.
- "Use a fresh checkout to import a different installation; existing game or modified payload directories are deliberately preserved." (SETUP.md)
| Variable | Default | Effect | Where |
|---|---|---|---|
HALO_EXE |
game/halo.exe |
Path of the PE image read by the generator. The wizard sets it to <ROOT>/game/halo.exe for generation, import export and builds. |
setup_halo.py:362, upstream.py:21 |
WINEPREFIX |
(set by wizard) |
.setup/wine-prefix for the installer and patch. |
setup_halo.py:173 |
WINEDEBUG |
(set by wizard) |
-all to silence Wine debug output. |
setup_halo.py:173 |
WINEARCH |
inherited value removed | Never inherited, so a global Wine architecture choice cannot affect the dedicated prefix. | setup_halo.py:175 |
| Route | Command | Human steps |
|---|---|---|
| Retail ISO |
./setup.sh "/path/to/HALO.iso" (optionally --patch ...) |
Key entry and terms in the original installer; run the 1.10 updater window; Xcode signing; headset trust/unlock. |
| Existing install + Windows export | ./setup.sh --game-dir <Halo> --registry <halo-install.reg> |
Exporting the key on Windows (reg export "HKLM\SOFTWARE\WOW6432Node\Microsoft\Microsoft Games\Halo" ...; omit WOW6432Node on 32-bit Windows); Xcode signing. |
| Existing Wine prefix |
./setup.sh --wine-prefix <prefix> (add --game-dir for a non-default path) |
Xcode signing. |
| Complete release |
./setup.sh --bundled --registry <export> or --bundled --wine-prefix <prefix>
|
Xcode signing. |
| Resume after ISO install (agent) | ./setup.sh --wine-prefix "$PWD/.setup/wine-prefix" --non-interactive --no-open |
None until Xcode. |
AGENTS.md and AGENT_SETUP.md define the contract:
- Use
setup.shrather than reconstructing the commands. Verify the checkout's actual remote ismitchaiet/master-chef; setup needs no GitHub write access and must not publish anything. - Run the read-only check first:
./setup.sh <iso> --stage check --json(or with--game-dirand--registry/--wine-prefix). Exit 2 means action is needed; "do not treat a parseable report as a successful setup." - Resolve prerequisites with the user's existing authorisation. Do not bypass the disk guard, delete unrelated files, weaken platform requirements, silently install privileged components, change the global Xcode selection, or use the user's normal Wine prefix.
- The ISO installer must run in an interactive terminal for the human;
--non-interactiveintentionally refuses to start it. Never substitute a Custom Edition binary or bypass the hash. - After the human finishes the installer, resume non-interactively with the managed prefix, or use
--game-dir/--registryfor Windows-prepared inputs, or plain./setup.sh --non-interactive --no-openwhengame/exists. - Never inspect, print or attach raw product values; never invent
PID/DigitalProductIDvalues; never ask the user to paste a key into chat. - Report each stage separately (prerequisites, import, generation, Xcode project/unsigned build, signing, installation, launch, gameplay) and claim only what actual outputs show.
.setup/status.jsondoes not prove installation.
| Symptom | Cause | Recovery |
|---|---|---|
| "Free disk space is X GiB; setup needs at least 12 GiB" | need_space() |
Free space, rerun. |
| "Another setup process is using this checkout" |
setup.lock held |
Wait for the other run. |
| "A macOS Wine installation is needed ..." |
find_wine() found nothing |
Install Wine, pass --wine, or use --game-dir with a Windows-prepared install. |
| "The original installer needs a human ..." | non-TTY or --non-interactive on the ISO route |
Run the command in Terminal. |
| "Installer did not create a Halo installation" | Wine installer compatibility | Use the Windows fallback; do not retry indefinitely. |
| "The installed executable is not the supported PC 1.10 binary" | wrong or unpatched executable | Supply the retail PC 1.10 patch (not Custom Edition). |
| "game/ already exists and differs" / "The supplied registry differs from game/" | different inputs | Use a fresh checkout. |
| "Existing .setup/GamePayload differs or is incomplete" | modified payload | Preserve it; use a fresh checkout. |
| "Existing Xcode project predates setup resources" | stale project | Note Team/bundle ID, regenerate with --generate-only. |
| "Visual asset is missing or changed" | modified .setup/VisualMods
|
See Visual Mods Pipeline. |
"<label> failed (exit N)" |
child process failure | Read .setup/logs/<label>.log locally; report a redacted excerpt only. |
python3 tools/test_setup_halo.py (run by run_source_checks.py) contains 24 tests in three classes:
| Class | Tests |
|---|---|
RegistryTests (7) |
clean ExitFlag and no foreign keys; UTF-16 Windows export with wrapped hex and Wow6432Node; Wine registry with unrelated sections; invalid license values never echoed; missing PID rejected; malformed private string not echoed; 0600 private writes. |
GameImportTests (11) |
only allow-listed assets copied and the seed is 0600; existing game/ not overwritten; matching import resumes without rewriting; different registry never replaces; failed import never promoted; symlink escape rejected; wrong executable rejected; payload manifest and resume; modified payload not overwritten; unexpected files in game/ not bundled; disk guard. |
WorkflowTests (6) |
non-interactive never starts the installer; ISO detaches after a bad disc; ISO detaches on cancellation; failed child is a failure and its log is 0600; public project includes the optional payload and signing; conflicting sources rejected. |
The fixtures patch HALO_SHA256, REQUIRED_GAME_FILES and CAMPAIGN to synthetic values. Per AGENT_SETUP.md, passing them "does not prove a fresh Wine installer works or that a headset can run the resulting app." VALIDATION.md records that a complete fresh Wine install/key-entry/update session and the Windows export instructions were not exercised for the 1.0.1 setup release.
Documents master-chef at commit 9f915af (v1.0.3). Unofficial project, not affiliated with Microsoft, Bungie, Gearbox or Apple. Original code is MIT licensed; game content is not included.
Overview
- Architecture Overview
- Repository Layout
- Glossary
- Environment Variables
- Contributing Guide
- Open Questions
Translation
- Static Translation Pipeline
- XWA Decoder and Lifter
- Function Address Lists
- EngineReuse Runtime
- x87 Floating Point
Host runtime
- EngineHost Overview
- Win32 Compatibility Layer
- Threading and Synchronization
- Guest Memory and Heap
- Engine Overrides and Hooks
- Runtime Settings
Graphics
- Direct3D9 Bridge
- Metal Renderer
- Shader Translation
- Textures and Texture Packs
- Geometry Fast Paths
- Radial Fog
Panorama and presentation
- Panorama System
- Panorama Budget and LOD
- Frame Pacing
- visionOS App
- Immersive Presenter
- Layer Alignment
Audio and input
Tooling and process