1 Commits
Author SHA1 Message Date
ClassicWasTaken 0ec49c5177 Release 1.4.0 native VR co-op and in-place upgrades 2026-10-06 11:11:45 -07:00
34 changed files with 5383 additions and 379 deletions

No files matched your search

+12 -14
View File
@@ -6,24 +6,22 @@
<img src="resources/artwork/halo-ce-logo.png" alt="Halo: Combat Evolved" width="180">
</p>
**Halo CE Steam Frame VR Mod — 1.4.0**
[**Download Halo-Steam-Frame-Mod-Setup-1.4.0.exe**](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.0/Halo-Steam-Frame-Mod-Setup-1.4.0.exe) for Windows 10/11 x64. Run this one file; Python, an installer ZIP, and a separate USB tools download are unnecessary. [Release notes & SHA256 checksum](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.4.0) · [1.4.0 source](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.4.0).
Play native ARM64/OpenXR Halo CE with **LAN and online campaign co-op**, **head-directed walking**, and Xbox-style buttons. Aim with the right controller; the reticle follows the nominal shot trajectory at render rate. Stereo sun glow defaults to half intensity. The headset tester confirmed reticle motion and LAN/online campaign co-op work with the engine patch used by this release. Measured performance and distant-peer coverage have not been reported.
<p align="center">
<a href="https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.3.5/Halo-Steam-Frame-Mod-Setup-1.3.5.exe"><strong>DOWNLOAD VR MOD INSTALLER v1.3.5 — WINDOWS EXE</strong></a><br>
<sub>One installer file · No administrator rights needed</sub><br>
<a href="https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.3.5">Release notes &amp; checksum</a>
<img src="docs/images/halocevr-steam-frame-example.jpeg" alt="Halo CE VR gameplay example with tracked hands and a plasma rifle beneath Halo's night sky" width="800">
</p>
**Community VR mod installer for Halo: Combat Evolved on Steam Frame.** Installs an experimental native ARM64/OpenXR port with Xbox-style controls and Steam library artwork.
*Community VR example by [u/Sea-Communication760](https://www.reddit.com/r/virtualreality/comments/1wpvi8n/halo_halocevr_working_great_on_steam_frame/): PC HaloCEVR on Steam Frame through Proton ARM. This is not a native 1.4.0 capture.*
You need **Windows 10/11 x64**, a **Steam Frame**, and your own authorized **original Xbox Halo CE ISO/XISO or extracted maps**. **USA Rev 2 is validated**; Halo PC, Xbox 360 Anniversary, and MCC data are unsupported.
Supply your own authorized **original Xbox Halo CE ISO/XISO or extracted maps**. USA Rev 2 was validated with the stable installer; PC, Xbox 360 Anniversary, and MCC data are unsupported. No retail executable, disc image, maps, product keys, or soundtrack is included or downloaded. Halo artwork and licensed components retain their rights and notices.
No retail game executable, disc image, maps, product keys, or soundtrack is included or downloaded. Halo artwork and trademarks retain their owners' rights; software components keep their licenses. This community project is unaffiliated with Microsoft, Xbox, Bungie, or Valve.
Choose **Install**, select your data, and follow the USB-C or network SSH guide. Setup adds Steam artwork and Notes automatically. The first build on the Frame can take tens of minutes.
1. Run the EXE, choose **Install**, and select your ISO or maps folder.
2. Enable Frame Developer Mode, set its user password, and connect over **USB-C** or **Wi-Fi / Ethernet**. The app guides you through SSH setup.
3. Save and close games, keep **Steam Home** running, and click **Install Halo VR**. The first build can take tens of minutes; your Frame needs internet access.
1.4.0 **replaces 1.3.5 in place** at `~/Games/HaloCENativeVR`, keeping **Halo: Combat Evolved VR (Native)** in Steam. **Install** recognizes a verified 1.3.5 installation and rebuilds it; **Repair** also upgrades it. Verified Xbox maps can be reused without an ISO. Old saves remain unchanged; the new engine uses `save-v1.4` and requires a new profile/campaign because older checkpoints are incompatible. Campaign has one player per machine, with LAN, Internet, and invite joining. Peers need matching **protocol 17** builds; 1.3.5 protocol 11 cannot join.
Steam artwork and the description/control guide in **Notes** are added automatically during Install and Repair. Already installed? Choose **Repair** to refresh the game, or **Uninstall** to remove it with an optional save backup.
The port is experimental; newer USB and library features still need physical Frame verification.
[Detailed setup & troubleshooting](docs/SETUP.md) · [Controls](docs/CONTROLS.md) · [Sources](docs/SOURCES.md) · [Third-party notices](docs/THIRD_PARTY.md) · [Build from source](docs/SETUP.md#develop-and-rebuild-the-executable)
[Setup](docs/SETUP.md) · [Controls](docs/CONTROLS.md) · [Release notes](docs/RELEASE_NOTES.md) · [Sources](docs/SOURCES.md) · [Third-party notices](docs/THIRD_PARTY.md)
+11 -6
View File
@@ -1,10 +1,10 @@
# Steam Frame controls
# Steam Frame controls — 1.4.0
The default installation uses tracked controller aiming with Xbox-style buttons. Point the right controller to aim; move your head to look around. These controls assume the game's default profile button layout.
1.4.0 uses head-directed walking and tracked controller aiming with Xbox-style buttons. Point the right controller to aim; the left stick moves relative to the horizontal direction of your headset. These controls assume the game's default profile button layout.
| Frame input | Action |
| --- | --- |
| Left stick | Move |
| Left stick | Move relative to headset direction |
| Left stick click | Crouch |
| Right stick left/right | Turn |
| Right stick click | Zoom |
@@ -24,19 +24,24 @@ The default installation uses tracked controller aiming with Xbox-style buttons.
The installer defaults to controller aiming, seated height, 72 Hz, resolution scale 1.0, smooth turning at 90 degrees per second, and two-handed aiming. Gesture melee is disabled so **B** remains the predictable melee input. These are installer preferences, not the upstream defaults. You can change supported options in **Settings > VR Setup** or the game's `config.toml`. Choose snap turning in VR Setup if you prefer turning in steps. Some display changes require restarting the game.
The reticle previews the nominal primary shot using Halo's firing calculation and projectile collision mask. On foot it uses render-frame aim, so its direction is no longer limited to the unit's 30 Hz updates; the hit is traced anew each frame. The headset tester confirmed the updated reticle motion works. Random spread and ballistic motion can still move individual impacts away from that point. Stereo sun glow defaults to `vr.sun_glow_strength = 0.5` in `config.toml`; 0 disables its glow, and 1 restores full intensity. Repair preserves a custom value.
## Local changes to upstream
The [included patch](../resources/frame-controls.patch) makes two changes:
The [included patch](../resources/frame-controls.patch) includes these controls and menu changes:
1. It sets **LB = change grenade** and **RB = flashlight**. The pinned upstream branch assigns those two bumpers the other way around.
2. Physical headset movement counts toward the campaign's look-direction tutorial while motion-controller aiming is active. Follow the technician's instructions and physically look up, down, left or right as requested. The patch ignores small tracking jitter and does not add head rotation to the weapon's aim. It lets the tutorial complete at the selected difficulty; it does not skip the campaign or raise its difficulty.
3. On foot, left-stick movement follows the headset's horizontal heading while your weapon still follows the controllers. Tracking loss suppresses walking instead of producing arbitrary movement. Vehicle controls keep their existing direction. Set `vr.movement = "aim"` (or `HALO_VR_MOVEMENT=aim`) to restore aim-relative movement; the installer defaults to `vr.movement = "head"`.
4. **Multiplayer > CO-OP CAMPAIGN** offers **HOST LOCAL (LAN)**, **JOIN LOCAL (LAN)**, **HOST ONLINE (INTERNET)**, **JOIN ONLINE (INTERNET)**, and **JOIN INVITE LINK**. Choose a host route, then campaign level and difficulty. Your friend uses the corresponding join route. Online hosting stays private by default; **LISTING = PUBLIC** allows server-browser joining by anyone. Each machine has one campaign player in VR and flat mode; campaign has no second-controller or split-screen option. See the [co-op setup guide](SETUP.md#online-campaign-co-op-with-a-friend).
The game's Steam shortcut also suppresses Steam's duplicate virtual Xbox gamepad with `SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0`. Frame controller poses, buttons and haptics still use OpenXR. This setting applies only to this Halo shortcut.
## First launch
Keep Steam Home and the headset runtime awake during setup. Launch **Halo: Combat Evolved VR (Native)** from the Frame's library and create a profile. Use the d-pad to select menu items and **A** to accept. Keep the headset on and hold both grips if the menu appears outside your view.
Keep Steam Home and the headset runtime awake during setup. Launch **Halo: Combat Evolved VR (Native)** from the Frame's library. 1.4.0 replaces the 1.3.5 program while retaining its old saves unchanged. The new engine uses `~/Games/HaloCENativeVR/save-v1.4`; create a new profile and campaign because old checkpoints are incompatible and setup does not migrate profiles. Use the d-pad to select menu items and **A** to accept. Keep the headset on and hold both grips if the menu appears outside your view.
For a control issue, first check **Settings > Gamepads** for a changed profile layout. Then check **Settings > VR Setup**: this layout uses controller aiming. Upstream also offers gamepad aiming, which moves aim with the right stick and has different camera behavior.
The original controls and all VR configuration options are documented in the [pinned upstream VR guide](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/README.md#vr). The [SDL hint source](https://github.com/libsdl-org/SDL/blob/release-3.4.16/include/SDL3/SDL_hints.h) documents virtual-gamepad filtering.
The original controls and all VR configuration options are documented in the [pinned upstream VR guide](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/README.md#vr). The [SDL hint source](https://github.com/libsdl-org/SDL/blob/release-3.4.16/include/SDL3/SDL_hints.h) documents virtual-gamepad filtering.
+11 -13
View File
@@ -1,20 +1,18 @@
# Halo CE Steam Frame VR Mod 1.3.5
# Halo CE Steam Frame VR Mod 1.4.0
Download only **[DOWNLOAD THIS FILE — Halo-Steam-Frame-Mod-Setup-1.3.5.exe](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.3.5/Halo-Steam-Frame-Mod-Setup-1.3.5.exe)**. This is the complete Windows mod installer; no separate ZIP or Python installation is needed.
[**Download Halo-Steam-Frame-Mod-Setup-1.4.0.exe**](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.0/Halo-Steam-Frame-Mod-Setup-1.4.0.exe) for Windows 10/11 x64. This is the release's only installer asset. Its SHA256 is listed on the [release page](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.4.0); matching installer source is in the [v1.4.0 tag](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.4.0). The headset tester confirmed the engine and controls used by this release, including reticle motion and LAN/online campaign co-op.
This patch fixes interrupted uninstall recovery, renamed native Steam entries, and errors during cancellation or connection cleanup.
- **Render-rate reticle:** corrects the visible stepping reported after 1.4.0a4. The read-only shot preview uses current-frame aim for the local on-foot player instead of only the 30 Hz unit direction. It retains the shared muzzle/camera calculation, trigger offsets, aim assist, collision mask and current hit test. Actual shots, spread and ballistics keep their behavior. Vehicle aiming retains the bounded turret path. The tester confirmed the new motion on the headset; no frame-time or FPS measurements were supplied.
- **LAN and online campaign:** Multiplayer > CO-OP CAMPAIGN offers HOST LOCAL (LAN), JOIN LOCAL (LAN), HOST ONLINE (INTERNET), JOIN ONLINE (INTERNET), and JOIN INVITE LINK. Campaign uses one local player per machine in VR and flat mode; the former second-controller/split-screen campaign route is removed. Campaign hosting starts on SINGLEPLAYER maps. Competitive multiplayer keeps its existing routes.
- **Updated Steam Notes:** Install/Repair adds the new co-op directions automatically. Only exact unchanged previous installer notes are upgraded; edited notes and other user fields are preserved.
- **Earlier fixes retained:** nominal shot/reticle alignment, half-strength stereo sun glow, head-directed movement, Xbox-style buttons and physical-head tutorial controls. The tester reported that 1.4.0a4 corrected aiming alignment, then confirmed the updated reticle motion and campaign routes work in the build used for 1.4.0.
- **Resume an interrupted uninstall.** Running Uninstall again can finish verified unfinished game folders from an earlier attempt, including when the main folder is already gone or Halo has been reinstalled. Setup verifies its private operation record and the remaining folder's identity before deleting files. Verified campaign save backups stay in place and their paths are reported. Unverified or changed folders are kept and reported as removal pending at 99%, rather than success or “already absent.”
- **Remove renamed Steam entries.** Uninstall matches the exact native Halo executable regardless of the entry's title. Unrelated games and similarly named entries remain untouched. Invalid native shortcut IDs stop removal before library-file writes or game deletion; existing backup and artwork-preservation safeguards remain in place.
- **Preserve cancellation and failure messages.** A failed stop request or disconnect no longer hides the operation's original error. Setup tries to stop a prepared build after cancellation, timeout, or a connection failure. If the existing stop request fails, setup makes one bounded recovery connection using the previously approved host key to stop that exact installer run. If remote stopping cannot be confirmed, setup explicitly warns that the build may still be running and displays the exact Frame-terminal command to stop it before retrying.
- **Separate connection cleanup from the game outcome.** If game installation or removal succeeds but connection cleanup fails, setup retains the result and shows a separate actionable warning at 99%. It does not claim complete disconnection or undo a completed game operation. USB cleanup remains limited to this operation's temporary forward; the shared ADB server and unrelated forwards are preserved.
**1.4.0 replaces 1.3.5 in place.** Close the native game and choose **Install** or **Repair**. Install recognizes a verified 1.3.5 installation and rebuilds it; verified Xbox maps can be reused without another ISO. The game remains at `~/Games/HaloCENativeVR` with the Steam title **Halo: Combat Evolved VR (Native)** and the normal installer cache.
Thanks to [dime-online](https://github.com/dime-online) for contributions [#7](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/7), [#8](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/8), and [#9](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/9), addressing issues [#1](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/1), [#2](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/2), and [#3](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/3). This release builds on those fixes with additional verification and explicit pending states.
Old 1.3.5 saves remain unchanged in their original `save` folder or configured location. The original config and program files are backed up in the owned installer run's `previous-program-files` folder. The new engine uses `~/Games/HaloCENativeVR/save-v1.4`: create a new profile and campaign. Its larger game-state layout cannot load old checkpoints, and setup does not automatically migrate checkpoints or profiles. Existing 1.4.0a2–a5 copies under `~/Games/HaloCENativeVRExperimental` remain untouched and can be kept for reference; this public installer targets the normal native installation.
The pinned native ARM64/OpenXR source, Xbox controls, tutorial patch, automatic Steam artwork/Notes, music preview, USB/network transfer, and Steam Home safeguards remain unchanged. The mod is experimental. Interrupted-removal recovery, cancellation recovery, direct USB transfer, full uninstall, a fresh first install, and newer Steam banner/logo/icon/Notes still need physical Frame verification.
Source remains pinned to `2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4`, native peer protocol **17**. Use matching builds/maps for co-op; stable protocol 11 and retail Halo clients cannot join. Online lobbies stay private by default; public listing lets anyone join. Internet play has no gameplay relay, so restrictive NAT can prevent distant peers from connecting. The reported headset test does not specify peer distance or prove compatibility across all networks. Renderer changes do not establish the cause or resolution of earlier white flashes or stuttering; actual Frame performance has not been measured.
Supply your own authorized **original Xbox Halo CE** data. **USA Rev 2 is validated**; other retail revisions need supported map headers. PC, Xbox 360 Anniversary, MCC, and Quest game-data inputs are unsupported. No retail game executable, disc image, maps, product keys, or soundtrack is included or downloaded, and setup provides no game-download links. Halo identification artwork and licensed software retain their rights and notices; see the [third-party guide](https://github.com/ClassicWasTaken/HaloSteamFrameMod/blob/v1.3.5/docs/THIRD_PARTY.md).
Supply your own authorized original Xbox Halo CE data. No retail executable, ISO, maps, keys, or soundtrack is bundled or downloaded. Halo identification artwork and licensed components retain their notices. The EXE is unsigned; verify it against the SHA256 shown on the release page.
The EXE is unsigned. Its SHA256 appears below. [Matching source and third-party notices](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.3.5) remain available for rebuilding. GitHub's automatic source downloads are optional for developers.
[Setup guide](https://github.com/ClassicWasTaken/HaloSteamFrameMod/blob/v1.3.5/docs/SETUP.md) · [Sources and credits](SOURCES.md).
[Setup guide](SETUP.md) - [Sources and credits](SOURCES.md) - [Third-party notices](THIRD_PARTY.md)
+50 -18
View File
@@ -1,28 +1,56 @@
# Halo CE Steam Frame VR mod setup
# Halo CE Steam Frame VR mod — setup 1.4.0
[Back to the project README](../README.md)
This community VR mod installer guides you through installing the experimental native Halo CE VR port. This guide covers USB-C and network connections, repair, Steam library updates, removal, troubleshooting, and rebuilding the Windows executable.
1.4.0 replaces an existing verified 1.3.5 installation at `~/Games/HaloCENativeVR`, keeping the Steam title **Halo: Combat Evolved VR (Native)**. Old saves are retained; the new engine starts with `save-v1.4` and a new profile/campaign.
## Before you begin
Download the single **[Halo-Steam-Frame-Mod-Setup-1.3.5.exe](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.3.5/Halo-Steam-Frame-Mod-Setup-1.3.5.exe)** for Windows 10/11 x64. It includes its runtime and USB transfer tools; no administrator rights, Python installation, installer ZIP, or separate Platform Tools download is needed. The [release notes](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.3.5) include the EXE's SHA256 checksum.
Download and run the single [**Halo-Steam-Frame-Mod-Setup-1.4.0.exe**](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.0/Halo-Steam-Frame-Mod-Setup-1.4.0.exe) for Windows 10/11 x64. It includes its runtime and USB transfer tools; no administrator rights, Python installation, installer ZIP, or separate Platform Tools download is needed. Verify its SHA256 against the checksum shown on the [release page](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.4.0).
Use your own authorized original Xbox Halo CE ISO/XISO or extracted maps. USA Rev 2 is validated; the supported-build table below explains other retail revisions. Halo PC, Xbox 360 Anniversary, MCC, and Quest packages are unsupported. No retail game executable, disc image, maps, product keys, or soundtrack is bundled or downloaded. The project provides no game-download sources.
Use your own authorized original Xbox Halo CE ISO/XISO or extracted maps. USA Rev 2 was validated with the stable installer; the supported-build table below explains other retail revisions. Halo PC, Xbox 360 Anniversary, MCC, and Quest packages are unsupported. No retail game executable, disc image, maps, product keys, or soundtrack is bundled or downloaded. The project provides no game-download sources.
Halo artwork and trademarks remain copyrighted to their respective owners, and third-party software retains its licenses and notices. This community project is unaffiliated with Microsoft, Xbox, Bungie, or Valve. Its license does not grant rights in retail game data or certify the legal status of upstream source; see [third-party notices](THIRD_PARTY.md).
The Frame needs ARM64 SteamOS, SteamVR/OpenXR, rootless Podman, internet access, and **12 GiB of free internal storage**. Setup checks these before installing. Your Windows computer needs about 2.5 GB of temporary space for maps. The SteamOS root filesystem stays read-only. Podman runs as the ordinary `steamos` host user; dependency installation uses root only inside the rootless container, with `no-new-privileges` retained.
The native game, Xbox controls, and older Steam shortcut/covers were previously tested on a physical Frame. USB transfer, a complete fresh-device install/uninstall, and the newer banner/logo/icon/Notes still need hardware verification. The upstream VR implementation is experimental, and performance varies by scene and device.
The headset tester confirmed the engine and controls used by this release, including reticle motion and LAN/online campaign co-op. The earlier 1.4.0a4 test confirmed shot/reticle alignment but identified visible reticle stepping, which the current render-rate preview addresses. No measured FPS, frame timing, peer-distance details, or complete uninstall test was supplied. Earlier white flashes and stuttering have not been separately diagnosed.
The executable uses the classic Halo PC Master Chief helmet-and-shoulders icon. Its [source and hashes](../resources/ui/README.md) are documented separately from the installer's code license. The Steam library's project-drawn helmet icon is unchanged.
## Engine and save compatibility
The source is pinned to `2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4`. It includes upstream network campaign co-op, delayed texture-stage binding, and vertex clip-position handling changes. These renderer changes are a candidate improvement; they do not establish the cause or resolution of reported white flashes or frame drops.
1.4.0 uses **`~/Games/HaloCENativeVR`** and the Steam title **Halo: Combat Evolved VR (Native)**, replacing 1.3.5's program in place. Old 1.3.5 saves remain unchanged in their original `save` folder or configured location. The original config and program files are backed up in the owned installer run's `previous-program-files` folder. The new engine's larger game-state capacity makes old checkpoints incompatible, so setup selects **`~/Games/HaloCENativeVR/save-v1.4`** for new saves. Create a new profile and campaign; setup does not automatically migrate checkpoints or profiles.
Existing 1.4.0a2–a5 installations at `~/Games/HaloCENativeVRExperimental`, with the Steam title **Halo: Combat Evolved VR (Experimental)**, remain untouched. You can keep that tested copy for reference. The public 1.4.0 installer targets the normal native installation.
With `vr.aim = "controller"`, **`vr.movement = "head"`** makes the left stick follow the headset's horizontal facing on foot. The right controller still aims the weapon. `vr.movement = "aim"` restores the previous aim-directed movement; `HALO_VR_MOVEMENT=aim` is the equivalent environment override. The movement patch excludes menus, flat mode, gamepad aiming, cinematics, remote players, and vehicle steering. Invalid tracking pauses head-directed movement rather than using an untrusted direction. The Xbox bumper layout and physical-head tutorial tests remain included.
**Multiplayer > CO-OP CAMPAIGN** opens network campaign choices in VR and flat mode: **HOST LOCAL (LAN)**, **JOIN LOCAL (LAN)**, **HOST ONLINE (INTERNET)**, **JOIN ONLINE (INTERNET)**, and **JOIN INVITE LINK**. Campaign uses one local player per machine; there is no second-controller or split-screen campaign route. Hosts start with **SINGLEPLAYER** maps selected. All peers need compatible **protocol 17** builds, matching game data and capacity limits; stable protocol 11 and retail Xbox/PC/Custom Edition/MCC clients cannot join. Extra co-op enemies default to `"none"`; Repair preserves a configured enemy mode.
The build stages `brokers.txt` for internet invite/server discovery. The relative default `network.brokers_file = "brokers.txt"` resolves beside the game's config; a custom setting is preserved. The tester confirmed LAN and online campaign work; successful joining still depends on compatible peers, reachable brokers, and each network's connectivity. [Pinned co-op implementation and protocol sources](SOURCES.md#140-engine-changes) explain these requirements.
## Online campaign co-op with a friend
Both players should use **the same 1.4.0 build** and supply their own supported original Xbox game data. Each player runs the game on their own headset or computer; campaign has one local player per machine. Use the left d-pad to navigate rows and change left/right choices, **A** to accept, and **B** to go back.
1. **Host:** open **Multiplayer > CO-OP CAMPAIGN**. On the same network choose **HOST LOCAL (LAN)**; across different networks choose **HOST ONLINE (INTERNET)**. The Map screen starts on **SINGLEPLAYER**. Choose a campaign level and difficulty.
2. In **Server Setup**, set **MAXIMUM PLAYERS** to 2 for two players, then select **START GAME**. For online browser discovery, change **LISTING** to **PUBLIC**. **PUBLIC lets anyone see and join the game.** Online co-op starts **PRIVATE** by default; setup does not force public listing.
3. **Friend:** open **Multiplayer > CO-OP CAMPAIGN > JOIN LOCAL (LAN)** or **JOIN ONLINE (INTERNET)** to match the host. Choose **REFRESH** if needed, select the host's game and press **A**. When both names appear in the lobby, let the countdown finish or select **START NOW**. If joining a game already underway opens its preview, select **JOIN GAME**.
For invite-only play, keep **LISTING = PRIVATE**. When the host's **INVITE LINK** row shows a `halo://join/` link, select that row and press **A** to copy it. Share it yourself through an app on the host's Frame. The friend copies it onto **their Frame's clipboard**, opens **Multiplayer > CO-OP CAMPAIGN > JOIN INVITE LINK**, selects **PASTE LINK**, then selects the host's game. Returning to the game can also consume the clipboard invite automatically. The PC and Frame have separate clipboards. The invite becomes unusable after the hosting game quits; switching a public game to private makes a new invite.
If a public game is missing, check that internet play is enabled on both games and that both use at least one reachable common broker. The browser needs broker support for retained listings; the included list supplies the defaults. Repairs preserve custom network settings, so an existing `network.online = false` or `network.public_lobby = false` must be corrected. **Settings > Network Setup > INTERNET PLAY** can enable internet play; restart Halo afterward.
If a game is visible but joining says **FAILED**, check matching builds/maps first. Gameplay uses **direct UDP**, STUN, and optional UPnP; the MQTT brokers carry discovery/signaling, **not a gameplay relay**. Strict NAT or carrier-grade NAT can prevent a distant friend joining. Try hosting from the other network. If an accessible home router still blocks it, choose a fixed `network.tunnel_port` in that game's `config.toml`, restart Halo, and forward that **UDP** port to the Frame on that router. UPnP cannot bypass a second upstream NAT. The successful headset report does not specify distant-peer or restrictive-NAT test coverage. [Primary internet-play and menu sources](SOURCES.md#online-co-op-menu-and-connection).
## Install Halo VR
1. **Game data.** Choose **Install**, then use **Choose ISO** or **Choose maps folder** to select an original **Xbox** Halo CE `.iso` / `.xiso`, or its extracted `maps` folder, that you are authorized to use. Setup validates the retail Xbox map format and extracts only the required maps. Halo PC, Xbox 360 Anniversary, MCC, and Quest packages are not inputs for this build. The repository and executable contain no commercial game data or download links for it. Click **Continue** to move to the next step.
1. **Game data.** Choose **Install**. For a first installation, use **Choose ISO** or **Choose maps folder** to select an original **Xbox** Halo CE `.iso` / `.xiso`, or its extracted `maps` folder, that you are authorized to use. Setup validates the retail Xbox map format and extracts only the required maps. A recognized, verified 1.3.5 installation can reuse its Xbox maps without another ISO; see [upgrade instructions](#already-have-halo-installed). Halo PC, Xbox 360 Anniversary, MCC, and Quest packages are not inputs for this build. The repository and executable contain no commercial game data or download links for it. Click **Continue** to move to the next step.
2. **Connect Frame.** Enable Developer Mode and set the Frame's user password, then choose **Network (Wi-Fi or Ethernet)** or **USB-C cable**. For network transfer, keep both devices on a network that permits connections between them and enter the Frame's hostname/IP. For USB, connect a data-capable cable directly to the PC and approve device authorization on the headset if prompted; no Frame IP is required. The SSH login remains `steamos`, port 22; username and port are under **Advanced connection settings**. Click **Test connection** and approve the displayed host fingerprint only after identifying your device. Your password is used in memory and is never saved or logged.
3. **Install Halo VR.** Save and close games, confirm **I have saved and closed games on my Frame**, then click **Install Halo VR**. Keep Steam Home and SteamVR running, and leave the Frame awake and connected. The app uploads maps, creates a rootless build container, compiles the pinned VR source, applies the controls patch, and registers **Halo: Combat Evolved VR (Native)** through the running Steam client when its verified library interface is available. Any remaining shortcut or artwork step is shown at the end; it does not require rebuilding the game. The first build may take tens of minutes. Activity opens automatically and shows the current phase and live build output; use **Save log** for troubleshooting.
3. **Install Halo VR.** Save and close games, confirm **I have saved and closed games on my Frame**, then click **Install Halo VR**. Keep Steam Home and SteamVR running, and leave the Frame awake and connected. The app uploads or verifies maps, creates a rootless build container, compiles the pinned VR source, applies the controls patch, and registers **Halo: Combat Evolved VR (Native)** through the running Steam client when its verified library interface is available. Any remaining shortcut or artwork step is shown at the end; it does not require rebuilding the game. The first build may take tens of minutes. Activity opens automatically and shows the current phase and live build output; use **Save log** for troubleshooting.
### Transfer over a USB-C cable
@@ -48,7 +76,7 @@ After you select valid local Xbox game data, the installer automatically loops t
### Supported Xbox ISO revisions
Use **Halo: Combat Evolved for the original Xbox**. **USA Rev 2 is validated with this installer.** Other retail revisions, including an original USA release or an image labeled **USA Rev 1**, and PAL releases are accepted when their actual map headers match one of these supported builds:
Use **Halo: Combat Evolved for the original Xbox**. **USA Rev 2 was validated with the stable installer.** 1.4.0 retains its map-header checks. Other retail revisions, including an original USA release or an image labeled **USA Rev 1**, and PAL releases are accepted when their actual map headers match one of these supported builds:
| Map-header build | Upstream region |
| --- | --- |
@@ -56,11 +84,13 @@ Use **Halo: Combat Evolved for the original Xbox**. **USA Rev 2 is validated wit
| `01.08.15.1749` | NTSC |
| `01.01.14.2342` | PAL |
These builds come from the [pinned port's compatibility table](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/source/cache/cache_files.c#L616-L631). Its [PAL conversion notes](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/game/pal_tags.c#L4-L22) describe the shared NTSC maps across three retail releases. Setup checks the image contents, not its filename or a Rev label: it requires all 24 original Xbox version-5 maps from one supported build. Incomplete or mixed-build sets and unlisted builds are rejected. Halo PC/Custom Edition, Xbox 360 Anniversary, MCC, and Quest packages are unsupported inputs. Supply your own authorized retail ISO/XISO or extracted maps.
These builds come from the [pinned port's compatibility table](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/cache/cache_files.c#L616-L631). Its [PAL conversion notes](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/game/pal_tags.c#L4-L22) describe the shared NTSC maps across three retail releases. Setup checks the image contents, not its filename or a Rev label: it requires all 24 original Xbox version-5 maps from one supported build. Incomplete or mixed-build sets and unlisted builds are rejected. Halo PC/Custom Edition, Xbox 360 Anniversary, MCC, and Quest packages are unsupported inputs. Supply your own authorized retail ISO/XISO or extracted maps.
### Already have Halo installed?
Choose **Repair** in the **Game data** step, connect your Frame, and click **Repair Halo VR** in the last step. Setup detects the standard native folder `~/Games/HaloCENativeVR` and reinstalls the native executable, SDL library, controls/tutorial patch, and notices; it restores the required VR/control settings while preserving saves and unrelated settings. Valid original Xbox maps already on the Frame can be reused without selecting another ISO. If maps are missing or damaged, select a valid local image/maps folder and retry. Previous installs made by this installer or the original guided native setup are recognized. A hand-installed native game in the same folder can be adopted only through the explicit Repair action, after its native executable and original Xbox maps validate. Unknown folders and other install locations are not overwritten.
To replace 1.3.5, close **Halo: Combat Evolved VR (Native)** and choose **Install** or **Repair**. Install recognizes a verified 1.3.5 installation in `~/Games/HaloCENativeVR` and rebuilds it with 1.4.0. Setup replaces the native program, SDL library, broker list, patch, and notices, reusing verified Xbox maps without another ISO. If maps are missing or damaged, select valid local data and retry. An unknown folder can be adopted only through explicit Repair after its native executable and Xbox maps validate; other locations are left alone.
The upgrade retains old saves at their original location and backs up the original config and program in the owned build run's `previous-program-files` folder. It points the new engine to `save-v1.4`, leaving the old game-state data untouched. Start a new profile/campaign; there is no automatic checkpoint or profile migration. Later repairs retain 1.4.0 saves and unrelated settings. A failed replacement rolls back the affected program/config files. The separate 1.4.0a2–a5 Experimental copy is not removed or upgraded by this installer.
The old PC installation at `~/Games/HaloCEVR` is detected and explained separately. Its PC maps cannot be used for this native Xbox build. Setup installs the native version separately and does not automatically remove the PC version. Close the native game before repair. Program-file backups are retained in the owned build workspace, and a failed repair rolls back the affected files. **Add to Steam again** repairs library registration and adds missing library artwork; it does not rebuild the game.
@@ -68,7 +98,7 @@ With Steam running, setup uses the current client's library interface and verifi
### Automatic Steam library information
Install and Repair automatically update the native Steam shortcut, missing artwork, and **Halo CE VR — About & controls** note after the game is ready. There is no separate Steam info option to select. Save and close games, leave Steam Home running, and keep the headset connected until setup disconnects. If registration, artwork, or Notes remains pending at the end, **Add to Steam again** retries that step without transferring maps or rebuilding the game.
Install and Repair automatically update the native Steam shortcut, missing artwork, and **Halo CE VR — About & controls** note after the game is ready. The note includes online hosting/joining instructions. Only exact unchanged earlier installer notes are upgraded; edited or unfamiliar notes remain untouched. There is no separate Steam info option to select. Save and close games, leave Steam Home running, and keep the headset connected until setup disconnects. If registration, artwork, or Notes remains pending at the end, **Add to Steam again** retries that step without transferring maps or rebuilding the game.
Open **Notes** on the Halo game's Steam details page or in its overlay to read the description and control guide. These are private game notes using Steam's normal notes synchronization. They do not replace the non-Steam About panel or add a store page, achievements, reviews, or another game's Steam identity. Existing notes and edited installer notes remain unchanged. If Notes is unavailable or synchronization cannot be verified, setup shows copyable text for manual use. Missing artwork or an icon awaiting Steam's shortcut-file persistence is reported separately and can be retried with **Add to Steam again**. Notes requires a running, verified Steam client; setup leaves a closed Steam client closed.
@@ -76,7 +106,7 @@ Open **Notes** on the Halo game's Steam details page or in its overlay to read t
Choose **Uninstall** on the first page to remove the recognized, owned native installation at `~/Games/HaloCENativeVR` and its native Steam shortcut. You do not need an ISO or local maps for uninstall, but you must connect over SSH with the Frame's user password. Save and close games; keep Steam Home and SteamVR running. Setup matches native shortcuts by their exact executable, so renaming the entry in Steam does not prevent removal. It verifies shortcut removal before deleting game files. If the client interface is unavailable or removal cannot be verified, setup keeps the native game files and stops at 99%, explaining Steam's **Remove Non-Steam Game** option. Invalid matching shortcut records also stop removal before library-file changes. An owned installation build still running stops removal before deletion. Setup never shuts down or relaunches Steam. If Steam is already closed, automatic removal can use its backed-up library-file path across the local accounts.
**Keep my campaign saves in a backup (recommended)** is checked by default. Setup backs up saves inside the native game folder and the original config to `~/Games/HaloCENativeVR-saves-<operation ID>`, and reports the exact backup location. The recognized native folder, including its maps, program files, and config, is then removed; configured saves outside that folder remain untouched. Turning off the checkbox allows saves inside the removed folder to be deleted. Uninstall leaves unrelated games, foreign or unrecognized folders, and unrelated settings alone. The build cache remains available for troubleshooting. Setup disconnects its SSH session after the operation and reaches 100% only after completion.
**Keep my campaign saves in a backup (recommended)** is checked by default. Setup backs up recognized saves inside the native game folder, including retained `save` and active `save-v1.4` data, together with the game's config to `~/Games/HaloCENativeVR-saves-<operation ID>`. It reports the exact backup location. The recognized native folder, including its maps, program files, and config, is then removed; configured saves outside that folder remain untouched. Turning off the checkbox allows saves inside the removed folder to be deleted. Uninstall leaves unrelated games, foreign or unrecognized folders, and unrelated settings alone. The build cache remains available for troubleshooting. Setup disconnects its SSH session after the operation and reaches 100% only after completion.
If an earlier uninstall was interrupted, run **Uninstall** again. Setup can finish deleting its verified unfinished game folders even when the main game folder is gone or Halo has since been reinstalled. It checks the earlier operation's private record and the remaining folder's identity before resuming. Verified save backups remain in place, and their paths appear in the result. Changed or unverified folders are preserved and listed for review; setup reports removal pending at 99% instead of claiming the game was already absent. Review **Show activity** and **Save log** for these paths. Do not delete a reported save backup. If setup reports that it reached its recovery limit, run Uninstall again to continue; folders that fail verification need manual review.
@@ -109,19 +139,19 @@ See [controls and patch details](CONTROLS.md).
## What gets installed
Game: `~/Games/HaloCENativeVR`, including `halo`, bundled SDL3, your maps, config, notices, and an ownership/provenance manifest. New-install saves are in that game's separate `save` folder; repairs preserve existing save locations. Build workspace: `~/.cache/halo-frame-installer`. Source is fetched at the fixed commit below, not a moving branch. Build cache is retained for troubleshooting. Setup does not remove another Halo installation or touch unrelated games.
Game: `~/Games/HaloCENativeVR`, including `halo`, bundled SDL3, your maps, config, notices, and an ownership/provenance manifest. New engine saves use `save-v1.4`; old 1.3.5 saves stay in `save` or their original configured location. Build workspace: `~/.cache/halo-frame-installer`; previous program/config backups are retained inside the owned installer run's `previous-program-files` folder. Windows installer state uses `%LOCALAPPDATA%/HaloFrameInstaller`. Source is fetched at the fixed commit below, not a moving branch. Build cache is retained for troubleshooting. Setup replaces the recognized 1.3.5 native program and leaves other Halo installations and unrelated games alone.
The original Halo CE cover is shown inside the installer. The native shortcut receives missing portrait and landscape covers, a 1,920 × 620 original CE promotional banner, transparent title logo, and the project-drawn Master Chief helmet icon. With Steam running, setup uses its verified client interface, checks the active account and native executable, and verifies the artwork hashes. The icon is kept in the owned game's `.installer-artwork` folder so it survives the setup EXE closing. Its existing path is verified through read-only shortcut-file checks before changing it through Steam's API. Both running-client and already-closed file paths preserve custom art and icons, including different supported image extensions. An icon that cannot be verified is reported as pending rather than replacing an uncertain user choice.
The description and Xbox controls use **Steam Game Notes**. Setup appends one bounded note without replacing other notes or an edited installer note. Artwork and Notes have independent pending states; Install, Repair, and any completion retry use the same handling. No artwork download is needed. The new bundled images total less than 1 MB, with sources and exact hashes in the [artwork resource notice](../resources/artwork/README.md). The user confirmed the older entry and covers on the Frame tested with 1.2.3; the newer banner, logo, icon, and Notes still need a physical Frame check.
The description and Xbox controls use **Steam Game Notes**. Setup appends one bounded note without replacing other notes or an edited installer note. Artwork and Notes have independent pending states; Install, Repair, and any completion retry use the same handling. No artwork download is needed. The bundled images total less than 1 MB, with sources and exact hashes in the [artwork resource notice](../resources/artwork/README.md). The user confirmed the entry and covers on the Frame tested with 1.2.3. Banner, logo, icon, and Notes integration also have automated coverage.
The renderer uses the Xbox game's data and translated Xbox shaders. Native OpenXR VR and competitive multiplayer come from the port. The PC-only Chimera, PC Restored maps, and HaloCEVR DLLs do not belong in this native build. Multiplayer requires compatible native/OpenCE protocol builds; Halo PC/Custom Edition/MCC servers and PC save files are incompatible.
## Sources and credits
- [OpenCE Steam Frame VR PR #85](https://github.com/OpenCommunityEdition/OpenCE/pull/85), by startupfoundry.
- [Exact VR source commit `88142798513ebd99fc7c6224023e8b44c05d0106`](https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106), with a documented local controller/tutorial patch.
- [Upstream native Linux/VR build instructions](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/README.md#vr).
- [Exact VR source commit `2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4`](https://github.com/startupfoundry/halo-ce-universal/tree/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4), with a documented local controller/tutorial patch.
- [Upstream native Linux/VR build instructions](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/README.md#vr).
- [Valve's official Frame setup](https://partner.steamgames.com/doc/steamhardware/steamframe/setup) and [SSH/debugging instructions](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging).
- [Google's ADB connection and forwarding reference](https://developer.android.com/tools/adb) and [official Platform Tools download and terms](https://developer.android.com/tools/releases/platform-tools).
- [Original Xbox dashboard visual references](SOURCES.md#dashboard-visual-references) and [Halo CE artwork attribution and Steam Notes references](SOURCES.md#steam-library-artwork).
@@ -153,7 +183,7 @@ Full source attribution, decompilation lineage, multiplayer references, and thir
- **Steam Home remains on a loading logo after an older installer run:** 1.2.3 removes the automatic shutdown request that disrupted the SteamOS-managed session in a reported case. It does not repair a session already in a failed state. Restart the Frame using its normal power controls, and contact Steam Support if Steam Home does not return. [Steam session safeguard and diagnostic sources](SOURCES.md#steam-session-safeguard).
- **Menu music unavailable:** select valid local original Xbox data containing `ui.map`; music starts automatically after validation. A repair using only remote maps cannot preview music. Playback errors do not block setup; Windows volume controls the final sound level.
The release executable is unsigned. Verify its SHA256 against the checksum in the release description; this project does not ask you to disable antivirus or other security protections. Review logs for private information before posting an issue.
The executable is unsigned. Verify its SHA256 against the checksum shown on the release page; this project does not ask you to disable antivirus or other security protections. Review logs for private information before sharing them.
## Develop and rebuild the executable
@@ -165,7 +195,9 @@ python -m venv .venv
.\.venv\Scripts\python scripts/build_release.py
```
Corresponding installer source and applicable third-party source/notices are available in the **[version 1.3.5 source tag](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.3.5)** so you can modify and rebuild it, including its LGPL Paramiko dependency. GitHub's automatically generated Source code ZIP is optional for developers and is not needed to run setup. ADB binaries are not checked into Git; [bundle_usb_tools.py](../scripts/bundle_usb_tools.py) downloads the pinned official archive during the executable build and verifies its archive and selected-file checksums before packaging. [resources/usb/manifest.json](../resources/usb/manifest.json) records those hashes and provenance, and the original notices are retained. The build script packages installer code/resources and verified ADB components with their notices; no retail game executable, user ISO, maps, SSH password, or private key is included. GitHub Actions runs tests and builds the Windows setup executable. A local rebuild also writes a source bundle and checksum file to `dist/`; the public release attaches only the clearly named mod installer executable and prints its checksum in the release description.
The build writes a matching installer source bundle and `SHA256SUMS.txt` to `dist/` alongside the EXE. These remain local build outputs. The public release uploads one EXE and lists its SHA256 in the release notes; matching installer source is available in the [v1.4.0 Git tag](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.4.0), with GitHub's automatic source downloads available for developers. Applicable third-party source/notices are included for rebuilding, including the LGPL Paramiko dependency. ADB binaries are not checked into Git; [bundle_usb_tools.py](../scripts/bundle_usb_tools.py) downloads the pinned official archive during the executable build and verifies its archive and selected-file checksums before packaging. [resources/usb/manifest.json](../resources/usb/manifest.json) records those hashes and provenance. The package contains installer code/resources and verified tools with their notices; no retail executable, ISO, maps, SSH password, or private key is included.
To check an exact local engine checkout without connecting a headset or downloading data, run `python scripts/check-experimental-engine.py PATH_TO_ENGINE_CHECKOUT`. The checker accepts a clean or fully patched checkout at the pinned revision, checks patch applicability in a private temporary projection, and verifies renderer/co-op/protocol/control integration. It generates the ARM64 Ninja graph offline with external downloads disabled; this is a source/configuration check, not an ARM64 compilation or headset test.
The workflow also includes an ARM64 Linux check, [check-build-container.py](../scripts/check-build-container.py), that uses the same rootless Podman arguments as the Frame build. It checks APT package installation, `setgroups`, `seteuid`, `setegid`, `chown`, and that container output belongs to the ordinary host user. The Windows executable CI job depends on that check as well as the unit tests. This container check uses no game data and does not replace a full Steam Frame installation test.
+50 -16
View File
@@ -1,32 +1,61 @@
# VR mod sources and acknowledgements
# Halo CE Steam Frame VR mod sources and acknowledgements
This community VR mod installer builds the experimental native ARM64 Linux VR port of **Halo: Combat Evolved** from the revision below. It provides a guided installation; the decompilation, renderer, OpenXR implementation and networking are the work of the credited upstream contributors. No retail game executable, disc image, maps, product keys, or soundtrack is bundled or downloaded; users provide authorized original Xbox game data.
Sources were checked on 2026-10-05. Links to pinned files describe the build used here; moving documentation may describe newer behavior.
Sources were checked on 2026-10-06 against a local checkout at the exact engine revision. Release 1.4.0 uses the headset-tested 1.4.0a5 engine patch: the tester confirmed reticle motion and LAN/online campaign co-op work. The earlier 1.4.0a4 test confirmed shot/reticle alignment but identified visible stepping. The current build adds render-rate reticle presentation and explicit network campaign routes. The report contains no measured FPS, frame timings, or peer-distance details; source and production C checks provide additional software coverage.
## Installer contributions
[dime-online](https://github.com/dime-online) contributed the fixes for [interrupted uninstalls (#7)](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/7), [renamed native Steam shortcuts (#8)](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/8), and [SSH cancellation/error handling (#9)](https://github.com/ClassicWasTaken/HaloSteamFrameMod/pull/9), addressing issues [#1](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/1), [#2](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/2), and [#3](https://github.com/ClassicWasTaken/HaloSteamFrameMod/issues/3). Release 1.3.5 builds on those contributions with operation-record and directory-identity checks before resuming file removal, explicit pending-removal and cleanup results, refusal of malformed native shortcut IDs before writes, and one bounded cancellation recovery connection using the already-approved host key.
These changes affect installer recovery and library cleanup. The pinned native game source, controller/tutorial patch, and game-data requirements are unchanged. Automated regression coverage does not replace physical Frame verification of interruption, cancellation, and full uninstall.
Those contributions remain in 1.4.0. This release separately changes the engine pin and movement patch; automated regression coverage does not replace physical Frame verification of interruption, cancellation, and full uninstall.
## Native game and VR
| Source | Contribution or verified requirement |
| --- | --- |
| [startupfoundry's native Steam Frame VR work, OpenCE PR #85](https://github.com/OpenCommunityEdition/OpenCE/pull/85) | OpenXR stereo, tracked head and controllers, native ARM64 host, desktop OpenGL and stereo multiview. This is experimental work in an open pull request. The normal upstream CI builds do not enable `--vr`. |
| [Pinned fork revision `88142798513ebd99fc7c6224023e8b44c05d0106`](https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106) | Exact source revision selected for this installer, rather than a moving branch. |
| [Upstream build instructions](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/README.md#64-bit-arm) | Native ARM64 compiler requirements, SDL 3.4.16, musl 1.2.5, and the `linux_arm64` target. |
| [Upstream VR documentation](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/README.md#vr) | `configure.py --vr`, OpenXR runtime requirements, motion aiming, gamepad mode, comfort options, and experimental limitations. |
| [startupfoundry's native Steam Frame VR work, OpenCE PR #85](https://github.com/OpenCommunityEdition/OpenCE/pull/85) | OpenXR stereo, tracked head and controllers, native ARM64 host, desktop OpenGL and stereo multiview. This experimental work is documented in the pull request. The normal upstream CI builds do not enable `--vr`. |
| [Pinned fork revision `2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4`](https://github.com/startupfoundry/halo-ce-universal/tree/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4) | Exact source revision selected for this installer, rather than a moving branch. |
| [Upstream build instructions](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/README.md#64-bit-arm) | Native ARM64 compiler requirements, SDL 3.4.16, musl 1.2.5, and the `linux_arm64` target. |
| [Upstream VR documentation](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/README.md#vr) | `configure.py --vr`, OpenXR runtime requirements, motion aiming, gamepad mode, comfort options, and experimental limitations. |
| [OpenCommunityEdition/OpenCE](https://github.com/OpenCommunityEdition/OpenCE) | Community port and integration work from which the VR fork descends. |
| [bnunu/halo-1](https://github.com/bnunu/halo-1) and [punpckhdq/halo](https://github.com/punpckhdq/halo) | Decompilation lineage identified by the upstream README. |
| [Upstream XDK declarations](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/include/xdk/README.md) | The port supplies its own declarations; installing the proprietary Xbox SDK is unnecessary. |
| [Upstream XDK declarations](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/include/xdk/README.md) | The port supplies its own declarations; installing the proprietary Xbox SDK is unnecessary. |
The installer applies [its documented controller and tutorial patch](../resources/frame-controls.patch) to that pinned revision. [Controls and defaults](CONTROLS.md) distinguish these changes from upstream behavior. Performance depends on game scene, headset software and settings; native ARM64 does not imply unlimited performance.
The installer applies [its documented controls, reticle and campaign-menu patch](../resources/frame-controls.patch) to that pinned revision. Its SHA256 is `639f2694cf9574eefca6204e74193b95a72b642128884189ad398c86f7c1b30e`. [Controls and defaults](CONTROLS.md) distinguish these changes from upstream behavior. Performance depends on game scene, headset software and settings; native ARM64 does not imply unlimited performance.
### 1.4.0 engine changes
- [Two-pass texture-stage resolution and binding](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/src/d3d8_gl.c#L2669-L2743) resolves/uploads all textures before binding each sampler stage, avoiding an upload overwriting a previous stage's binding.
- [Vertex clip-position handling](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/src/nv2a_vsh.c#L309-L402) retains the pre-reciprocal clip position and moves zero/NaN clip `w` behind the camera. These changes are upstream renderer candidates; they do not diagnose the user's white flashes or prove improved Frame performance.
- [Network co-op implementation](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/game/network_coop.c) and [distributed-netcode design](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/NETCODE.md) describe campaign maps hosted over LAN/internet, synchronized script presentation/events, and host-run AI. The local patch replaces the split-screen campaign entry with explicit LAN and Internet choices.
- [Native network version](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/include/halo_port_limits.h#L54-L74) is **17**, compared with **11** in the stable engine. Compatible peers need matching versions and capacity limits; legacy retail/PC/MCC clients are incompatible.
- [Capacity and saved-game warning](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/include/halo_port_capacity.h#L19-L45) records the larger game-state/AI pools and explicitly invalidates earlier save layouts. 1.4.0 replaces the verified 1.3.5 program at `~/Games/HaloCENativeVR` and keeps **Halo: Combat Evolved VR (Native)**. Old saves stay unchanged at their original location; the new engine uses `save-v1.4` and a new profile/campaign. The original config and program are backed up in the owned installer run's `previous-program-files` folder. Checkpoints and profiles are not automatically migrated. Existing 1.4.0a2–a5 Experimental copies remain untouched.
- [Co-op and broker defaults](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/src/port_config.c#L340-L375) include 50% additional enemies per extra player and `network.brokers_file`. [The ARM64 staging graph](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/tools/linux_arm64_build.py#L182-L187) includes [the broker list](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/assets/network/brokers.txt); setup stages it while preserving custom broker settings.
The local patch adds independent head-directed stick movement (`vr.movement = "head"`, `HALO_VR_MOVEMENT`), with `"aim"` retaining the previous behavior. It transforms local on-foot throttle after aiming and before queuing the player's action, leaving controller weapon aiming, vehicles, cinematics, menus, gamepad/flat input and remote players separate. Finite-pose/focus checks stop invalid movement; near-vertical head direction uses the last usable horizontal heading. The existing Xbox bumper mapping and physical-head tutorial hook are ported to this source.
[check-experimental-engine.py](../scripts/check-experimental-engine.py) verifies the exact checkout, clean patch applicability, these renderer/network/configuration invariants, and the actual offline ARM64 Ninja graph. Focused production C math checks cover locomotion. These checks complement the successful headset report; they do not measure comfort or hardware performance or establish co-op behavior across every network.
### Reticle and sun-glow changes
The local renderer patch previews the nominal primary shot through shared calculations from [weapon projectile creation](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/items/weapons.c) and [player projectile aiming](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/game/aim_assist.c). The preview omits target/timestamp writes and firing randomness. It uses the projectile collision mask and current hit depth instead of the controller-origin ray and smoothed reticle depth. This predicts the nominal line, not random spread or later ballistic drop. Production C checks cover the shared calculation and render integration. The tester confirmed alignment in 1.4.0a4 and confirmed the updated reticle motion in the 1.4.0a5 build used for 1.4.0.
The render-rate preview supplies an optional presentation pose for the local on-foot player: current-frame aim and the unshaken interpolated observer camera. It keeps the game's muzzle projection and trigger offsets, rather than adding physical headset/hand translations to the shot origin. The unit, weapon and aim-assist helpers share their ordinary math; real firing uses no presentation override. Limited turrets retain the authoritative aiming path; states without a valid projectile preview use the ordinary HUD. The reticle traces a new hit each rendered frame and projects it into both eyes, without interpolating previous hit points across walls. This adds no extra per-eye aim search; actual Frame timing still needs measurement.
The sun-glow patch retains the original mask/depth/blur routine and scales only its final VR composite by `vr.sun_glow_strength` (default 0.5, bounded 0–1). Non-finite settings fall back to 0.5; flat rendering uses 1.0. The production C sun tests verify asymmetric stereo projection, per-eye occlusion and source sampling, state restoration and intensity bounds.
### Online co-op menu and connection
The [pinned internet-play guide](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/README.md#internet-play) documents invite links, public server browsing, broker discovery, UDP hole punching and UPnP. It explicitly provides **no gameplay relay**: some pairs of strict NATs cannot connect without a usable forwarded port. The broker list and [configuration declarations](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/src/port_config.c#L320-L383) enable internet play and the browser by default, while `network.coop_public` defaults to false. A custom broker file and network settings remain user choices.
[Upstream menu code](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/game/menu_functions.c) implements **Create Game > Internet**, a **SINGLEPLAYER** map chooser, campaign difficulty, **Server Setup > LISTING**, **Join Game > Server Browser**, and **Direct Link > PASTE LINK**. [Cooperative server setup](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/interface/ui_widget_event_handler_functions.c#L5950-L5976) marks a selected campaign map as a network co-op game. The direct-link paste operation reads the clipboard on the machine running Halo, so the Frame's clipboard is needed, not its installer PC's.
The [local integration patch](../resources/frame-controls.patch) replaces **CO-OP CAMPAIGN** with explicit LAN/Internet host and join actions in VR and flat mode, selecting campaign maps with one local player. Its scoped map intent is cleared during synchronous widget loading; failed loading disposes the new host/client. The campaign XML and its generator are changed together so regeneration cannot restore the split-screen path. Generic profile widgets and competitive routes are retained. `tools/test_online_coop_menu.py` executes the production host/map/action helpers and XML event contracts, including failures, cancellation and single-local-player campaign behavior. These tests do not open a network connection or prove gameplay with a distant friend. [Practical hosting and joining steps](SETUP.md#online-campaign-co-op-with-a-friend).
## Game data and compatibility
[Upstream game-data instructions](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/README.md#game-data) require an **original Xbox Halo: Combat Evolved** disc image (`.iso` or `.xiso`) or its extracted maps. This installer uses the exact released-map build list in the [pinned cache compatibility table](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/source/cache/cache_files.c#L616-L631):
[Upstream game-data instructions](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/README.md#game-data) require an **original Xbox Halo: Combat Evolved** disc image (`.iso` or `.xiso`) or its extracted maps. This installer uses the exact released-map build list in the [pinned cache compatibility table](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/cache/cache_files.c#L616-L631):
| Accepted Xbox version-5 map build | Upstream region |
| --- | --- |
@@ -34,13 +63,13 @@ The installer applies [its documented controller and tutorial patch](../resource
| `01.08.15.1749` | NTSC |
| `01.01.14.2342` | PAL |
**USA Rev 2 is validated with this installer.** Other original Xbox retail revisions and regions, including an image labeled USA Rev 1 or the original USA release, are accepted only when all 24 required maps have one of these builds. [Upstream PAL conversion notes](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/game/pal_tags.c#L4-L22) identify shared NTSC maps across three releases and explain the PAL timing conversion. Those source files do not provide a definitive filename-to-Rev-label mapping; setup validates actual headers instead of trusting a filename. Incomplete or mixed-build sets and other build strings are rejected. This is a supported-data rule, not a claim that every disc pressing has been tested on a Frame.
**USA Rev 2 was validated with the stable installer.** 1.4.0 retains its map-header checks. Other original Xbox retail revisions and regions, including an image labeled USA Rev 1 or the original USA release, are accepted only when all 24 required maps have one of these builds. [Upstream PAL conversion notes](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/game/pal_tags.c#L4-L22) identify shared NTSC maps across three releases and explain the PAL timing conversion. Those source files do not provide a definitive filename-to-Rev-label mapping; setup validates actual headers instead of trusting a filename. Incomplete or mixed-build sets and other build strings are rejected. This is a supported-data rule, not a claim that every disc pressing has been tested on a Frame.
Select a local disc image or maps that you are authorized to use. The installer checks original Xbox map headers and extracts the required data locally. The repository and downloadable installer contain no game disc image or commercial map data and provide no unauthorized game-download links.
Halo PC, Custom Edition, Xbox 360 Anniversary and MCC data are different formats. This is the native **Steam Frame** Linux VR port; the Android port in the upstream repository is not a promise of a Quest VR installation. PC Chimera, PC Restored map replacements and HaloCEVR DLLs are not loaded by this ARM64 build. Its native renderer and VR implementation supply the relevant graphics and headset support. PC Halo saves and profiles also differ from the native port's Xbox formats.
[Upstream multiplayer](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/README.md#multiplayer) supports LAN and internet play among compatible builds of this port. The [netcode design](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/NETCODE.md) describes its replacement networking. Use matching native/OpenCE protocol versions; compatibility with legacy Halo PC, Custom Edition, MCC or retail Xbox servers is not established by this project.
[Upstream multiplayer](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/README.md#multiplayer) supports LAN and internet play among compatible builds of this port. The [netcode design](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/NETCODE.md) describes its replacement networking. Use matching native/OpenCE protocol versions; compatibility with legacy Halo PC, Custom Edition, MCC or retail Xbox servers is not established by this project.
## Steam Frame setup and connection
@@ -105,8 +134,9 @@ The Windows installer uses the classic Halo PC helmet-and-shoulders icon in
1.3.2. [HaloNet's favicon ICO](https://halonet.net/favicon.ico) is byte-identical
to icon group 102 of the locally installed retail Halo PC executable. Only the
icon is bundled; its [resource notice](../resources/ui/README.md) records the
verification and exact hashes. The README displays only the banner and title
logo, with the installer icon separate from Steam's retained project drawing.
verification and exact hashes. The README displays the banner, title logo and
credited community gameplay example below, with the installer icon separate
from Steam's retained project drawing.
The bundled portrait image is the original **Halo: Combat Evolved** cover from [Halopedia's cover-art file page](https://www.halopedia.org/File:HCE_Cover_Art.jpg), with the [original JPEG](https://www.halopedia.org/images/8/8e/HCE_Cover_Art.jpg). Halopedia records the [official Halo Facebook image](https://www.facebook.com/Halo/photos/a.137195553028391/1561119617302637/) as its source. The landscape library image arranges the same cover in a wider layout without stretching it.
@@ -120,10 +150,14 @@ The new adapter's artwork type mapping, `SetCustomArtworkForApp`, `SetShortcutIc
[Valve's June 2023 Steam update](https://store.steampowered.com/news/app/593110/view/3687931965598906184), also available in its [official announcement feed](https://store.steampowered.com/news/posts/?enddate=1687195871&feed=steam_blog), introduces rich-text Notes accessible from a game's details page and overlay, with Steam synchronization. This patch uses Notes for an original Halo summary, Xbox controls, compatibility guidance, and source links. It does not replace Steam's non-Steam About text or create official store metadata.
The helper matches the current account and exact native executable before writing, synchronizes before reading, preserves the full existing document, guards against intervening changes, saves one identified note, and verifies the result. User-edited installer notes remain untouched. Unsupported schemas, account/name ambiguity, or synchronization failures produce a manual, copyable description. Notes use Steam's normal private-note synchronization; setup sends no description to another game's app ID or outside service. The banner, logo, icon, and Notes have automated tests but still need verification on a physical Frame.
The helper matches the current account and exact native executable before writing, synchronizes before reading, preserves the full existing document, guards against intervening changes, saves one identified note, and verifies the result. User-edited installer notes remain untouched. Unsupported schemas, account/name ambiguity, or synchronization failures produce a manual, copyable description. Notes use Steam's normal private-note synchronization; setup sends no description to another game's app ID or outside service. The banner, logo, icon, and Notes integration have automated tests.
## README gameplay example
The [README image](images/halocevr-steam-frame-example.jpeg) is the unchanged screenshot posted by [u/Sea-Communication760](https://www.reddit.com/r/virtualreality/comments/1wpvi8n/halo_halocevr_working_great_on_steam_frame/) in **Halo (HaloCEVR). Working great on Steam Frame standalone. (Proton ARM)**. It illustrates the PC HaloCEVR mod running on Steam Frame through Proton ARM, not this project's native 1.4.0 build. The original image, exact hash and rights notice are recorded in [the image source notice](images/README.md).
## Local Halo menu music
The preview reads only the `sound\music\title1\loops` sound tag from the user's selected original Xbox `ui.map`, either inside an ISO/XISO or in extracted maps. Cache and sound structures are based on the pinned native port's [cache_files.c](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/source/cache/cache_files.c) and [sound_definitions.h](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/source/sound/sound_definitions.h). Xbox ADPCM decoding follows its [dsound_sdl.c implementation](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/linux/src/dsound_sdl.c).
The installer reads only the `sound\music\title1\loops` sound tag from the user's selected original Xbox `ui.map`, either inside an ISO/XISO or in extracted maps. Cache and sound structures are based on the pinned native port's [cache_files.c](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/cache/cache_files.c) and [sound_definitions.h](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/source/sound/sound_definitions.h). Xbox ADPCM decoding follows its [dsound_sdl.c implementation](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/linux/src/dsound_sdl.c).
The installer decodes the loop into a private temporary Windows WAV with a fixed 12% sample-amplitude gain. Playback loops automatically after local game data validates. Closing the installer stops playback and clears its temporary audio. No soundtrack is present in the repository or executable, fetched from a website, or uploaded over SSH. Audio-file extensions are rejected by the release packager. Remote-only repair has no local audio preview, and an unavailable preview does not block installation. The original music remains copyrighted game content; the installer's code license does not apply to it. Playback still needs a listening check.
+9 -5
View File
@@ -53,14 +53,14 @@ The Windows download contains the mod installer and build instructions; no retai
| Upstream component | Notice/reference |
| --- | --- |
| [Halo native port / decompilation](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/LICENSE.md) | CC0-1.0 for the upstream source. This does not grant rights in the commercial game data or third parties' rights. |
| [Halo native port / decompilation](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/LICENSE.md) | CC0-1.0 for the upstream source. This does not grant rights in the commercial game data or third parties' rights. |
| [Local controller/tutorial patch](../resources/frame-controls.patch) | A documented modification of the pinned upstream source; see [CONTROLS.md](CONTROLS.md). |
| [SDL 3.4.16](https://github.com/libsdl-org/SDL/blob/release-3.4.16/LICENSE.txt) | zlib license; retain the original notice with the native library. |
| [musl 1.2.5](https://git.musl-libc.org/cgit/musl/tree/COPYRIGHT?h=v1.2.5) | MIT and notices identified in its COPYRIGHT file. Used for the upstream ILP32 guest. |
| [Khronos OpenXR SDK headers](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/third_party/openxr/README.md) | OpenXR SDK 1.1.63, Apache-2.0 OR MIT. Header provenance is documented upstream. |
| [mbedTLS, expat, miniupnpc, KCP, tomlc17 and stb](https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106/port/third_party) | Their individual upstream license files apply. Preserve the actual files when producing the native installation. |
| [extract-xiso](https://github.com/startupfoundry/halo-ce-universal/blob/88142798513ebd99fc7c6224023e8b44c05d0106/port/third_party/extract-xiso/LICENSE.TXT) | Custom BSD-style notice with acknowledgement and attribution conditions. This product includes software developed by in <in@fishtank.com>. |
| [Upstream font resources](https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106/port/assets/fonts) | Retain Overpass's SIL Open Font License and Newtown's public-domain notice when using those upstream resources. |
| [Khronos OpenXR SDK headers](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/third_party/openxr/README.md) | OpenXR SDK 1.1.63, Apache-2.0 OR MIT. Header provenance is documented upstream. |
| [mbedTLS, expat, miniupnpc, KCP, tomlc17 and stb](https://github.com/startupfoundry/halo-ce-universal/tree/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/third_party) | Their individual upstream license files apply. Preserve the actual files when producing the native installation. |
| [extract-xiso](https://github.com/startupfoundry/halo-ce-universal/blob/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/third_party/extract-xiso/LICENSE.TXT) | Custom BSD-style notice with acknowledgement and attribution conditions. This product includes software developed by in <in@fishtank.com>. |
| [Upstream font resources](https://github.com/startupfoundry/halo-ce-universal/tree/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/assets/fonts) | Retain Overpass's SIL Open Font License and Newtown's public-domain notice when using those upstream resources. |
The rootless build environment installs [Ubuntu packages](https://ubuntu.com/legal/open-source) and [LLVM](https://llvm.org/docs/DeveloperPolicy.html#license) under their own licenses inside its container. The installer does not relicense those tools or SteamOS/SteamVR. Game artwork, names and data remain the property of their respective rights holders.
@@ -80,6 +80,10 @@ Existing library art and icons are retained, including custom images with anothe
The Steam Notes description is original summary and control-guide text, not copied store text. Notes remain a private Steam feature governed by Steam's own behavior; no Steam client code is redistributed by the helper. Observed client interfaces are credited in [SOURCES.md](SOURCES.md#description-and-controls-in-steam-notes).
## README gameplay image
`docs/images/halocevr-steam-frame-example.jpeg` is an unchanged community screenshot by [u/Sea-Communication760](https://www.reddit.com/r/virtualreality/comments/1wpvi8n/halo_halocevr_working_great_on_steam_frame/), showing the PC HaloCEVR mod on Steam Frame through Proton ARM. It is labeled as a community example and is not a native 1.4.0 capture. The screenshot and underlying Halo content retain their respective rights; the installer's MIT code license does not apply to this image. See [the image source notice](images/README.md) for its original URL and SHA256.
## User-supplied menu audio
The original Halo menu music is not bundled with the installer. After the user selects valid local original Xbox data, setup reads the menu loop from that game's `ui.map`, decodes it quietly to a private temporary WAV, and automatically plays it locally. It is not downloaded or uploaded to the Frame, and its temporary decoded file is cleared on close. Repair with only remote maps does not supply a preview. The original music retains its rights holders' copyright and is separate from the installer's MIT code license.
+10
View File
@@ -0,0 +1,10 @@
# README gameplay image source
`halocevr-steam-frame-example.jpeg` is an unchanged 1,920 × 1,080 JPEG screenshot by **u/Sea-Communication760**, retrieved on 2026-10-06 from their [HaloCEVR Steam Frame post](https://www.reddit.com/r/virtualreality/comments/1wpvi8n/halo_halocevr_working_great_on_steam_frame/).
- Original image: [Reddit-hosted JPEG](https://i.redd.it/u721sjfvxnrh1.jpeg).
- SHA256: `612f7d0471940fd1fba4e7b0f6052384a69164e401729ce7e95a2c0d62c562f0`.
- Depicted software: the PC **HaloCEVR** mod running on Steam Frame through **Proton ARM**, as identified by the original post. This is not a capture of this project's native 1.4.0 build.
- Rights: the screenshot and underlying Halo content retain their respective rights. The project's MIT code license does not apply to this image; no open image license is claimed.
The local copy keeps the README image available without depending on the external image host. No pixels were cropped, resized or retouched; the README only sets its displayed width.
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "halo-steam-frame-installer"
version = "1.3.5"
version = "1.4.0"
description = "Community Halo CE VR mod installer for Steam Frame; original Xbox game data must be supplied separately"
requires-python = ">=3.11"
license = "MIT"
+5
View File
@@ -28,6 +28,11 @@ for tool in clang ld.lld llvm-ar; do
done
clang --version
cd /build/src
phase configure 'Checking Xbox controls, render-rate reticle, stereo sun glow and LAN/online campaign menus...'
python3 tools/test_vr_locomotion.py --cc clang
python3 tools/test_vr_sun_glow.py --cc clang
python3 tools/test_vr_projectile_reticle.py --cc clang
python3 tools/test_online_coop_menu.py --cc clang
phase configure 'Configuring the native ARM64 OpenXR game and downloading its build sources...'
python3 configure.py --release --vr --linux-arm64-cc clang
export CMAKE_BUILD_PARALLEL_LEVEL=4
File diff suppressed because it is too large. Load diff
+4 -26
View File
@@ -106,15 +106,15 @@
},
{
"name": "halo-steam-frame-installer",
"version": "1.3.5",
"version": "1.4.0",
"license_expression": "MIT",
"declared_license": null,
"project_urls": [],
"notices": [
{
"path": "packages/halo-steam-frame-installer/halo_steam_frame_installer-1.3.5.dist-info/licenses/LICENSE",
"sha256": "b19db6d1e8942380f6a0dc3b9ec39cec802606c9d1c08a6050eae2990568afbb",
"bytes": 1318
"path": "packages/halo-steam-frame-installer/halo_steam_frame_installer-1.4.0.dist-info/licenses/LICENSE",
"sha256": "04a51df2edd89917e3099e2f6f171c36891f1d6ac234015867b57a1d10f928d5",
"bytes": 1343
}
]
},
@@ -215,28 +215,6 @@
}
]
},
{
"name": "pillow",
"version": "12.3.0",
"license_expression": "MIT-CMU",
"declared_license": null,
"project_urls": [
"Release notes, https://pillow.readthedocs.io/en/stable/releasenotes/index.html",
"Changelog, https://github.com/python-pillow/Pillow/releases",
"Documentation, https://pillow.readthedocs.io",
"Funding, https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi",
"Homepage, https://python-pillow.github.io",
"Mastodon, https://fosstodon.org/@pillow",
"Source, https://github.com/python-pillow/Pillow"
],
"notices": [
{
"path": "packages/pillow/pillow-12.3.0.dist-info/licenses/LICENSE",
"sha256": "4f7866a74802c6326f81faff59a56546b6aec2b10b91973e0e9308de95e79857",
"bytes": 78016
}
]
},
{
"name": "pip",
"version": "25.0.1",
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +0,0 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
@@ -1,25 +1,25 @@
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
MIT License
Copyright (c) 2026 Halo Steam Frame Installer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers original installer code. Refer to docs/THIRD_PARTY.md
and resources/licenses for upstream code, patch, dependencies, and notices.
Commercial Halo game data is not included or licensed by this project.
+144 -23
View File
@@ -23,7 +23,8 @@ import tomllib
OWNER = "halo-frame-installer"
SOURCE_URL = "https://github.com/startupfoundry/halo-ce-universal.git"
SOURCE_COMMIT = "88142798513ebd99fc7c6224023e8b44c05d0106"
SOURCE_COMMIT = "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4"
LEGACY_SOURCE_COMMIT = "88142798513ebd99fc7c6224023e8b44c05d0106"
MIN_FREE_BYTES = 12 * 1024 ** 3
EXPECTED_MAPS = frozenset("maps/" + name + ".map" for name in (
"a10", "a30", "a50", "b30", "b40", "c10", "c20", "c40", "d20", "d40",
@@ -179,6 +180,62 @@ def installed_maps_manifest():
return value
def save_root():
return GAME / "save-v1.4"
def configured_save_path(settings):
"""Inspect a legacy location without opening or modifying its contents."""
paths = settings.get("paths", {})
if not isinstance(paths, dict):
raise ValueError("The game configuration has an unsupported paths section.")
location = paths.get("saves", str(GAME / "save"))
if not isinstance(location, str) or not location:
raise ValueError("The configured save location is invalid. Existing saves were kept.")
selected = pathlib.Path(location)
selected = pathlib.Path(os.path.normpath(selected if selected.is_absolute() else GAME / selected))
if selected == GAME:
raise ValueError("The save location points at the entire game folder. Existing saves were kept.")
if selected.is_relative_to(GAME):
beneath(selected, GAME)
return str(selected)
def previous_save_paths(metadata):
paths = metadata.get("previousSavePaths", [])
if not isinstance(paths, list) or len(paths) > 32:
raise ValueError("The native marker has an invalid previous save-location record.")
checked = []
for location in paths:
selected = configured_save_path({"paths": {"saves": location}})
if selected not in checked:
checked.append(selected)
return checked
def validate_save_paths(settings, require_explicit=False):
"""New engine checkpoints must never share the incompatible legacy directory."""
paths = settings.get("paths", {})
if not isinstance(paths, dict):
raise ValueError("Version 1.4 game and save paths must use their owned installation folder.")
for key, expected in (("data", GAME), ("saves", save_root())):
beneath(expected, HOME)
if key not in paths and not require_explicit:
continue
value = paths.get(key)
if not isinstance(value, str) or not value:
raise ValueError("Repair the game to set its version 1.4 game and save paths before reuse.")
candidate = pathlib.Path(value)
if not candidate.is_absolute():
candidate = GAME / candidate
try:
beneath(candidate, GAME)
except ValueError:
raise ValueError("Version 1.4 game and save paths must stay in their owned installation folder. Existing saves were kept.") from None
if candidate != expected:
raise ValueError("Version 1.4 must use its own game folder and save-v1.4 subfolder. Existing saves were kept.")
def existing_install(repair=False, adopt=False):
beneath(GAME, HOME)
if not GAME.exists():
@@ -201,17 +258,39 @@ def existing_install(repair=False, adopt=False):
installed_maps_manifest()
value = {"owner": "manual-native-adoption", "files": {}}
source = None
if source != SOURCE_COMMIT and not repair:
needs_upgrade = source == LEGACY_SOURCE_COMMIT and value["owner"] in (OWNER, "codex-halo-native-frame-20261005")
if source != SOURCE_COMMIT and not repair and not needs_upgrade:
raise ValueError("An existing native game uses a different source revision. It was not modified.")
if repair:
if repair or needs_upgrade:
game_closed()
else:
if not repair:
elf_arm64(GAME / "halo")
ordinary(GAME / "libSDL3.so.0")
if needs_upgrade:
elf_arm64(GAME / "libSDL3.so.0")
else:
ordinary(GAME / "libSDL3.so.0")
previous_paths = previous_save_paths(value)
migrating = source != SOURCE_COMMIT
beneath(save_root(), GAME)
if save_root().exists() and (not save_root().is_dir() or (migrating and any(save_root().iterdir()))):
raise ValueError("The version 1.4 save destination already contains files or is not a directory. Existing saves were kept.")
if (GAME / "config.toml").exists():
ordinary(GAME / "config.toml")
settings = tomllib.loads((GAME / "config.toml").read_text())
if migrating:
previous = configured_save_path(settings)
if previous not in previous_paths:
previous_paths.append(previous)
else:
validate_save_paths(settings, require_explicit=not repair)
if repair:
merge_config((GAME / "config.toml").read_text())
merge_config((GAME / "config.toml").read_text(), migrate=migrating)
elif not repair and not needs_upgrade:
raise ValueError("Repair the game to create its version 1.4 save configuration before reuse.")
elif migrating and str(GAME / "save") not in previous_paths:
previous_paths.append(str(GAME / "save"))
if len(previous_paths) > 32:
raise ValueError("The native marker exceeds the supported previous save-location count.")
# Validate all hashes recorded by either supported installer before reusing it.
files = value.get("files", {})
if not isinstance(files, dict):
@@ -236,7 +315,8 @@ def existing_install(repair=False, adopt=False):
raise
maps_verified = False
return {"gamePath": str(GAME), "reused": True, "owner": value["owner"], "sourceCommit": source,
"mapsVerified": maps_verified, "needsImage": not maps_verified, "programIssues": issues}
"mapsVerified": maps_verified, "needsImage": not maps_verified, "programIssues": issues,
"needsUpgrade": needs_upgrade, "previousSavePaths": previous_paths}
def os_release():
@@ -288,7 +368,7 @@ def preflight(repair=False, adopt=False):
raise ValueError("An installer resource path is not a private ordinary file.")
existing = existing_install(repair, adopt)
free = shutil.disk_usage(HOME).free
if (existing is None or repair) and free < MIN_FREE_BYTES:
if (existing is None or repair or existing.get("needsUpgrade")) and free < MIN_FREE_BYTES:
raise ValueError("At least 12 GiB of free space is needed for the Xbox data and isolated build. Free space and retry.")
# Podman itself is unprivileged. Never use sudo or disable SteamOS read-only protection.
podman = json.loads(command(["podman", "info", "--format", "json"]))
@@ -331,6 +411,8 @@ def preflight_library():
current = existing_install()
if current is None:
raise ValueError("Install and verify native Halo VR before updating its Steam information.")
if current.get("needsUpgrade"):
raise ValueError("Use Install to upgrade the existing native game to version 1.4 before updating its Steam information.")
info.pop("uninstallSupported", None)
info["existing"] = current
return info
@@ -745,7 +827,10 @@ class BuildLogMonitor:
def build(value, repair=False, adopt=False):
directory = run_dir(value)
metadata = read_marker(directory / MARKER)
if existing_install(repair, adopt) and not repair:
existing = existing_install(repair, adopt)
if existing and not repair:
if existing.get("needsUpgrade"):
raise ValueError("The existing native game needs a version 1.4 upgrade. Start Install to rebuild it.")
return {"reused": True}
upload = directory / "upload"
def check_cancelled():
@@ -857,12 +942,18 @@ def cancel(value):
return {"cancelled": True}
def merge_config(original):
def merge_config(original, migrate=False):
"""Restore required native controls while preserving unrelated TOML settings."""
settings = tomllib.loads(original) if original.strip() else {}
required = {"vr": {"enabled": True, "aim": "controller", "melee_gesture": False},
"update": {"auto": False}}
defaults = {"refresh_rate": 72.0, "resolution_scale": 1.0, "turn": "smooth",
if migrate:
configured_save_path(settings)
else:
validate_save_paths(settings)
required = {"vr": {"enabled": True, "aim": "controller", "movement": "head", "melee_gesture": False},
"update": {"auto": False}, "paths": {"data": str(GAME), "saves": str(save_root())}}
if "coop_enemies_mode" not in settings.get("network", {}):
required["network"] = {"coop_enemies_mode": "none"}
defaults = {"refresh_rate": 72.0, "resolution_scale": 1.0, "sun_glow_strength": 0.5, "turn": "smooth",
"smooth_turn_speed": 90.0, "two_handed": True, "height": "seated", "depth": False}
vr = settings.get("vr", {})
for key, default in defaults.items():
@@ -966,6 +1057,8 @@ def finalize(value, repair=False, adopt=False):
check_cancelled()
existing = existing_install(repair, adopt)
if existing and not repair:
if existing.get("needsUpgrade"):
raise ValueError("The existing native game needs a version 1.4 upgrade. Start Install to rebuild it.")
return existing
source = directory / "src"
binary = source / "build/linux_arm64/halo"
@@ -977,6 +1070,16 @@ def finalize(value, repair=False, adopt=False):
shutil.copy2(binary, stage / "halo")
(stage / "halo").chmod(0o755)
shutil.copy2(ordinary(source / "build/linux_arm64/libSDL3.so.0"), stage / "libSDL3.so.0")
broker_source = ordinary(source / "build/linux_arm64/brokers.txt")
if not 0 < broker_source.stat().st_size <= 16 * 1024:
raise ValueError("The native internet-play broker list is missing or invalid.")
broker_text = broker_source.read_text(encoding="utf-8")
if not any(
line.strip() and not line.lstrip().startswith("#") for line in broker_text.splitlines()):
raise ValueError("The native internet-play broker list is missing or invalid.")
shutil.copy2(broker_source, stage / "brokers.txt")
if digest(stage / "brokers.txt") != digest(broker_source):
raise ValueError("The native internet-play broker list failed copy verification.")
upload = directory / "upload"
manifest = json.loads(ordinary(upload / "xbox-data-manifest.json").read_text())
reuse_maps = run_metadata.get("mapsOrigin") == "existing"
@@ -992,15 +1095,15 @@ def finalize(value, repair=False, adopt=False):
if not existing or not reuse_maps:
verify_maps(stage, manifest, check_cancelled)
if not existing:
(stage / "save").mkdir()
config = ('[paths]\ndata = "' + str(GAME) + '"\nsaves = "' + str(GAME / "save") + '"\n\n'
'[update]\nauto = false\n\n[network]\nonline = true\n\n'
'[vr]\nenabled = true\nrefresh_rate = 72.0\nresolution_scale = 1.0\n'
'aim = "controller"\nturn = "smooth"\nsmooth_turn_speed = 90.0\n'
(stage / save_root().name).mkdir()
config = ('[paths]\ndata = ' + json.dumps(str(GAME)) + '\nsaves = ' + json.dumps(str(save_root())) + '\n\n'
'[update]\nauto = false\n\n[network]\nonline = true\ncoop_enemies_mode = "none"\n\n'
'[vr]\nenabled = true\nrefresh_rate = 72.0\nresolution_scale = 1.0\nsun_glow_strength = 0.5\n'
'aim = "controller"\nmovement = "head"\nturn = "smooth"\nsmooth_turn_speed = 90.0\n'
'melee_gesture = false\ntwo_handed = true\nheight = "seated"\ndepth = false\n')
if existing:
original = ordinary(GAME / "config.toml").read_text() if (GAME / "config.toml").exists() else ""
config = merge_config(original)
config = merge_config(original, migrate=existing["sourceCommit"] != SOURCE_COMMIT)
(stage / "config.toml").write_text(config)
shutil.copy2(CACHE / "resources/frame-controls.patch", stage / "frame-controls.patch")
notices = {
@@ -1027,12 +1130,20 @@ def finalize(value, repair=False, adopt=False):
raise ValueError("A required native runtime library is missing:\n" + libs)
metadata = {"owner": OWNER, "version": 1, "sourceCommit": SOURCE_COMMIT,
"sourceUrl": SOURCE_URL, "architecture": "native ARM64 ELF64 / ILP32 AArch64 guest",
"files": {name: digest(stage / name) for name in ("halo", "libSDL3.so.0", "frame-controls.patch")},
"files": {name: digest(stage / name) for name in ("halo", "libSDL3.so.0", "brokers.txt", "frame-controls.patch")},
"maps": {"files": 24, "bytes": manifest["totalBytes"]},
"controls": "Xbox buttons, motion aim, LB grenade change, RB flashlight",
"controls": "Head-directed walking, Xbox buttons, motion aim, LB grenade change, RB flashlight",
"installerVersion": "1.4.0", "networkProtocol": 17, "saveRoot": str(save_root()),
"previousSavePaths": existing.get("previousSavePaths", []) if existing else [],
"buildLog": str(directory / "native-build.log")}
(stage / MARKER).write_text(json.dumps(metadata, indent=2) + "\n")
if existing:
# This directory contains only new-format checkpoints. Program rollback
# restores the old config and leaves both generations of saves untouched.
beneath(save_root(), GAME).mkdir(exist_ok=True)
if existing.get("previousSavePaths"):
progress("upgrade", "Previous checkpoints remain in " + ", ".join(existing["previousSavePaths"])
+ ". Version 1.4 uses fresh campaign saves in " + str(save_root()) + ".")
names = [item.name for item in stage.iterdir() if item.is_file() and item.name != MARKER]
if not reuse_maps:
names += [item["path"] for item in manifest["files"]]
@@ -1040,7 +1151,8 @@ def finalize(value, repair=False, adopt=False):
names.append(MARKER)
backup = repair_program_files(stage, directory, names, check_cancelled)
return {"gamePath": str(GAME), "reused": False, "repaired": True,
"sourceCommit": SOURCE_COMMIT, "backupPath": backup, "mapsReinstalled": not reuse_maps}
"sourceCommit": SOURCE_COMMIT, "backupPath": backup, "mapsReinstalled": not reuse_maps,
"previousSavePaths": existing.get("previousSavePaths", [])}
beneath(GAME.parent, HOME).mkdir(exist_ok=True)
if GAME.exists() or GAME.is_symlink():
raise ValueError("The game destination appeared during installation. It was not modified.")
@@ -1069,6 +1181,8 @@ def shortcut(close_steam=False):
current = existing_install()
if current is None:
raise ValueError("Install and verify the native game before adding it to Steam.")
if current.get("needsUpgrade"):
raise ValueError("Use Install to upgrade the existing native game to version 1.4 before adding it to Steam.")
sys.path.insert(0, str(CACHE / "resources"))
from steam_shortcut import add_native_shortcut
response = add_native_shortcut(HOME, GAME, close_steam=close_steam)
@@ -1200,7 +1314,14 @@ def uninstall_inventory(directory):
def uninstall_save_files(inventory):
roots, external = [GAME / "save"], []
roots, external = [GAME / "save", save_root()], []
metadata = uninstall_identity()
for location in previous_save_paths(metadata or {}):
selected = pathlib.Path(location)
if selected.is_relative_to(GAME):
roots.append(beneath(selected, GAME))
else:
external.append(str(selected))
config = GAME / "config.toml"
if "config.toml" in inventory:
if config.stat().st_size > MAX_MANIFEST_BYTES:
+122 -13
View File
@@ -10,7 +10,9 @@ from steam_live import Client, GAME, HOME, NAME, expression, native_spec
NOTE_ID = "haloframeaboutv1"
NOTE_TITLE = "Halo CE VR — About & controls"
NOTE_CONTENT = """[h1]Halo: Combat Evolved VR (Native)[/h1]
# Exact unchanged 1.4.0a2 note, verified from its local source ZIP.
# UTF-8 SHA256: a4e1776caa60548620e0510acbe194aa414645be8d042771d9cbe1ec49edc05e
A2_NOTE_CONTENT = """[h1]Halo: Combat Evolved VR (Experimental)[/h1]
Play the original 2001 Halo: Combat Evolved campaign as Master Chief, fighting
the Covenant and uncovering the secrets of a mysterious ringworld.
@@ -21,7 +23,7 @@ vary by scene, headset software, and settings.
[h2]Xbox-style Frame controls[/h2]
Point the right controller to aim; move your head to look around.
Left stick: move. Left stick click: crouch.
Left stick: move relative to your head direction. Left stick click: crouch.
Right stick left/right: turn. Right stick click: zoom.
Right trigger: fire. Left trigger: throw grenade.
A: jump; accept menu selections. B: melee; go back in menus.
@@ -36,14 +38,110 @@ VR options are available in Settings > VR Setup.
[h2]Multiplayer[/h2]
LAN and internet play require compatible native/OpenCE protocol builds.
This experimental build uses protocol 17; stable protocol 11 cannot join it.
For campaign co-op, host a LAN or internet game and choose a SINGLEPLAYER map.
The experimental game and saves use a separate folder. Older checkpoints
cannot be loaded. Extra co-op enemies are disabled by default.
Halo PC, Custom Edition, MCC, and retail Xbox server compatibility is not
provided by this installer.
[h2]Sources and setup help[/h2]
[url=https://github.com/ClassicWasTaken/HaloCE-SteamFrame]Installer, controls, and troubleshooting[/url]
[url=https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106]Pinned native port source[/url]
[url=https://github.com/ClassicWasTaken/HaloSteamFrameMod]Stable installer project and sources[/url]
[url=https://github.com/startupfoundry/halo-ce-universal/tree/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4]Pinned native port source[/url]
"""
A4_NOTE_CONTENT = A2_NOTE_CONTENT.replace(
"""[h2]Multiplayer[/h2]
LAN and internet play require compatible native/OpenCE protocol builds.
This experimental build uses protocol 17; stable protocol 11 cannot join it.
For campaign co-op, host a LAN or internet game and choose a SINGLEPLAYER map.
The experimental game and saves use a separate folder. Older checkpoints
cannot be loaded. Extra co-op enemies are disabled by default.
Halo PC, Custom Edition, MCC, and retail Xbox server compatibility is not
provided by this installer.
""",
"""[h2]Online campaign co-op with a friend[/h2]
LAN and internet play require compatible native/OpenCE protocol builds.
Both players should install the same experimental version and supply their
own supported original Xbox maps. This build uses protocol 17; stable
protocol 11, Halo PC, Custom Edition, MCC and retail Xbox clients cannot join.
Host: in an active headset, Multiplayer > CO-OP CAMPAIGN opens online
hosting with SINGLEPLAYER campaign maps. Choose a level, then difficulty.
In Server Setup, set LISTING to PUBLIC for controller-only discovery.
PUBLIC lets anyone find and join the game; PRIVATE is the default.
Choose START GAME. Friend: Multiplayer > Join Game > Server Browser,
REFRESH if needed, select the host's game and press A. When both players
appear in the lobby, wait for the countdown or select START NOW.
Use the left d-pad to navigate/change row choices, A to accept, B to go back.
Each player uses their own headset; no local second controller is required.
For invite-only play, keep PRIVATE. On the host, select the INVITE LINK row
and press A to copy its halo://join/ link; share it yourself. The friend
copies it on their Frame, then uses Join Game > Direct Link > PASTE LINK
and selects the host's game. A PC clipboard is separate from the Frame's.
An invite stops working after its hosting game quits.
Internet play uses direct UDP with signaling brokers, not a gameplay relay.
Strict NAT/CGNAT can prevent joining even when a listing is visible. UPnP,
hosting from the other network, or forwarding a fixed network.tunnel_port
on an accessible router may help; distant-peer VR co-op remains unverified.
The experimental game/saves stay separate; older checkpoints cannot load.
Extra co-op enemies are disabled by default. For the original menu route,
use Create Game > Internet and change the Map chooser to SINGLEPLAYER.
Flat-screen CO-OP CAMPAIGN retains local split-screen.
""")
# Exact previously shipped a3/a4 guide; upgrade only unchanged managed notes.
# UTF-8 SHA256: 55785ec794857e844722ba758a4ce47fdb607a6bba2c30f23a437dd8f320ae30
# Exact unchanged stable 1.3.5 note; UTF-8 SHA256: ceed0d2d403fa18904c5d529ae409c8bfedad733fa1f1a7d93a6e2c2fdb8d32a
STABLE_NOTE_CONTENT = "[h1]Halo: Combat Evolved VR (Native)[/h1]\nPlay the original 2001 Halo: Combat Evolved campaign as Master Chief, fighting\nthe Covenant and uncovering the secrets of a mysterious ringworld.\n\nThis independent community installation uses your original Xbox game data\nwith an experimental native ARM64 Linux/OpenXR VR port on Steam Frame.\nIt is unaffiliated with Microsoft, Bungie, or Valve. VR support and performance\nvary by scene, headset software, and settings.\n\n[h2]Xbox-style Frame controls[/h2]\nPoint the right controller to aim; move your head to look around.\nLeft stick: move. Left stick click: crouch.\nRight stick left/right: turn. Right stick click: zoom.\nRight trigger: fire. Left trigger: throw grenade.\nA: jump; accept menu selections. B: melee; go back in menus.\nX: reload/use; hold for context actions such as exchanging a weapon.\nY: change weapon. Left bumper: change grenade type.\nRight bumper: toggle flashlight. Left controller d-pad: navigate menus.\nView: scoreboard/back. Menu: pause.\nBoth grips held: recenter the headset view; release a held foregrip first.\nLeft grip near a long gun's foregrip: aim the gun with both hands.\nThese controls assume the game's default profile button layout.\nVR options are available in Settings > VR Setup.\n\n[h2]Multiplayer[/h2]\nLAN and internet play require compatible native/OpenCE protocol builds.\nHalo PC, Custom Edition, MCC, and retail Xbox server compatibility is not\nprovided by this installer.\n\n[h2]Sources and setup help[/h2]\n[url=https://github.com/ClassicWasTaken/HaloCE-SteamFrame]Installer, controls, and troubleshooting[/url]\n[url=https://github.com/startupfoundry/halo-ce-universal/tree/88142798513ebd99fc7c6224023e8b44c05d0106]Pinned native port source[/url]\n"
PREVIOUS_NOTE_CONTENTS = (A2_NOTE_CONTENT, A4_NOTE_CONTENT, STABLE_NOTE_CONTENT)
NOTE_CONTENT = (A4_NOTE_CONTENT[:A4_NOTE_CONTENT.index("[h2]Online campaign co-op with a friend[/h2]")]
+ """[h2]LAN and online campaign co-op[/h2]
LAN and internet play require compatible native/OpenCE protocol builds.
Both players should install the same experimental version and supply their
own supported original Xbox maps. This build uses protocol 17; stable
protocol 11, Halo PC, Custom Edition, MCC and retail Xbox clients cannot join.
Open Multiplayer > CO-OP CAMPAIGN. Each machine uses one local player.
On the same network: HOST LOCAL (LAN) and JOIN LOCAL (LAN).
Across different networks: HOST ONLINE (INTERNET) and JOIN ONLINE (INTERNET).
Choose a campaign level and difficulty on the host, then START GAME.
The friend selects the host's game; REFRESH if needed. Once both names
appear in the lobby, wait for the countdown or select START NOW.
Use the left d-pad to navigate/change choices, A to accept, B to go back.
Online hosting is PRIVATE by default. For server-browser discovery, set
LISTING to PUBLIC in Server Setup. PUBLIC lets anyone find and join.
For invite-only play, keep PRIVATE, select the INVITE LINK row and press A
to copy its halo://join/ link. Share it yourself. The friend copies it on
their Frame, selects JOIN INVITE LINK, then PASTE LINK and the host's game.
A PC clipboard is separate from the Frame's. The invite expires when its
hosting game quits. Each player uses their own headset or computer.
Internet play uses direct UDP with signaling brokers, not a gameplay relay.
Strict NAT/CGNAT can prevent joining even when a listing is visible. UPnP,
hosting from the other network, or forwarding a fixed network.tunnel_port
on an accessible router may help; distant-peer VR co-op remains unverified.
The experimental game/saves stay separate; older checkpoints cannot load.
Extra co-op enemies are disabled by default.
"""
+ A4_NOTE_CONTENT[A4_NOTE_CONTENT.index("[h2]Sources and setup help[/h2]"):])
# Public 1.4.0 uses the existing native shortcut and preserves older checkpoints.
NOTE_CONTENT = (NOTE_CONTENT
.replace("Halo: Combat Evolved VR (Experimental)", "Halo: Combat Evolved VR (Native)")
.replace("with an experimental native ARM64", "with a native ARM64")
.replace("the same experimental version", "the same 1.4.0 release")
.replace("This build uses protocol 17; stable", "1.4.0 uses protocol 17; 1.3.5")
.replace("The experimental game/saves stay separate; older checkpoints cannot load.",
"1.4.0 upgrades the existing native game. New profiles and saves use\n"
"save-v1.4 inside its game folder. Your old save folder is retained\n"
"unchanged; older checkpoints cannot load in the newer engine.")
.replace("Stable installer project and sources", "Installer project and sources"))
MAX_DOCUMENT_BYTES = 1024 * 1024
MAX_NOTES = 512
@@ -60,6 +158,10 @@ NOTES = r"""(async () => {
!user || typeof user.accountName !== 'string' || !user.accountName ||
typeof TextEncoder !== 'function')
return manual('Steam Game Notes is unavailable. Add the supplied text as a note manually.');
if (!Array.isArray(S.previousContents) || S.previousContents.length > 4 ||
S.previousContents.some(content => typeof content !== 'string' ||
new TextEncoder().encode(content).length > 16384))
return manual('The previous installer note list is invalid. Existing notes were preserved.');
const accountName = user.accountName;
async function bounded(value, limit=3500) {
let timer;
@@ -136,18 +238,23 @@ NOTES = r"""(async () => {
const doc=parse(first,true);
const matches=doc.notes.filter(note => note.id === S.noteId);
if (matches.length > 1) throw new Error('The Halo notes identifier is ambiguous');
let previous;
if (matches.length) {
await identity();
if (matches[0].title !== S.title || matches[0].content !== S.content ||
matches[0].shortcut_name !== S.name)
if (matches[0].title !== S.title || matches[0].shortcut_name !== S.name ||
(matches[0].content !== S.content && !S.previousContents.includes(matches[0].content)))
return {ok:true,status:'custom',message:'Your edited Halo note was kept unchanged.'};
return {ok:true,status:'existing',message:'The Halo About and controls note is already present.'};
if (matches[0].content === S.content)
return {ok:true,status:'existing',message:'The Halo About and controls note is already present.'};
previous=matches[0];
}
if (doc.notes.length >= S.maxNotes) throw new Error('Steam notes exceed supported bounds');
if (!previous && doc.notes.length >= S.maxNotes) throw new Error('Steam notes exceed supported bounds');
const timestamp=Math.floor(Date.now()/1000);
const note={id:S.noteId,shortcut_name:S.name,ordinal:0,time_created:timestamp,
const note=previous ? {...previous,content:S.content,time_modified:timestamp} :
{id:S.noteId,shortcut_name:S.name,ordinal:0,time_created:timestamp,
time_modified:timestamp,title:S.title,content:S.content};
const updated={...doc,notes:[...doc.notes,note]};
const updated={...doc,notes:previous ? doc.notes.map(entry => entry === previous ? note : entry) :
[...doc.notes,note]};
const text=encoded(updated);
// Do not overwrite edits made while the installer was preparing the note.
const reread=await bounded(notes.GetNotes(filename,images));
@@ -164,7 +271,8 @@ NOTES = r"""(async () => {
if (encoded(readback) !== text)
throw new Error('The Halo note could not be verified after saving');
await identity();
return {ok:true,status:'added',message:'Added the Halo About and controls note to Steam Game Notes.'};
return previous ? {ok:true,status:'updated',message:'Updated the unchanged Halo note with online co-op instructions.'} :
{ok:true,status:'added',message:'Added the Halo About and controls note to Steam Game Notes.'};
} catch(error) {
return manual('Steam could not verify the Halo note. Your other notes were preserved; add the supplied text manually.');
}
@@ -182,13 +290,14 @@ def add_notes(home, game, appid, name=NAME):
return result
spec.update({"appid": appid, "noteId": NOTE_ID, "title": NOTE_TITLE,
"content": NOTE_CONTENT, "maxBytes": MAX_DOCUMENT_BYTES,
"maxNotes": MAX_NOTES})
"maxNotes": MAX_NOTES, "previousContents": list(PREVIOUS_NOTE_CONTENTS)})
with Client() as client:
value = client.evaluate(expression(NOTES, spec))
status = value.get("status")
if status in ("added", "existing", "custom") and value.get("ok") is True:
if status in ("added", "updated", "existing", "custom") and value.get("ok") is True:
result.update({"ok": True, "status": status,
"message": {"added": "Added the Halo About and controls note to Steam Game Notes.",
"updated": "Updated the unchanged Halo note with online co-op instructions.",
"existing": "The Halo About and controls note is already present.",
"custom": "Your edited Halo note was kept unchanged."}[status]})
elif status == "manual":
+1 -1
View File
@@ -26,7 +26,7 @@ ARTWORK = (("p", ".jpg", "halo-ce-cover.jpg", "84fee9349f8f00d8ade44c840330ac54e
("_logo", ".png", "halo-ce-logo.png", "7df3336e9988291552270764e80196b1892ab9d11b79d7d289e6243746d42dc8"))
ICON_NAME = "halo-ce-icon.png"
ICON_SHA256 = "184620356407e42a33877528c4f14b702df8b026415a99ed16976565fcfbce36"
ICON_SOURCE_COMMIT = "88142798513ebd99fc7c6224023e8b44c05d0106"
ICON_SOURCE_COMMIT = "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4"
ARTWORK_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".gif")
# Verified against the installed Steam client's library.js asset enum.
# Shortcut icons have their own path setter and are not sent through this API.
+1 -1
View File
@@ -9,7 +9,7 @@ import zipfile
ROOT = Path(__file__).resolve().parents[1]
DIST = ROOT / "dist"
VERSION = "1.3.5"
VERSION = "1.4.0"
EXE_NAME = f"Halo-Steam-Frame-Mod-Setup-{VERSION}.exe"
def allowed_source_files():
+404
View File
@@ -0,0 +1,404 @@
"""Offline checks of the pinned experimental engine and shipped local patch.
Reads a local vanilla/patched checkout. Applies the patch only to a temporary
projection and generates the real ARM64 Ninja graph without fetching tools,
compiling a game, using retail data, or connecting a headset.
"""
from __future__ import annotations
import argparse
import io
import json
from pathlib import Path, PurePosixPath
import re
import subprocess
import sys
import tarfile
import tempfile
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
SOURCE_COMMIT = "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4"
PATCH_FILES = frozenset((
"port/linux/src/vr.c", "port/linux/src/vr.h", "port/linux/src/vr_locomotion.h",
"port/linux/src/port_config.c", "source/game/player_control.c", "tools/test_vr_locomotion.py",
"port/linux/game/menu_functions.c", "port/linux/game/vr_render.c",
"port/linux/src/d3d8_gl.c", "port/linux/src/nv2a_psh.c", "port/linux/src/xgpu.h",
"source/rasterizer/xbox/rasterizer_xbox_lights.c",
"tools/test_online_coop_menu.py", "tools/test_vr_sun_glow.py",
"source/items/weapons.c", "source/items/weapons.h",
"source/game/aim_assist.c", "source/game/aim_assist.h",
"tools/test_vr_projectile_reticle.py",
"port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml", "tools/port_settings.py",
"source/units/units.c", "source/units/units.h",
))
class EngineCheckError(RuntimeError):
pass
def require(condition, message):
if not condition:
raise EngineCheckError(message)
def git(source, *arguments, check=True):
result = subprocess.run(["git", "-C", str(source), *arguments], capture_output=True, timeout=30)
if check:
require(result.returncode == 0, "A local Git source check failed; no engine files were changed.")
return result
def patch_files(text):
files = re.findall(r"^diff --git a/(\S+) b/(\S+)$", text, re.M)
require(bool(files), "The local engine patch has no ordinary file diffs.")
for before, after in files:
require(before == after and after in PATCH_FILES,
"The engine patch changes a path outside the reviewed integration files.")
return frozenset(after for _, after in files)
def c_function(text, name):
# Keep offsets while hiding comments and string contents from brace counting.
comments = re.compile(r"/\*.*?\*/|//[^\n]*", re.S)
clean = comments.sub(lambda match: " " * len(match.group()), text)
masked = re.sub(r'"(?:\\.|[^"\\])*"', lambda match: " " * len(match.group()), clean)
start = re.search(r"\b" + re.escape(name) + r"\s*\([^;{}]*\)\s*\{", masked)
require(start is not None, "Missing engine function: " + name)
depth = 1
for offset in range(start.end(), len(masked)):
depth += (masked[offset] == "{") - (masked[offset] == "}")
if not depth:
return clean[start.end():offset]
raise EngineCheckError("Unterminated engine function: " + name)
def check_texture_binding(text):
body = c_function(text, "bind_textures")
loops = list(re.finditer(r"for\s*\(stage\s*=\s*0;\s*stage\s*<\s*D3DTSS_MAXSTAGES;\s*stage\+\+\)", body))
bindings = list(re.finditer(r"\bstate_texture\s*\(", body))
require(len(loops) == 2 and len(bindings) == 1 and bindings[0].start() > loops[1].start(),
"Texture binding is not delayed until the second complete stage pass.")
resolve = body.find("xgpu_texture_get(")
require(loops[0].start() < resolve < loops[1].start(),
"Texture uploads must finish before the final stage-binding pass.")
require("state_texture(stage, gl_targets[stage], gl_textures[stage])" in body,
"Final texture bindings do not use the resolved per-stage arrays.")
def check_upstream(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
limits = read("port/linux/include/halo_port_limits.h")
require(re.search(r"^#define HALO_PORT_NETWORK_VERSION 17\s*$", limits, re.M),
"The candidate must use native peer protocol 17.")
check_texture_binding(read("port/linux/src/d3d8_gl.c"))
shader = read("port/linux/src/nv2a_vsh.c")
require("clip_position = oPos" in shader and "clip_captured = true" in shader
and "if (!(abs(position.w) > 0.0))" in shader
and "position = vec4(0.0, 0.0, 0.0, -1.0)" in shader,
"The pinned vertex clip-position safeguards are missing.")
distributed = read("port/linux/game/network_distributed.c")
coop = read("port/linux/game/network_coop.c")
for hook in ("network_coop_host_tick", "network_coop_client_tick", "network_coop_handle_presentation", "network_coop_handle_events"):
require(hook + "(" in distributed and hook in coop, "Missing integrated co-op hook: " + hook)
config = read("port/linux/src/port_config.c")
require('"network.coop_enemies_mode"' in config and '"network.coop_enemies"' in config
and '"network.brokers_file"' in config, "Co-op/broker configuration is missing.")
require((source / "port/assets/network/brokers.txt").is_file(), "The candidate broker list is missing.")
require("saved games of builds before it no longer load" in read("port/linux/include/halo_port_capacity.h"),
"The candidate saved-game layout change needs explicit compatibility handling.")
def check_controls(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
vr = read("port/linux/src/vr.c")
config = read("port/linux/src/port_config.c")
require(re.search(r'"vr\.movement",\s*_config_string,\s*"\\"head\\"",\s*"HALO_VR_MOVEMENT"', config),
"Head-directed walking is not the configurable default.")
movement = c_function(vr, "halo_vr_locomotion")
for guard in ("!vr.initialized", "!settings.head_movement", "!settings.controller_aim", "settings.gamepad_aim"):
require(guard in movement, "Missing unchanged-input mode guard: " + guard)
for guard in ("vr.views.focused", "vr.views.head.valid", "vr.origin_valid", "vr.aim_synced", "!vr.menus", "halo_vr_locomotion_orientation"):
require(guard in movement, "Missing valid-tracking locomotion guard: " + guard)
require("halo_vr_locomotion_project(" in movement, "The tested production movement math is not wired into VR input.")
player = read("source/game/player_control.c")
handling = c_function(player, "handle_one_player_input")
call = handling.find("halo_vr_locomotion(player->desired_angles.yaw")
require(call >= 0 and handling.find("player->desired_angles.yaw = vr_yaw") < call
< handling.find("player->throttle = input.throttle"),
"Movement must be transformed after aiming and before the player/action throttle is stored.")
guard = handling[max(0, call - 300):call]
for condition in ("local_player_index == 0", "unit->object.parent_object_index == NONE", "!director_inhibited_facing", "!cinematic_in_progress"):
require(condition in guard, "Missing local on-foot movement gate: " + condition)
gamepad = c_function(vr, "halo_vr_gamepad")
require("XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER" in gamepad
and "XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER" in gamepad,
"The agreed Xbox bumper layout was not preserved.")
require("halo_vr_tutorial_look(" in c_function(player, "player_control_action_test_note"),
"The physical-head tutorial hook was not ported to the new co-op action test.")
c_function(vr, "halo_vr_tutorial_look")
require((source / "port/linux/src/vr_locomotion.h").is_file(), "The production locomotion header is missing.")
def check_stereo_glow_and_online_menu(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
lights = read("source/rasterizer/xbox/rasterizer_xbox_lights.c")
draw = c_function(lights, "rasterizer_sun_glow_draw")
require("vr_render_sun_glow_begin(" in draw and "vr_render_sun_glow_end(" in draw,
"The per-eye sun-glow wrapper is not integrated into the actual renderer.")
require('config_real("vr.sun_glow_strength")' in draw
and 'rasterizer_sun_glow_draw_internal(flare, 1.0f)' in draw,
"The VR sun-strength option must preserve the original flat-screen composite.")
strength = c_function(lights, "rasterizer_vr_sun_glow_strength")
require("isfinite(configured)" in strength and "configured < 0.0" in strength
and "configured > 1.0" in strength and "return 0.5f" in strength,
"Sun-glow intensity must reject non-finite values and stay within 0–1.")
require('"vr.sun_glow_strength", _config_real, "0.5"' in read("port/linux/src/port_config.c"),
"The bounded half-strength sun-glow default is missing.")
renderer = read("port/linux/game/vr_render.c")
begin = c_function(renderer, "vr_render_sun_glow_begin")
for integration in ("vr_render.eye_cameras[eye]", "vr_render.eyes[eye]", "halo_vr_screen_space(1)", "halo_vr_draw_eye(eye)"):
require(integration in begin, "Missing per-eye glow state: " + integration)
require("halo_vr_screen_space(0)" in c_function(renderer, "vr_render_sun_glow_end"),
"Glow screen-space state is not restored.")
backend = c_function(read("port/linux/src/d3d8_gl.c"), "prepare_draw")
require(0 <= backend.find("key.sample_eye") < backend.find("fragment_shader_get(&key)"),
"Source-eye selection must participate in the actual fragment cache key.")
shader = read("port/linux/src/nv2a_psh.c")
require("key->multiview == XGPU_MULTIVIEW_NONE && key->sample_eye" in shader,
"A mono glow scratch target does not sample the selected stereo source eye.")
menu = read("port/linux/game/menu_functions.c")
route = c_function(menu, "cooperative_begin")
routes = {
"coop_host_lan": "cooperative_network_host(widget, event, controller, _multiplayer_mode_host_lan, widget_deleted)",
"coop_host_online": "cooperative_network_host(widget, event, controller, _multiplayer_mode_host_internet, widget_deleted)",
"coop_join_lan": "cooperative_network_join(widget, controller, _multiplayer_mode_lan, widget_deleted)",
"coop_join_online": "cooperative_network_join(widget, controller, _multiplayer_mode_server_browser, widget_deleted)",
"coop_join_invite": "cooperative_network_join(widget, controller, _multiplayer_mode_direct_link, widget_deleted)",
}
for name, call in routes.items():
require(re.search(r'if \(!strcmp\(widget->name, "' + name + r'"\)\)\s*return ' + re.escape(call) + r';', route),
"Missing LAN/Internet campaign dispatch: " + name)
require("cooperative_session_cleanup();" in route and '"network_screen"' in route,
"Leaving the campaign network chooser must clean up its pending session.")
single_player = c_function(menu, "cooperative_single_player")
for integration in ("player_spawn_count = 1", "player_ui_reset_single_player_local_player_controllers()",
"lobby_join_reset()", "player_ui_local_player_left_multiplayer_game(controller)"):
require(integration in single_player, "Missing one-player campaign reset: " + integration)
host = c_function(menu, "cooperative_network_host")
require('mode == _multiplayer_mode_host_internet && !config_boolean("network.online")' in host
and "multiplayer_host_mode(widget, event, controller, mode, TRUE, widget_deleted)" in host
and "cooperative_map_opening = TRUE" in host and "cooperative_map_opening = FALSE" in host,
"Campaign hosts must distinguish offline LAN from Internet and open campaign maps.")
join = c_function(menu, "cooperative_network_join")
require('mode != _multiplayer_mode_lan && !config_boolean("network.online")' in join
and "cooperative_network_game = TRUE" in join and "multiplayer.mode = mode" in join,
"Campaign joins must distinguish LAN, Internet browser and invite modes.")
handler = c_function(menu, "pc_menu_event_function_invoke")
require(re.search(r'else if \(!strcmp\(name, "port coop begin"\)\)\s*\{\s*return cooperative_begin\(', handler),
"Campaign dispatch must work in both flat and VR builds.")
require(re.search(r'else if \(!strcmp\(name, "port coop player 2 list initialize"\)[^{]+\{\s*return campaign_fail\(\);', handler)
and "player_spawn_count = 2;" not in menu and "vr_cooperative_begin" not in menu,
"Stale split-screen campaign routes must be rejected.")
for helper in ("lobby_add_player", "lobby_join_start", "lobby_player_choose", "preview_add"):
body = c_function(menu, helper)
require("lobby_campaign_single_player()" in body and "return campaign_local_player_block();" in body,
"Campaign must block a second local player: " + helper)
require("lobby_local_player_count() > 1" in c_function(menu, "map_list_choose")
and "cooperative_single_player()" in c_function(menu, "multiplayer_game_player"),
"Ordinary map/browser entry points must also use one local campaign player.")
require('!strcmp(name, "gamespy back handler") && cooperative_network_game &&' in handler
and "!global_network_game_server_get()" in handler,
"Browser cancellation must preserve a host when returning from Server Setup.")
asset = source / "port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml"
nodes = {node.attrib["name"].rsplit("/", 1)[-1]: node for node in ET.parse(asset).getroot()}
require("player_2_profile_screen" not in nodes and "player_2_profile_list" not in nodes,
"The campaign menu must not open a second-player profile screen.")
for event in ("a", "start"):
item = nodes["multiplayer_type_coop_item"].find('on[@event="' + event + '"]')
require(item is not None and item.get("run") == "port coop begin"
and item.get("open", "").endswith("coop/network_screen"),
"The campaign button must open the LAN/Internet chooser.")
captions = {"coop_host_lan": "HOST LOCAL (LAN)", "coop_join_lan": "JOIN LOCAL (LAN)",
"coop_host_online": "HOST ONLINE (INTERNET)", "coop_join_online": "JOIN ONLINE (INTERNET)",
"coop_join_invite": "JOIN INVITE LINK"}
for name, caption in captions.items():
require(name in nodes and nodes[name].get("text") == caption, "Missing campaign menu choice: " + caption)
for event in ("a", "start"):
item = nodes[name].find('on[@event="' + event + '"]')
require(item is not None and item.attrib == {"event": event, "run": "port coop begin"},
"Campaign choices must dispatch their named route without a static widget fallback.")
generator = read("tools/port_settings.py")
generated_coop = generator.split("def _coop() -> list:", 1)[1].split("def _item_options_extras", 1)[0]
require(all(name in generated_coop for name in captions) and "network_screen" in generated_coop
and "player_2_profile_screen" not in generated_coop,
"The menu generator must retain the network-only campaign choices.")
for script in ("tools/test_vr_sun_glow.py", "tools/test_online_coop_menu.py"):
require((source / script).is_file(), "Missing production C regression: " + script)
def check_nominal_projectile_reticle(source):
read = lambda path: (source / path).read_text(encoding="utf-8")
weapons = read("source/items/weapons.c")
firing = c_function(weapons, "trigger_create_projectiles")
require(re.search(r"trigger_adjust_projectile_ray\([^;]+&error,\s*FALSE,\s*NULL\)", firing),
"Real shots must retain the shared firing calculation with target recording.")
preview = c_function(weapons, "weapon_preview_projectile_ray")
require(re.search(r"trigger_adjust_projectile_ray\([^;]+&error,\s*TRUE,\s*pose\)", preview),
"The reticle must use the same pre-spread calculation in preview mode.")
require(not re.search(r"\b(?:random|projectile_new|weapon_trigger_fire)\w*\s*\(", preview),
"Reticle preview must not fire or consume shot randomness.")
assist = read("source/game/aim_assist.c")
require("player_aim_projectile_internal(player_index, position, direction, FALSE, pose)"
in c_function(assist, "player_preview_projectile_aim"),
"Reticle preview must suppress player targeting metadata writes.")
require("player_aim_projectile_internal(player_index, position, direction, TRUE, NULL)"
in c_function(assist, "player_aim_projectile"),
"Actual shots must preserve player targeting metadata writes.")
renderer = read("port/linux/game/vr_render.c")
crosshairs = c_function(renderer, "vr_render_crosshairs")
point = c_function(renderer, "vr_render_projectile_point")
require("vr_render_projectile_point(" in crosshairs and "vr_render_projectile_ray(" in point
and "_collision_test_for_projectiles_flags" in point,
"The rendered reticle must trace the game's nominal projectile ray.")
require("MAXIMUM_COLLISION_USER_STACK_DEPTH - 2" in point,
"The reticle must reserve capacity for UI, aim-assist and nested line-of-sight contexts.")
require("weapon_preview_projectile_ray(weapon_index, 0, player_index"
in c_function(renderer, "vr_render_projectile_ray"),
"The reticle must query the equipped primary weapon's actual firing ray.")
require("crosshair_inverse_distance" not in crosshairs and "CROSSHAIR_SMOOTHING" not in crosshairs,
"Reticle depth must not lag behind its current collision hit.")
require((source / "tools/test_vr_projectile_reticle.py").is_file(),
"The production projectile-reticle regression is missing.")
pose = c_function(renderer, "vr_render_projectile_pose")
for guard in ("local_player_get_player_index(0)", "_object_type_biped", "parent_object_index != NONE",
"gunner_object_index != NONE", "_director_perspective_first_person", "director_inhibited_facing(0)",
"aiming_velocity_maximum != 0.0f", "aiming_acceleration_maximum != 0.0f"):
require(guard in pose, "Missing guarded render-rate preview state: " + guard)
for integration in ("vr_render.view.hand_forward", "player_control_get_facing_direction(0",
"DEGREES_TO_RADIANS(85.0f)", "unit_clip_to_aiming_bounds(",
"render_interpolation_camera(0, observer_get_camera(0))"):
require(integration in pose, "Missing current-frame preview input: " + integration)
require("hand_position" not in pose and "camera_effects" not in pose,
"Preview origin must not add hand translation or apply camera effects again.")
ray = c_function(renderer, "vr_render_projectile_ray")
require("vr_render_projectile_pose(player_index, unit, &pose) ? &pose : NULL" in ray,
"The current-frame pose is not integrated into the primary reticle preview.")
units = read("source/units/units.c")
require("adjust_origin, use_aiming_vector, NULL, NULL" in c_function(units, "unit_adjust_projectile_ray"),
"Actual unit shots must not use presentation overrides.")
require("unit_adjust_projectile_ray_internal(" in c_function(units, "unit_preview_projectile_ray"),
"Unit preview must share the original camera/muzzle projection math.")
GRAPH_SMOKE = r'''
import io
from pathlib import Path
from types import SimpleNamespace
import sys
import re
sys.path.insert(0, str(Path.cwd()))
from tools import linux_arm64_build as build
from tools.ninja_syntax import Writer
headers = Path(".offline-check-headers")
(headers / "GLES3").mkdir(parents=True)
(headers / "GLES3/gl32.h").write_text("/* graph check only */")
build.GL_HEADERS = headers
build.is_linux_arm64 = lambda: True
build.fetch_third_party = lambda directory: None
sln = SimpleNamespace(port_release=True, port_vr=True, linux_arm64_cc="clang", port_pgo="off", port_lto="off")
output = io.StringIO()
build.generate_linux_arm64_build(Writer(output), sln)
graph = output.getvalue()
# pathlib follows the checker host even though this is a Linux build graph.
# Normalize only the inspected text; retain the actual generated graph below.
inspected_graph = re.sub(r"\$\r?\n[ \t]*", "", graph).replace("\\", "/")
for required in ("port/linux/game/network_coop.c", "port/linux/game/coop_scripts.c", "port/linux/game/coop_enemies.c", "port/linux/src/vr.c", "port/linux/arm64/host_vr.c", "-DHALO_VR=1", "-iquote port/linux/game", "build/linux_arm64/brokers.txt", "port/assets/network/brokers.txt"):
if required not in inspected_graph:
raise RuntimeError("The ARM64 graph omitted " + required)
if "-DHALO_GLES=1" in graph:
raise RuntimeError("The VR guest graph selected GLES instead of desktop OpenGL")
Path(".offline-check-build.ninja").write_text(graph, encoding="utf-8")
print("ARM64/OpenXR guest, host, co-op and broker graph generated without downloads or compilation.")
'''
def project_source(source, destination):
archive = git(source, "archive", "--format=tar", SOURCE_COMMIT).stdout
require(len(archive) <= 256 * 1024 * 1024, "The source archive exceeds the offline check limit.")
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as entries:
for entry in entries:
name = PurePosixPath(entry.name)
require(not name.is_absolute() and ".." not in name.parts and (entry.isdir() or entry.isfile()),
"The source projection contains an unexpected path or link.")
target = destination.joinpath(*name.parts)
if entry.isdir():
target.mkdir(parents=True, exist_ok=True)
else:
target.parent.mkdir(parents=True, exist_ok=True)
with entries.extractfile(entry) as incoming, target.open("wb") as outgoing:
outgoing.write(incoming.read())
def check_source(source: Path):
source = source.resolve()
require(git(source, "rev-parse", "HEAD").stdout.decode().strip() == SOURCE_COMMIT,
"Use the exact pinned experimental engine commit; a moving branch is not accepted.")
patch = ROOT / "resources/frame-controls.patch"
changed = patch_files(patch.read_text(encoding="utf-8"))
dirty = git(source, "diff", "--name-only", "HEAD").stdout.decode().splitlines()
untracked = git(source, "ls-files", "--others", "--exclude-standard").stdout.decode().splitlines()
require(set(dirty + untracked) <= changed, "The engine checkout has changes outside the reviewed local patch.")
vanilla = git(source, "apply", "--check", str(patch), check=False).returncode == 0
patched = git(source, "apply", "--reverse", "--check", str(patch), check=False).returncode == 0
require(vanilla or patched, "The local patch neither applies cleanly nor exactly reverses in this checkout.")
require(not vanilla or not dirty + untracked, "A vanilla source input must have no local engine changes.")
# Always test a clean canonical projection, so a patched input is not trusted
# merely because its HEAD still names the selected revision.
(ROOT / "build").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix="experimental-engine-check-", dir=ROOT / "build") as temporary:
projection = Path(temporary)
project_source(source, projection)
check_upstream(projection)
subprocess.run(["git", "init", "--quiet"], cwd=projection, check=True, capture_output=True, timeout=30)
for arguments in (("--check",), ()):
result = subprocess.run(["git", "apply", *arguments, str(patch)], cwd=projection,
capture_output=True, timeout=30)
require(result.returncode == 0, "The local patch does not apply to the canonical pinned engine source.")
if patched:
for path in changed:
require((source / path).read_bytes().replace(b"\r\n", b"\n")
== (projection / path).read_bytes().replace(b"\r\n", b"\n"),
"The patched input contains an extra change beyond the shipped local patch: " + path)
check_controls(projection)
check_stereo_glow_and_online_menu(projection)
check_nominal_projectile_reticle(projection)
result = subprocess.run([sys.executable, "-I", "-c", GRAPH_SMOKE], cwd=projection,
capture_output=True, text=True, timeout=30)
require(result.returncode == 0, "Offline ARM64 configuration generation failed: " + result.stderr[-2000:])
return {"ok": True, "sourceCommit": SOURCE_COMMIT, "workingTree": "vanilla" if vanilla else "patched",
"protocolVersion": 17, "rendererTwoPassBinding": True, "campaignCoopIntegration": True,
"headDirectedMovementIntegration": True, "xboxControlsPreserved": True,
"stereoSunGlowIntegration": True, "boundedSunGlowStrength": True, "vrInternetCampaignMenu": True,
"nominalProjectileReticleIntegration": True,
"renderRateProjectilePreviewIntegration": True,
"networkCampaignOnlyIntegration": True,
"offlineArm64NinjaGraph": True, "arm64LinkedBuild": False, "hardwareValidated": False}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path, help="Local exact-pinned vanilla or fully patched engine checkout")
arguments = parser.parse_args()
try:
result = check_source(arguments.source)
except (EngineCheckError, OSError, subprocess.SubprocessError) as error:
print(json.dumps({"ok": False, "error": str(error)}))
return 1
print(json.dumps(result, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -1,2 +1,2 @@
"""Guided installer for the native ARM64 Halo CE Steam Frame VR port."""
__version__ = "1.3.5"
__version__ = "1.4.0"
+11 -6
View File
@@ -25,7 +25,12 @@ from .ui import (BG, SIDEBAR, SURFACE, TEXT, MUTED, BORDER,
SETUP_URL = "https://partner.steamgames.com/doc/steamhardware/steamframe/setup"
PLATFORM_TOOLS_URL = "https://developer.android.com/tools/releases/platform-tools"
SOURCE_URL = "https://github.com/OpenCommunityEdition/OpenCE/pull/85"
APP_TITLE = "Halo • Community Steam Frame VR Mod Installer"
APP_TITLE = "Halo • Steam Frame VR Mod Installer"
EXPERIMENTAL_NOTICE = (
"LAN/online campaign, head-directed walking, and render-rate reticle.\n"
"Upgrades the native game; old saves are kept. Older checkpoints cannot be loaded.\n"
"Campaign uses one player per machine; co-op partners need matching builds."
)
CONTENT_DISCLOSURE = "No retail game executable, ISO, maps, product keys, or soundtrack is provided."
ARTWORK_CREDIT = "Halo artwork is included. Halo artwork and trademarks belong to Microsoft."
XBOX_REVISION_NOTE = "Original Xbox USA Rev 2 validated; other retail revisions are checked automatically."
@@ -39,7 +44,7 @@ STEAM_REMOVE_NOTE = ("Use Steam's Remove Non-Steam Game option for Halo: Combat
CONTROLS = (
"A Jump / accept B Melee / back X Reload / use Y Change weapon\n"
"RT Fire LT Grenade LB Change grenade RB Flashlight\n"
"Left stick Move / click to crouch Right stick Turn / click to zoom\n"
"Left stick Move by head direction / click to crouch Right stick Turn / click to zoom\n"
"Menu Pause View Scoreboard Both grips Recenter"
)
@@ -316,8 +321,8 @@ class App(tk.Tk):
choices.columnconfigure((0, 1, 2), weight=1, uniform="operations")
self.mode_choices = []
for index, title, subtitle, value in (
(0, "Install", "New native game", "install"),
(1, "Repair", "Keep saves", "repair"),
(0, "Install", "Install or upgrade", "install"),
(1, "Repair", "Refresh files", "repair"),
(2, "Uninstall", "Remove game", "uninstall")):
choice = Choice(choices, title, subtitle, self.mode, value, self._mode_changed)
choice.grid(row=0, column=index, sticky="ew",
@@ -609,7 +614,7 @@ class App(tk.Tk):
titles = ("UNINSTALL HALO VR" if uninstall else "GAME DATA", "CONNECT FRAME",
"UNINSTALL HALO VR" if uninstall else "INSTALL HALO VR")
subtitles = ("Choose your save preference before removing the native game." if uninstall else
"Install, repair, or uninstall the community VR mod.",
"Upgrades the native game; older checkpoints cannot load.\nCo-op uses one player per machine and matching builds.",
"Prepare SSH, then connect to your Steam Frame.",
"Review the native game and saves before removing them." if uninstall else
"Community VR mod. Xbox controls. Your Steam library.")
@@ -834,7 +839,7 @@ class App(tk.Tk):
"• Install SteamVR and allow at least 12 GB free on your Frame.\n"
"• Repair refreshes the program and controls while preserving saves.\n"
"• Install and repair automatically add Steam artwork and a game description in Steam Notes.\n"
"• Online play uses the native port's own multiplayer protocol.",
"• Online play uses the native port's own multiplayer protocol.\n\n" + EXPERIMENTAL_NOTICE,
10, background=SURFACE, wraplength=530).pack(anchor="w")
links = self._card(body, "Project references")
Button(links, "Steam Frame SSH guide", lambda:webbrowser.open(SETUP_URL),
+6 -1
View File
@@ -15,7 +15,7 @@ from typing import Callable
from .ssh import CancelledError, RemoteTimeoutError, Settings, SSHConnection, SSHError
SOURCE_COMMIT = "88142798513ebd99fc7c6224023e8b44c05d0106"
SOURCE_COMMIT = "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4"
EXPECTED_MAPS = frozenset(name + ".map" for name in (
"a10", "a30", "a50", "b30", "b40", "c10", "c20", "c40", "d20", "d40",
"beavercreek", "bloodgulch", "boardingaction", "carousel", "chillout", "damnation",
@@ -305,6 +305,11 @@ class Installer:
raise SSHError("The Frame reported an unexpected installation location.")
if registration_only and info.get("existing") is None:
raise SSHError("Install and verify native Halo VR before updating its Steam information.")
if (info.get("existing") or {}).get("needsUpgrade"):
if registration_only:
raise SSHError("Use Install to upgrade the existing native game to version 1.4 before updating its Steam information.")
repair = True
progress("upgrade", "Upgrading the existing native game to version 1.4. Previous checkpoints stay in place; new saves use save-v1.4...", None)
if info.get("pcVersionDetected"):
progress("detect", "Found an older Halo PC installation. The native VR build requires original Xbox maps.", None)
remote_resources = info["cachePath"] + "/resources"
+1 -1
View File
@@ -215,7 +215,7 @@ def owned_game(tmp_path):
program[18:20] = (183).to_bytes(2, "little")
(game / "halo").write_bytes(program)
(game / ".halo-frame-installer.json").write_text(json.dumps({
"owner": "halo-frame-installer", "sourceCommit": "88142798513ebd99fc7c6224023e8b44c05d0106",
"owner": "halo-frame-installer", "sourceCommit": "2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4",
"files": {"halo": hashlib.sha256(program).hexdigest()}}))
return game
+103
View File
@@ -0,0 +1,103 @@
"""Offline source-integration regressions; no game data or headset connection."""
import ast
import importlib.util
import os
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("experimental_engine_check", ROOT / "scripts/check-experimental-engine.py")
engine = importlib.util.module_from_spec(spec)
spec.loader.exec_module(engine)
TWO_PASS = """
static void bind_textures(struct nv2a_pixel_shader_key *key, float texture_scale[4][4])
{
GLenum gl_targets[D3DTSS_MAXSTAGES];
GLuint gl_textures[D3DTSS_MAXSTAGES];
int stage;
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
{
gl_textures[stage] = xgpu_texture_get(texture, palette, &gl_targets[stage], &description);
}
for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)
state_texture(stage, gl_targets[stage], gl_textures[stage]);
}
"""
def literal_assignment(path, name):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == name for target in node.targets):
return ast.literal_eval(node.value)
raise AssertionError("Missing " + name + " in " + str(path))
def test_installer_and_remote_metadata_share_the_exact_experimental_engine_pin():
for path, name in ((ROOT / "src/halo_frame_installer/install.py", "SOURCE_COMMIT"),
(ROOT / "resources/remote_install.py", "SOURCE_COMMIT"),
(ROOT / "resources/steam_shortcut.py", "ICON_SOURCE_COMMIT")):
assert literal_assignment(path, name) == engine.SOURCE_COMMIT
def test_two_pass_texture_binding_is_accepted():
engine.check_texture_binding(TWO_PASS)
@pytest.mark.parametrize("broken", [
TWO_PASS.replace("gl_textures[stage] = xgpu_texture_get", "state_texture(stage, gl_targets[stage], gl_textures[stage]); gl_textures[stage] = xgpu_texture_get"),
TWO_PASS.replace("for (stage = 0; stage < D3DTSS_MAXSTAGES; stage++)\n state_texture", "state_texture"),
TWO_PASS.replace("state_texture(stage, gl_targets[stage], gl_textures[stage])", "state_texture(stage, GL_TEXTURE_2D, 0)"),
])
def test_shader_upload_binding_regressions_are_rejected(broken):
with pytest.raises(engine.EngineCheckError, match="Texture binding|Final texture bindings"):
engine.check_texture_binding(broken)
@pytest.mark.parametrize("path", ["../outside.c", "/outside.c", "source/network/network_messages.c", "resources/remote_install.py"])
def test_engine_patch_cannot_hide_unreviewed_paths(path):
with pytest.raises(engine.EngineCheckError, match="outside the reviewed"):
engine.patch_files("diff --git a/" + path + " b/" + path + "\n")
def test_patch_path_allowlist_contains_both_new_header_and_existing_input_hooks():
text = (ROOT / "resources/frame-controls.patch").read_text(encoding="utf-8")
paths = engine.patch_files(text)
assert {"port/linux/src/vr_locomotion.h", "port/linux/src/vr.c", "port/linux/src/port_config.c", "source/game/player_control.c"} <= paths
def test_protocol_11_engine_cannot_be_mislabeled_as_matching_experimental_coop(tmp_path):
path = tmp_path / "port/linux/include/halo_port_limits.h"
path.parent.mkdir(parents=True)
path.write_text("#define HALO_PORT_NETWORK_VERSION 11\n")
with pytest.raises(engine.EngineCheckError, match="protocol 17"):
engine.check_upstream(tmp_path)
def test_public_release_documentation_names_the_single_installer_download():
for name in ("README.md", "docs/SETUP.md", "docs/RELEASE_NOTES.md"):
text = (ROOT / name).read_text(encoding="utf-8")
assert "1.4.0" in text and "Halo-Steam-Frame-Mod-Setup-1.4.0.exe" in text
assert "has not been published" not in text and "local experimental preview" not in text
readme = (ROOT / "README.md").read_text(encoding="utf-8")
assert "https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.0/Halo-Steam-Frame-Mod-Setup-1.4.0.exe" in readme
notes = (ROOT / "docs/RELEASE_NOTES.md").read_text(encoding="utf-8")
assert "cannot load old checkpoints" in notes and "save-v1.4" in notes
def test_actual_exact_pinned_engine_and_shipped_patch_when_checkout_is_supplied():
source = os.environ.get("HALO_EXPERIMENTAL_ENGINE_SOURCE")
if not source:
pytest.skip("Set HALO_EXPERIMENTAL_ENGINE_SOURCE for the local offline engine integration check")
result = engine.check_source(Path(source))
assert result["ok"] and result["protocolVersion"] == 17
assert result["offlineArm64NinjaGraph"] and result["headDirectedMovementIntegration"]
assert result["stereoSunGlowIntegration"] and result["vrInternetCampaignMenu"]
assert result["boundedSunGlowStrength"]
assert result["nominalProjectileReticleIntegration"]
assert result["renderRateProjectilePreviewIntegration"]
assert result["networkCampaignOnlyIntegration"]
assert not result["arm64LinkedBuild"] and not result["hardwareValidated"]
+2 -2
View File
@@ -126,7 +126,7 @@ def test_control_guide_matches_expected_layout():
def test_community_mod_title_and_content_disclosure_are_visible_and_accurate(app):
import tkinter as tk
from halo_frame_installer.gui import APP_TITLE, CONTENT_DISCLOSURE, ARTWORK_CREDIT
assert app.title() == APP_TITLE == 'Halo • Community Steam Frame VR Mod Installer'
assert app.title() == APP_TITLE == 'Halo • Steam Frame VR Mod Installer'
visible = '\n'.join(_display_text(app))
assert 'STEAM FRAME VR MOD' in visible and 'COMMUNITY MOD INSTALLER' in visible
assert CONTENT_DISCLOSURE in app.data_disclosure.cget('text')
@@ -883,7 +883,7 @@ def test_default_window_shows_all_operations_and_game_data_authorization(app):
assert top <= control.winfo_rooty()
assert control.winfo_rooty() + control.winfo_height() <= bottom
assert len({control.winfo_rooty() for control in app.mode_choices}) == 1
assert 'uninstall' in app.page_subtitle.cget('text').lower()
assert 'older checkpoints cannot load' in app.page_subtitle.cget('text').lower()
@WINDOWS_GUI
+354 -5
View File
@@ -452,6 +452,7 @@ def remote(tmp_path, monkeypatch):
module.HOME = home
module.CACHE = home / "cache"
module.GAME = home / "Games/HaloCENativeVR"
monkeypatch.setattr(module.os, "getuid", lambda: home.stat().st_uid, raising=False)
monkeypatch.setattr(module, "game_closed", lambda: None)
return module
@@ -549,10 +550,10 @@ def test_remote_detects_changed_original_xbox_header(remote, tmp_path):
remote.verify_maps(directory, manifest)
def test_config_repair_preserves_save_location_and_user_preferences(remote):
old = '[paths]\nsaves = "/another/save/location"\n\n[audio]\nvolume = 0.4\n\n[vr]\naim="head"\nturn="snap"\nrefresh_rate=90.0\n\n[[plugins]]\nname="custom"\n'
def test_config_repair_preserves_isolated_save_location_and_user_preferences(remote):
old = '[paths]\nsaves = ' + json.dumps(str(remote.save_root())) + '\n\n[audio]\nvolume = 0.4\n\n[vr]\naim="head"\nturn="snap"\nrefresh_rate=90.0\n\n[[plugins]]\nname="custom"\n'
result = remote.tomllib.loads(remote.merge_config(old))
assert result["paths"]["saves"] == "/another/save/location"
assert result["paths"]["saves"] == str(remote.save_root())
assert result["audio"]["volume"] == 0.4
assert result["vr"]["aim"] == "controller"
assert result["vr"]["turn"] == "snap" and result["vr"]["refresh_rate"] == 90.0
@@ -565,6 +566,71 @@ def test_config_repair_refuses_semantic_changes_inside_multiline_string(remote):
remote.merge_config(old)
@pytest.mark.parametrize('value', ['/another/save/location', '../HaloCENativeVR/save', 'save', '', False])
def test_new_config_refuses_legacy_external_or_ambiguous_save_paths(remote, value):
original = '[paths]\nsaves = ' + json.dumps(value) + '\n'
with pytest.raises(ValueError, match='owned|own game folder|version 1.4'):
remote.merge_config(original)
def test_new_config_missing_paths_gets_versioned_defaults(remote):
updated = remote.tomllib.loads(remote.merge_config('[audio]\nvolume = 0.4\n'))
assert updated['paths'] == {'data': str(remote.GAME), 'saves': str(remote.save_root())}
assert updated['audio']['volume'] == 0.4
assert updated['vr']['aim'] == 'controller' and updated['vr']['movement'] == 'head'
assert updated['vr']['sun_glow_strength'] == 0.5
assert updated['network']['coop_enemies_mode'] == 'none'
def test_repair_preserves_custom_sun_glow_strength(remote):
updated = remote.tomllib.loads(remote.merge_config('[vr]\nsun_glow_strength=0.25\n'))
assert updated['vr']['sun_glow_strength'] == 0.25
@pytest.mark.parametrize('repair', [False, True])
@pytest.mark.parametrize('unsafe', ['legacy-save-config', 'copied-experimental-config', 'symlinked-save-directory', 'missing-paths'])
def test_new_reuse_and_repair_guard_save_format_and_keep_experimental_instance(remote, repair, unsafe):
experimental = remote.GAME.parent / 'HaloCENativeVRExperimental'
(experimental / 'save').mkdir(parents=True)
(experimental / 'halo').write_bytes(b'experimental executable')
(experimental / 'save/checkpoint').write_bytes(b'experimental checkpoint')
game = remote.GAME
game.mkdir()
header = bytearray(64)
header[:6] = b'\x7fELF\x02\x01'
struct.pack_into('<H', header, 18, 183)
for name in ('halo', 'libSDL3.so.0'):
(game / name).write_bytes(header)
maps = xbox_maps(game, remote)
(game / 'xbox-data-manifest.json').write_text(json.dumps(maps))
(game / remote.MARKER).write_text(json.dumps({'owner': remote.OWNER,
'sourceCommit': remote.SOURCE_COMMIT,
'files': {name: remote.digest(game / name) for name in ('halo', 'libSDL3.so.0')}}))
if unsafe == 'legacy-save-config':
config = {'paths': {'data': str(game), 'saves': str(game / 'save')}}
elif unsafe == 'copied-experimental-config':
config = {'paths': {'data': str(experimental), 'saves': str(experimental / 'save')}}
elif unsafe == 'missing-paths':
config = {'audio': {'volume': 0.4}}
else:
try:
remote.save_root().symlink_to(experimental / 'save', target_is_directory=True)
except (OSError, NotImplementedError):
pytest.skip('This host cannot create directory symlinks')
config = {'paths': {'data': str(game), 'saves': str(remote.save_root())}}
original = '\n'.join('[' + section + ']\n' + '\n'.join(key + ' = ' + json.dumps(value)
for key, value in entries.items()) for section, entries in config.items()) + '\n'
(game / 'config.toml').write_text(original)
if unsafe == 'missing-paths' and repair:
assert remote.existing_install(repair=True)['mapsVerified']
else:
with pytest.raises(ValueError, match='version 1.4|owned|own game folder|Symbolic links'):
remote.existing_install(repair=repair)
assert (game / 'config.toml').read_text() == original
assert (experimental / 'halo').read_bytes() == b'experimental executable'
assert (experimental / 'save/checkpoint').read_bytes() == b'experimental checkpoint'
def test_repair_backup_and_replacements_preserve_saves_and_unrelated_files(remote):
game = remote.GAME
game.mkdir(parents=True)
@@ -641,7 +707,7 @@ def test_full_repair_rebuilds_damaged_native_files_preserves_maps_saves_and_conf
(game / "xbox-data-manifest.json").write_text(json.dumps(maps))
(game / "halo").write_bytes(b"damaged executable")
(game / "libSDL3.so.0").write_bytes(b"old library")
(game / "config.toml").write_text('[paths]\nsaves="/my/saves"\n[audio]\nvolume=0.3\n[vr]\naim="head"\nturn="snap"\n')
(game / "config.toml").write_text('[paths]\nsaves=' + json.dumps(str(game / "save")) + '\n[audio]\nvolume=0.3\n[vr]\naim="head"\nturn="snap"\n')
(game / "save").mkdir()
(game / "save/checkpoint").write_bytes(b"campaign progress")
(game / "maps/custom.map").write_bytes(b"user mod")
@@ -668,15 +734,298 @@ def test_full_repair_rebuilds_damaged_native_files_preserves_maps_saves_and_conf
struct.pack_into("<H", header, 18, 183)
(build / "halo").write_bytes(header + b"new native game")
(build / "libSDL3.so.0").write_bytes(header + b"new native library")
(build / "brokers.txt").write_text("broker.example.org:1883\n")
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: "native libraries resolved")
response = remote.finalize(identifier, repair=True)
assert response["repaired"] is True and response["mapsReinstalled"] is False
assert (game / "halo").read_bytes() == header + b"new native game"
assert (game / "save/checkpoint").read_bytes() == b"campaign progress"
assert (game / "brokers.txt").read_text() == "broker.example.org:1883\n"
assert remote.read_marker(game / remote.MARKER)["files"]["brokers.txt"] == remote.digest(game / "brokers.txt")
assert (game / "maps/custom.map").read_bytes() == b"user mod"
merged = remote.tomllib.loads((game / "config.toml").read_text())
assert merged["paths"]["saves"] == "/my/saves"
assert merged["paths"]["saves"] == str(remote.save_root())
assert remote.save_root().is_dir() and not any(remote.save_root().iterdir())
assert merged["audio"]["volume"] == 0.3
assert merged["vr"]["aim"] == "controller" and merged["vr"]["turn"] == "snap"
assert remote.read_marker(game / remote.MARKER)["sourceCommit"] == remote.SOURCE_COMMIT
assert remote.existing_install()["mapsVerified"] is True
def native_install(remote, *, source=None, save_path=None):
game = remote.GAME
game.mkdir(parents=True)
header = bytearray(64)
header[:6] = b"\x7fELF\x02\x01"
struct.pack_into("<H", header, 18, 183)
for name in ("halo", "libSDL3.so.0"):
(game / name).write_bytes(header + b"previous native program")
manifest = xbox_maps(game, remote)
(game / "xbox-data-manifest.json").write_text(json.dumps(manifest))
save_path = save_path or game / "save"
save_path.mkdir(parents=True)
(save_path / "checkpoint").write_bytes(b"incompatible previous checkpoint")
(game / "config.toml").write_text('[paths]\ndata = ' + json.dumps(str(game))
+ '\nsaves = ' + json.dumps(str(save_path)) + '\n[audio]\nvolume = 0.3\n[vr]\nturn = "snap"\n')
marker = {"owner": remote.OWNER, "sourceCommit": source or remote.LEGACY_SOURCE_COMMIT,
"files": {name: remote.digest(game / name) for name in ("halo", "libSDL3.so.0")}}
(game / remote.MARKER).write_text(json.dumps(marker))
return manifest, save_path
def built_run(remote, manifest, identifier, *, existing=True):
remote.owned(remote.CACHE)
resources = remote.CACHE / "resources"
resources.mkdir(exist_ok=True)
(resources / "frame-controls.patch").write_bytes(b"tested gameplay patch")
directory = remote.CACHE / "runs" / identifier
remote.owned(directory)
metadata = remote.read_marker(directory / remote.MARKER)
metadata["mapsOrigin"] = "existing" if existing else "upload"
(directory / remote.MARKER).write_text(json.dumps(metadata))
(directory / "stage").mkdir()
upload = directory / "upload"
upload.mkdir()
if not existing:
manifest = xbox_maps(upload, remote)
(upload / "xbox-data-manifest.json").write_text(json.dumps(manifest))
build = directory / "src/build/linux_arm64"
build.mkdir(parents=True)
header = bytearray(64)
header[:6] = b"\x7fELF\x02\x01"
struct.pack_into("<H", header, 18, 183)
for name in ("halo", "libSDL3.so.0"):
(build / name).write_bytes(header + b"new native program")
(build / "brokers.txt").write_text("broker.example.org:1883\n")
return directory
@pytest.mark.parametrize("kind", ["default", "custom-internal", "external"])
def test_verified_stable_upgrade_keeps_old_checkpoints_and_backs_up_config(remote, monkeypatch, kind):
path = {"default": remote.GAME / "save", "custom-internal": remote.GAME / "profiles/custom",
"external": remote.HOME / "external-checkpoints"}[kind]
manifest, previous = native_install(remote, save_path=path)
checkpoint = previous / "checkpoint"
before = (checkpoint.read_bytes(), checkpoint.stat().st_mtime_ns)
old_config = (remote.GAME / "config.toml").read_bytes()
old_program = (remote.GAME / "halo").read_bytes()
closed = Mock()
monkeypatch.setattr(remote, "game_closed", closed)
verified = remote.existing_install()
assert verified["needsUpgrade"] and verified["mapsVerified"]
assert verified["previousSavePaths"] == [str(previous)]
closed.assert_called_once()
identifier = "e" * 32
built_run(remote, manifest, identifier)
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: "native libraries resolved")
result = remote.finalize(identifier, repair=True)
assert result["repaired"] and not result["mapsReinstalled"]
assert result["previousSavePaths"] == [str(previous)]
assert (checkpoint.read_bytes(), checkpoint.stat().st_mtime_ns) == before
assert (Path(result["backupPath"]) / "config.toml").read_bytes() == old_config
assert (Path(result["backupPath"]) / "halo").read_bytes() == old_program
settings = remote.tomllib.loads((remote.GAME / "config.toml").read_text())
assert settings["paths"] == {"data": str(remote.GAME), "saves": str(remote.save_root())}
assert settings["audio"]["volume"] == 0.3 and settings["vr"]["turn"] == "snap"
assert remote.save_root().is_dir() and not any(remote.save_root().iterdir())
marker = remote.read_marker(remote.GAME / remote.MARKER)
assert marker["installerVersion"] == "1.4.0" and marker["networkProtocol"] == 17
assert marker["saveRoot"] == str(remote.save_root()) and "experimental" not in marker
assert not remote.existing_install()["needsUpgrade"]
# A later Repair preserves both generations and the former custom location.
(remote.save_root() / "checkpoint").write_bytes(b"new compatible checkpoint")
built_run(remote, manifest, "f" * 32)
remote.finalize("f" * 32, repair=True)
assert remote.read_marker(remote.GAME / remote.MARKER)["previousSavePaths"] == [str(previous)]
assert checkpoint.read_bytes() == before[0]
assert (remote.save_root() / "checkpoint").read_bytes() == b"new compatible checkpoint"
inventory = remote.uninstall_inventory(remote.GAME)
selected, external = remote.uninstall_save_files(inventory)
assert str(Path("save-v1.4/checkpoint")) in selected
if kind == "external":
assert external == [str(previous)] and not any("external-checkpoints" in item for item in selected)
else:
assert str(checkpoint.relative_to(remote.GAME)) in selected
@pytest.mark.parametrize("trigger", ["marker-failure", "cancel-after-marker"])
def test_failed_upgrade_restores_original_program_config_marker_and_all_checkpoints(remote, monkeypatch, trigger):
manifest, previous = native_install(remote)
old = {name: (remote.GAME / name).read_bytes() for name in ("halo", "libSDL3.so.0", "config.toml", remote.MARKER)}
checkpoint = previous / "checkpoint"
checkpoint_signature = (checkpoint.read_bytes(), checkpoint.stat().st_mtime_ns)
identifier = "a" * 32
directory = built_run(remote, manifest, identifier)
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: "native libraries resolved")
replace = remote.os.replace
def fail_marker(source, target):
if Path(source) == directory / "stage" / remote.MARKER:
if trigger == "marker-failure":
raise OSError("injected final marker failure")
replace(source, target)
(directory / "cancelled").write_text("cancel immediately after marker publication")
return
return replace(source, target)
monkeypatch.setattr(remote.os, "replace", fail_marker)
with pytest.raises((OSError, ValueError), match="marker failure|cancelled"):
remote.finalize(identifier, repair=True)
assert {name: (remote.GAME / name).read_bytes() for name in old} == old
assert (checkpoint.read_bytes(), checkpoint.stat().st_mtime_ns) == checkpoint_signature
assert remote.save_root().is_dir() and not any(remote.save_root().iterdir())
assert remote.existing_install()["needsUpgrade"] # Empty failed-run directory permits retry.
@pytest.mark.parametrize("damage", ["program-hash", "map", "unknown-revision", "occupied-new-save"])
def test_automatic_upgrade_refuses_unverified_or_unknown_state(remote, damage):
_, previous = native_install(remote)
config = (remote.GAME / "config.toml").read_bytes()
if damage == "program-hash":
with (remote.GAME / "halo").open("ab") as stream:
stream.write(b"modified")
elif damage == "map":
(remote.GAME / "maps/a10.map").write_bytes(b"invalid map")
elif damage == "unknown-revision":
marker = remote.read_marker(remote.GAME / remote.MARKER)
marker["sourceCommit"] = "unknown source"
(remote.GAME / remote.MARKER).write_text(json.dumps(marker))
else:
remote.save_root().mkdir()
(remote.save_root() / "checkpoint").write_bytes(b"unknown save format")
with pytest.raises(ValueError):
remote.existing_install()
assert (remote.GAME / "config.toml").read_bytes() == config
assert (previous / "checkpoint").read_bytes() == b"incompatible previous checkpoint"
def test_legacy_library_and_finalize_cannot_shortcut_the_upgrade(remote, monkeypatch):
manifest, _ = native_install(remote)
monkeypatch.setattr(remote, "preflight_uninstall", lambda: {"home": str(remote.HOME)})
with pytest.raises(ValueError, match="Use Install"):
remote.preflight_library()
with pytest.raises(ValueError, match="Use Install"):
remote.shortcut()
built_run(remote, manifest, "b" * 32)
with pytest.raises(ValueError, match="needs a version 1.4 upgrade"):
remote.build("b" * 32)
with pytest.raises(ValueError, match="needs a version 1.4 upgrade"):
remote.finalize("b" * 32)
def test_install_automatically_promotes_verified_legacy_source_to_repair_without_map_upload():
class UpgradeConnection(FakeConnection):
def run(self, argv, **kwargs):
if argv[1] == "-c":
reply = json.loads(super().run(argv, **kwargs).removeprefix("HFI_RESULT "))
reply["existing"]["needsUpgrade"] = True
return "HFI_RESULT " + json.dumps(reply)
if argv[2] in ("build", "finalize"):
self.commands.append(argv)
return "HFI_RESULT " + json.dumps({"gamePath": "/home/steamos/Games/HaloCENativeVR", "repaired": True})
return super().run(argv, **kwargs)
settings = Settings("frame", "private", reinstall_existing=False)
fake = UpgradeConnection(settings)
progress = []
result = Installer(lambda _: fake, RESOURCES).run(settings, None, lambda *args: progress.append(args))
assert result.repaired and not result.reused and fake.closed
steps = [argv for argv in fake.commands if argv[1] != "-c"]
assert [argv[2] for argv in steps] == ["prepare", "build", "finalize", "shortcut"]
assert all("--repair" in argv for argv in steps)
assert "--use-existing-maps" in steps[0]
assert all(not remote.endswith(".map") for _, remote in fake.uploads)
assert any(args[0] == "upgrade" and "save-v1.4" in args[1] for args in progress)
def test_library_registration_rejects_legacy_preflight_response_without_upload_or_build():
class LegacyConnection(FakeConnection):
def run(self, argv, **kwargs):
reply = json.loads(super().run(argv, **kwargs).removeprefix("HFI_RESULT "))
reply["existing"]["needsUpgrade"] = True
return "HFI_RESULT " + json.dumps(reply)
fake = LegacyConnection(Settings("frame", "private"))
with pytest.raises(SSHError, match="Use Install"):
Installer(lambda _: fake, RESOURCES).add_to_steam(fake.settings)
assert fake.closed and not fake.uploads and len(fake.commands) == 1
def test_keep_saves_retains_both_known_roots_even_without_config(remote):
native_install(remote)
(remote.GAME / "config.toml").unlink()
remote.save_root().mkdir()
(remote.save_root() / "checkpoint").write_bytes(b"new checkpoint")
selected, external = remote.uninstall_save_files(remote.uninstall_inventory(remote.GAME))
assert selected == [str(Path("save-v1.4/checkpoint")), str(Path("save/checkpoint"))] and not external
def frame_preflight(remote, monkeypatch, free):
monkeypatch.setattr(remote.pwd, "getpwuid", lambda uid: types.SimpleNamespace(pw_name="steamos"))
monkeypatch.setattr(remote.pathlib.Path, "home", lambda: remote.HOME)
monkeypatch.setattr(remote.platform, "machine", lambda: "aarch64")
monkeypatch.setattr(remote, "os_release", lambda: {"ID": "steamos"})
monkeypatch.setattr(remote.shutil, "which", lambda name: name)
monkeypatch.setattr(remote.shutil, "disk_usage", lambda path: types.SimpleNamespace(free=free))
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: '{"host":{"security":{"rootless":true}}}')
library = remote.HOME / "steamvr/linuxarm64/runtime.so"
library.parent.mkdir(parents=True)
library.write_bytes(b"test runtime")
runtime = remote.HOME / ".config/openxr/1/active_runtime.json"
runtime.parent.mkdir(parents=True)
runtime.write_text(json.dumps({"runtime": {"library_path": str(library)}}))
def test_automatic_upgrade_requires_build_space_and_closed_game(remote, monkeypatch):
native_install(remote)
frame_preflight(remote, monkeypatch, free=remote.MIN_FREE_BYTES - 1)
with pytest.raises(ValueError, match="12 GiB"):
remote.preflight()
monkeypatch.setattr(remote.shutil, "disk_usage", lambda path: types.SimpleNamespace(free=remote.MIN_FREE_BYTES))
closed = Mock(side_effect=ValueError("game is running"))
monkeypatch.setattr(remote, "game_closed", closed)
with pytest.raises(ValueError, match="game is running"):
remote.preflight()
closed.assert_called_once()
monkeypatch.setattr(remote, "game_closed", lambda: None)
assert remote.preflight()["existing"]["needsUpgrade"]
def test_fresh_install_publishes_only_new_save_format(remote, monkeypatch):
identifier = "c" * 32
built_run(remote, {}, identifier, existing=False)
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: "native libraries resolved")
monkeypatch.setattr(remote, "rename_noreplace", lambda source, target: source.rename(target))
result = remote.finalize(identifier)
assert not result["reused"] and remote.save_root().is_dir()
assert not (remote.GAME / "save").exists()
settings = remote.tomllib.loads((remote.GAME / "config.toml").read_text())
assert settings["paths"]["saves"] == str(remote.save_root())
assert remote.read_marker(remote.GAME / remote.MARKER)["previousSavePaths"] == []
assert not remote.existing_install()["needsUpgrade"]
def test_legacy_upgrade_refuses_previous_save_metadata_overflow_before_any_replacement(remote):
native_install(remote)
marker = remote.read_marker(remote.GAME / remote.MARKER)
marker["previousSavePaths"] = [str(remote.GAME / ("previous-" + str(index))) for index in range(32)]
(remote.GAME / remote.MARKER).write_text(json.dumps(marker))
config = (remote.GAME / "config.toml").read_bytes()
with pytest.raises(ValueError, match="save-location count"):
remote.existing_install()
assert (remote.GAME / "config.toml").read_bytes() == config and not remote.save_root().exists()
def test_recognized_codex_legacy_marker_upgrades_without_changing_old_checkpoint(remote, monkeypatch):
manifest, previous = native_install(remote)
marker = remote.read_marker(remote.GAME / remote.MARKER)
marker["owner"] = "codex-halo-native-frame-20261005"
marker["build"] = {"sourceCommit": marker.pop("sourceCommit")}
(remote.GAME / remote.MARKER).unlink()
codex = remote.GAME / ".codex-halo-native-install.json"
codex.write_text(json.dumps(marker))
before = codex.read_bytes()
assert remote.existing_install()["needsUpgrade"]
built_run(remote, manifest, "d" * 32)
monkeypatch.setattr(remote, "command", lambda argv, **kwargs: "native libraries resolved")
remote.finalize("d" * 32, repair=True)
assert codex.read_bytes() == before
assert (previous / "checkpoint").read_bytes() == b"incompatible previous checkpoint"
assert remote.read_marker(remote.GAME / remote.MARKER)["sourceCommit"] == remote.SOURCE_COMMIT
assert not remote.existing_install()["needsUpgrade"]
+98 -2
View File
@@ -1,5 +1,6 @@
"""Exercise the actual Notes JavaScript against a bounded native API contract."""
import importlib.util
import hashlib
import json
from pathlib import Path
import shutil
@@ -71,7 +72,9 @@ def run_notes(notes, **options):
spec = notes.native_spec(notes.HOME, notes.GAME, notes.NAME)
spec.update({"appid": 3000000001, "noteId": notes.NOTE_ID,
"title": notes.NOTE_TITLE, "content": notes.NOTE_CONTENT,
"maxBytes": notes.MAX_DOCUMENT_BYTES, "maxNotes": notes.MAX_NOTES})
"maxBytes": notes.MAX_DOCUMENT_BYTES, "maxNotes": notes.MAX_NOTES,
"previousContents": list(notes.PREVIOUS_NOTE_CONTENTS)})
spec.update(options.pop("spec_overrides", {}))
body = {"spec": spec, "expression": notes.expression(notes.NOTES, spec), **options}
output = subprocess.run([node, "-e", NODE_CLIENT], input=json.dumps(body),
capture_output=True, text=True, encoding="utf-8", check=True, timeout=12)
@@ -129,6 +132,80 @@ def test_matching_managed_note_is_idempotent(notes):
assert "SaveNotes" not in methods(result)
def test_only_exact_shipped_notes_are_allowed_previous_installer_content(notes):
assert notes.PREVIOUS_NOTE_CONTENTS == (notes.A2_NOTE_CONTENT, notes.A4_NOTE_CONTENT, notes.STABLE_NOTE_CONTENT)
assert hashlib.sha256(notes.A2_NOTE_CONTENT.encode("utf-8")).hexdigest() == \
"a4e1776caa60548620e0510acbe194aa414645be8d042771d9cbe1ec49edc05e"
assert hashlib.sha256(notes.A4_NOTE_CONTENT.encode("utf-8")).hexdigest() == \
"55785ec794857e844722ba758a4ce47fdb607a6bba2c30f23a437dd8f320ae30"
assert hashlib.sha256(notes.STABLE_NOTE_CONTENT.encode("utf-8")).hexdigest() == "ceed0d2d403fa18904c5d529ae409c8bfedad733fa1f1a7d93a6e2c2fdb8d32a"
assert "save-v1.4" in notes.NOTE_CONTENT and "old save folder is retained" in notes.NOTE_CONTENT
assert notes.NOTE_CONTENT != notes.A2_NOTE_CONTENT
assert "LISTING to PUBLIC" in notes.NOTE_CONTENT and "PUBLIC lets anyone" in notes.NOTE_CONTENT
assert "HOST LOCAL (LAN)" in notes.NOTE_CONTENT and "JOIN ONLINE (INTERNET)" in notes.NOTE_CONTENT
assert "Flat-screen CO-OP CAMPAIGN retains local split-screen" not in notes.NOTE_CONTENT
@pytest.mark.parametrize("previous", ["A2_NOTE_CONTENT", "A4_NOTE_CONTENT", "STABLE_NOTE_CONTENT"])
def test_exact_unchanged_previous_note_is_upgraded_with_every_other_field_preserved(notes, previous):
custom = {"id": "my-note", "title": "My campaign", "content": "Leave untouched", "extra": [1, 2]}
old = managed(notes, content=getattr(notes, previous), ordinal=8, time_created=123, unknown={"keep": True})
original = document(notes, [custom, old], unknown_document={"keep": [3]})
result = run_notes(notes, document=original)
assert result["result"]["status"] == "updated"
assert methods(result).count("SaveNotes") == 1
updated = result["document"]
assert updated["unknown_document"] == original["unknown_document"]
assert updated["notes"][0] == custom
assert {key: value for key, value in updated["notes"][1].items() if key not in ("content", "time_modified")} == \
{key: value for key, value in old.items() if key not in ("content", "time_modified")}
assert updated["notes"][1]["content"] == notes.NOTE_CONTENT
assert updated["notes"][1]["time_modified"] > old["time_modified"]
assert run_notes(notes, document=updated)["result"]["status"] == "existing"
@pytest.mark.parametrize("field", ["content", "title", "shortcut_name"])
@pytest.mark.parametrize("previous", ["A2_NOTE_CONTENT", "A4_NOTE_CONTENT", "STABLE_NOTE_CONTENT"])
def test_previous_note_with_any_user_edit_or_identity_mismatch_is_preserved(notes, field, previous):
old = getattr(notes, previous)
changes = {"content": old}
changes[field] = {"content": old + " ", "title": "My title", "shortcut_name": "Other game"}[field]
original = document(notes, [managed(notes, **changes)])
result = run_notes(notes, document=original)
assert result["result"]["status"] == "custom"
assert result["document"] == original
assert "SaveNotes" not in methods(result)
@pytest.mark.parametrize("previous", ["A2_NOTE_CONTENT", "A4_NOTE_CONTENT", "STABLE_NOTE_CONTENT"])
def test_concurrent_edit_during_previous_note_upgrade_is_kept_without_write(notes, previous):
old = getattr(notes, previous)
original = document(notes, [managed(notes, content=old)])
changed = document(notes, [managed(notes, content=old + " edited")])
result = run_notes(notes, document=original, changeDocumentAtRead=2, changedDocument=changed)
assert result["result"]["status"] == "manual"
assert result["document"] == changed
assert "SaveNotes" not in methods(result)
@pytest.mark.parametrize("previous", [None, [None], ["old"] * 5, ["é" * 8193]])
def test_malformed_or_excessive_previous_content_list_never_writes(notes, previous):
original = document(notes, [managed(notes, content=notes.A2_NOTE_CONTENT)])
result = run_notes(notes, document=original, spec_overrides={"previousContents": previous})
assert result["result"]["status"] == "manual"
assert result["document"] == original
assert "SaveNotes" not in methods(result)
@pytest.mark.parametrize("previous", ["A2_NOTE_CONTENT", "A4_NOTE_CONTENT", "STABLE_NOTE_CONTENT"])
def test_exact_previous_note_upgrade_does_not_need_an_extra_note_slot(notes, previous):
others = [{"id": str(index), "content": "Keep"} for index in range(notes.MAX_NOTES - 1)]
result = run_notes(notes, document=document(notes, [*others, managed(notes, content=getattr(notes, previous))]))
assert result["result"]["status"] == "updated"
assert len(result["document"]["notes"]) == notes.MAX_NOTES
assert result["document"]["notes"][:-1] == others
@pytest.mark.parametrize("changes", [{"content": "User edited"}, {"title": "User title"},
{"shortcut_name": "Other game"}])
def test_edited_or_colliding_note_is_never_overwritten(notes, changes):
@@ -217,6 +294,25 @@ def test_wrapper_client_failure_keeps_optional_manual_instructions(notes, monkey
assert "private endpoint" not in result["message"]
def test_wrapper_reports_verified_a2_upgrade_as_updated(notes, monkeypatch):
class UpdatedClient:
def __enter__(self):
return self
def __exit__(self, *arguments):
pass
def evaluate(self, expression):
assert "previousContents" in expression
return {"ok": True, "status": "updated"}
monkeypatch.setattr(notes, "Client", UpdatedClient)
result = notes.add_notes(notes.HOME, notes.GAME, 3000000001)
assert result["ok"] is True and result["status"] == "updated"
assert "online co-op instructions" in result["message"]
assert result["content"] == notes.NOTE_CONTENT
def test_module_has_no_file_or_session_mutation_routes(notes):
source = (RESOURCES / "steam_notes.py").read_text(encoding="utf-8")
assert "SetAppDescription" not in source and "steam_appid.txt" not in source
@@ -231,4 +327,4 @@ def test_authored_note_matches_current_controls_and_protocol_scope(notes):
assert "Left bumper: change grenade type" in notes.NOTE_CONTENT
assert "Right bumper: toggle flashlight" in notes.NOTE_CONTENT
assert "compatible native/OpenCE protocol builds" in notes.NOTE_CONTENT
assert "experimental native ARM64 Linux/OpenXR" in notes.NOTE_CONTENT
assert "native ARM64 Linux/OpenXR" in notes.NOTE_CONTENT
+5 -1
View File
@@ -180,6 +180,7 @@ def test_finalize_rechecks_retained_hashes_before_publishing(remote, monkeypatch
struct.pack_into("<H", header, 18, 183)
(build / "halo").write_bytes(header)
(build / "libSDL3.so.0").write_bytes(header)
(build / "brokers.txt").write_text("broker.example.org:1883\n")
changed = bytearray((older / "upload/maps/a10.map").read_bytes())
changed[100] = 1
(older / "upload/maps/a10.map").write_bytes(changed)
@@ -201,6 +202,7 @@ def test_finalize_copies_only_verified_retained_maps_and_keeps_old_upload(remote
struct.pack_into("<H", header, 18, 183)
(build / "halo").write_bytes(header)
(build / "libSDL3.so.0").write_bytes(header)
(build / "brokers.txt").write_text("broker.example.org:1883\n")
resources = remote.CACHE / "resources"
resources.mkdir()
(resources / "frame-controls.patch").write_bytes(b"verified native controls")
@@ -211,11 +213,13 @@ def test_finalize_copies_only_verified_retained_maps_and_keeps_old_upload(remote
monkeypatch.setattr(remote, "rename_noreplace", publish)
response = remote.finalize(NEW_RUN)
assert response["gamePath"] == str(remote.GAME)
assert (remote.GAME / "brokers.txt").read_text() == "broker.example.org:1883\n"
assert remote.read_marker(remote.GAME / remote.MARKER)["files"]["brokers.txt"] == remote.digest(remote.GAME / "brokers.txt")
assert remote.verify_maps(remote.GAME, manifest) == manifest["totalBytes"]
assert remote.verify_maps(older / "upload", manifest) == manifest["totalBytes"]
assert (older / "src/unfinished-build").read_bytes() == b"do not resume this old build"
assert not (remote.GAME / "unfinished-build").exists()
assert list((remote.GAME / "save").iterdir()) == []
assert list(remote.save_root().iterdir()) == []
class RetryConnection: