Skip to content

Setup Wizard

M T edited this page Oct 4, 2026 · 1 revision

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.

Source files

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.

Overview of the flow

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"]
Loading

Command-line interface

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):

  • --bundled with an ISO or --game-dir is rejected; --bundled without --registry/--wine-prefix is rejected (setup_halo.py:478-483).
  • --json with any stage other than check is rejected (setup_halo.py:484-485).
  • An ISO together with --game-dir is rejected (setup_halo.py:486-487); covered by test_conflicting_sources_are_rejected.
  • All path options are expanduser().resolve()d, so paths with spaces and ~ work; setup.sh itself is written to be run from any working directory.

Stages

--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".

Exit codes

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.

Constants

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

Stage 0: launcher (setup.sh)

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.

Stage 1: readiness doctor (--stage check)

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.

Stage 2: platform, toolchain and disk gates

For every stage except check, main():

  1. Refuses to run unless on Apple Silicon macOS ("See docs/SETUP.md for preparing your game on Windows").
  2. For xcode/build: requires python312(), requires xcodegen for xcode only, and requires xcrun --sdk xros --show-sdk-path to succeed.
  3. Calls need_space() (setup_halo.py:51-55): free space on the volume holding ROOT must 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).
  4. Rejects a symlinked .setup/ or game/ (prevents the wizard from writing private data through a link to an unexpected location).
  5. Creates .setup/ with mode 0700 (and re-chmods it to 0700 if it already existed).
  6. Opens .setup/setup.lock and takes a non-blocking exclusive fcntl.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).

Stage 3: choosing the game source

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.

Stage 4: ISO route

4a. Read-only mount

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-*, makes disc/ inside it, and runs hdiutil 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 runs hdiutil detach <mount> and, if that fails, hdiutil detach -force <mount> with check=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.

4b. Disc recognition

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) and FilesCab/ (directory) with at least one *.cab.
  • Returns retailLayout, the disc executable's SHA-256, and needsPC110Update = (sha != HALO_SHA256).

4c. Wine and the original installer

iso_install() (setup_halo.py:163-212):

  1. find_wine() (setup_halo.py:58-65): --wine as a file path or a PATH name; otherwise the first of which wine, /Applications/Wine Stable.app/Contents/Resources/wine/bin/wine, /Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine. None found is a SetupError pointing to the --game-dir route.
  2. If --non-interactive was 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_installer asserts Popen is never called.)
  3. A dedicated prefix .setup/wine-prefix (mode 0700) is used with WINEPREFIX=<that>, WINEDEBUG=-all, and any inherited WINEARCH removed: "Never inherit a global Wine architecture choice or mutate ~/.wine."
  4. The ISO comes from the positional argument or a file picker (choose_file() uses osascript "choose file"; the code comment explains that a file picker "avoids shell escaping and putting product keys in a terminal").
  5. 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.exe through run_logged(..., 'retail-installer', env, cwd=<mount>, timeout=45 min). Afterwards wineserver -k is run against the prefix to flush its registry and release the disc.
  6. If the prefix still has no installation: "Installer did not create a Halo installation ... No successful install is claimed."

4d. PC 1.10 updater and executable verification

Still inside iso_install():

  1. If the installed halo.exe does not hash to HALO_SHA256, the wizard prints that the retail PC patch (not Custom Edition) is required plus the publisher announcement URL, and takes --patch or a file picker result. The patch must be an existing local .exe.
  2. It runs wine start /wait /unix <patch> via run_logged(..., 'pc110-update', env, cwd=<installed game>, timeout=20 min), then wineserver -k.
  3. The executable is hashed again. Anything other than c9acf0c469543283cfed6d7dc04ade976dbdfc7cb4532cf070386de169c19545 fails: "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.

4e. Logged child processes

run_logged() (setup_halo.py:131-151) is used for every long-running child:

  • Log file: .setup/logs/<label>.log, opened with O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW and mode 0600 (test_failed_child_is_not_success asserts 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, then SIGKILL after 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

Stage 5: importing the game (import_game)

5a. File selection

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.

5b. Validation (check_game)

check_game() (setup_halo.py:250-263) validates a prepared directory without ever printing registry values:

  1. 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).
  2. The registry seed must parse (read_registry).
  3. validate_game() from the device tool checks every REQUIRED_GAME_FILES minimum size, the halo.exe SHA-256, and duplicate destinations after canonicalisation; it also returns the per-file rows, total bytes and the canonical tree hash.
  4. All ten campaign maps must exist and be at least 1,000,000 bytes.

5c. Import algorithm

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"]
Loading

Key properties (each covered by a test in GameImportTests):

  • The source installation is never modified (test_import_normalizes_and_only_copies_assets asserts HALO.EXE still exists in the source and that private-note.txt was not copied).
  • Copies use clone_copy() (APFS clonefile(2) copy-on-write when possible, shutil.copy2 otherwise), 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_resume compares st_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 raises PreparationError (test_wrong_executable_rejected).
  • The seed is written with private_write() (setup_halo.py:39-48): parent created 0700, data written to a NamedTemporaryFile in the same directory, chmod 0600, then atomically replaced into place. test_private_write_permissions asserts 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.

Stage 6: registry conversion

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.

Seed file format

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.

Accepted inputs

read_registry(path) (halo_setup_registry.py:118-126):

  • A directory (a Wine prefix): parses system.reg as HKLM and user.reg as HKCU if 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:

  1. Existing seed (text starts with HKLM| or HKCU|): 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.
  2. .reg syntax (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 to HKLM/HKCU (Wine section headers without a hive use the per-file default root); doubled backslashes and Wow6432Node are normalised by normalize_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.

Validation and output (finish)

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".

Stage 7: visual packs

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 against mods/visual-assets.json (size and SHA-256) and reused with no network access.
  • Else, if Release/HaloVision.app exists (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.

Stage 8: Python environment

Still in build_engine():

  1. python312() returns the running interpreter if it is 3.12, else which python3.12.
  2. If .venv/ is missing, it is created (python-environment log). If .venv/bin/python is missing or not 3.12, setup stops and asks the user to preserve and recreate it; it never deletes an existing .venv.
  3. Dependencies are (re)installed (pip install --no-cache-dir -r requirements-development.txt, python-dependencies log) when import capstone, pefile, numpy, PIL, unicorn fails, or when .setup/requirements.sha256 is missing or differs from the SHA-256 of requirements-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.

Stage 9: engine generation (fingerprinted)

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:

  1. HALO_SHA256 + <requirements digest>, then
  2. for each input file, its repository-relative path followed by its bytes, in this order: all tools/**/*.py (sorted), all third_party/xwa/**/*.py and *.h (sorted), all decompilation/**/*.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.py

and 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 10: payload staging (stage_payload)

stage_payload() (setup_halo.py:303-331) converts game/ into an app-ready resource tree:

  1. check_game(game/) returns the canonical rows, total bytes and tree digest.
  2. If .setup/GamePayload or .setup/GamePayloadManifest.json already exists, it is validated with validate_bundled_game_payload(.setup); if its payload_id equals 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).
  3. Otherwise each file is clone_copy'd into .setup/payload-*/GamePayload/<canonical path>, and a manifest is written with private_write:
{
  "formatVersion": 1,
  "payloadID": "<sha256 of canonical [{path,bytes,sha256}] JSON>",
  "fileCount": 0,
  "totalBytes": 0,
  "executableSHA256": "c9acf0c4...",
  "files": [{"path": "halo.exe", "bytes": 0, "sha256": "..."}]
}
  1. The staged pair is validated, then renamed to .setup/GamePayload and .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.

Stage 11a: Xcode handoff (--stage xcode, default)

setup_halo.py:397-411:

  • If native/EngineVision/EngineVision.xcodeproj does not exist, build_engine_vision.py --generate-only creates it (xcode-project log) from project.yml. That spec references ../../.setup/GamePayload (folder reference) and ../../.setup/GamePayloadManifest.json as optional resources and the four .setup/VisualMods files as resources, and sets CODE_SIGNING_ALLOWED: YES with CODE_SIGN_STYLE: Automatic. test_public_project_includes_optional_payload_and_signing asserts 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.pbxproj mentions GamePayloadManifest.json, TextureMods.hvt and ShaderMods.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.xcodeproj unless --no-open or --non-interactive.

Stage 11b: unsigned direct build (--stage build)

setup_halo.py:382-396:

  • Runs build_engine_vision.py --configuration Release --direct (unsigned-build log), producing native/EngineVision/.build/DirectXROS/HaloVision.app.
  • If the app already contains GamePayload, its payload_id must equal the staged one, otherwise "Existing app payload differs; use a clean unsigned build before repeating setup."
  • Otherwise .setup/GamePayload is copytree'd (with clone_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.py never adds game data.

The result is unsigned; signing is done in Xcode or with prepare_engine_vision_device.py.

Files and state

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.

Resumability summary

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-prefix is detected and the installer is not shown again.
  • Requirements and generation are skipped when their stamps match.
  • .setup/GamePayload and .setup/VisualMods are 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)

Environment variables

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

Routes at a glance

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.

How agents should drive the wizard

AGENTS.md and AGENT_SETUP.md define the contract:

  1. Use setup.sh rather than reconstructing the commands. Verify the checkout's actual remote is mitchaiet/master-chef; setup needs no GitHub write access and must not publish anything.
  2. Run the read-only check first: ./setup.sh <iso> --stage check --json (or with --game-dir and --registry/--wine-prefix). Exit 2 means action is needed; "do not treat a parseable report as a successful setup."
  3. 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.
  4. The ISO installer must run in an interactive terminal for the human; --non-interactive intentionally refuses to start it. Never substitute a Custom Edition binary or bypass the hash.
  5. After the human finishes the installer, resume non-interactively with the managed prefix, or use --game-dir/--registry for Windows-prepared inputs, or plain ./setup.sh --non-interactive --no-open when game/ exists.
  6. Never inspect, print or attach raw product values; never invent PID/DigitalProductID values; never ask the user to paste a key into chat.
  7. Report each stage separately (prerequisites, import, generation, Xcode project/unsigned build, signing, installation, launch, gameplay) and claim only what actual outputs show. .setup/status.json does not prove installation.

Failure modes and recovery

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.

Testing

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.

Related pages

Clone this wiki locally