mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-18 17:00:30 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baaddad2ab | ||
|
|
f7be1185eb | ||
|
|
9f23227ccd | ||
|
|
f84ab3d9a2 | ||
|
|
a59b3ee77a | ||
|
|
453822921b | ||
|
|
eed3a933e0 |
No files matched your search
@@ -40,8 +40,29 @@ jobs:
|
||||
- name: Verify package installation in the rootless build container
|
||||
run: python scripts/check-build-container.py
|
||||
|
||||
native-arm64:
|
||||
# Fully link the patched VR game; source-only tests cannot catch every
|
||||
# missing guest/host bridge. No retail files or headset runtime are used.
|
||||
runs-on: ubuntu-26.04-arm
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Build and fully link the shipped native ARM64 VR game
|
||||
run: python scripts/check-native-build.py --timeout-seconds 2400 --report "$RUNNER_TEMP/native-build-proof.json"
|
||||
- name: Save build proof without publishing native game binaries
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: HaloFrameSetup-native-build-proof
|
||||
path: ${{ runner.temp }}/native-build-proof.json
|
||||
if-no-files-found: error
|
||||
|
||||
windows-executable:
|
||||
needs: [test, build-container]
|
||||
needs: [test, build-container, native-arm64]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
@@ -6,22 +6,36 @@
|
||||
<img src="resources/artwork/halo-ce-logo.png" alt="Halo: Combat Evolved" width="180">
|
||||
</p>
|
||||
|
||||
**Halo CE Steam Frame VR Mod — 1.4.0**
|
||||
<h1 align="center">Halo CE Steam Frame VR Mod</h1>
|
||||
|
||||
[**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.
|
||||
<h2 align="center">
|
||||
<a href="https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.1/Halo-Steam-Frame-Mod-Setup-1.4.1.exe">Download 1.4.1 — Windows installer (.exe)</a>
|
||||
</h2>
|
||||
|
||||
<p align="center">
|
||||
<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">
|
||||
<strong>Halo-Steam-Frame-Mod-Setup-1.4.1.exe</strong><br>
|
||||
Windows 10/11 x64 · One file · Runtime and USB tools included<br>
|
||||
<a href="https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.4.1">Release notes & SHA256 checksum</a>
|
||||
</p>
|
||||
|
||||
Play native ARM64/OpenXR Halo CE on Steam Frame with **LAN and online campaign co-op**, **head-directed walking**, and **Xbox-style buttons**. Tracked controller aiming, a render-rate reticle, half-strength stereo sun glow, and automatic Steam artwork and Notes are included.
|
||||
|
||||
**1.4.1 fixes first-mission VR aiming, look-training checks, tutorial buttons and the unarmed flashlight.** Install and Repair apply standing mode and snap turning. Choose Repair to update while keeping your existing 1.4.0 profiles and saves.
|
||||
|
||||
## Install or upgrade
|
||||
|
||||
You need a Steam Frame with internet access and **12 GiB of free storage**, plus your own authorized **original Xbox Halo CE ISO/XISO or extracted maps**. USA Rev 2 was validated with the stable installer. PC, Anniversary and MCC data are unsupported.
|
||||
|
||||
1. **Run the EXE and choose Install or Repair.** Select your Xbox game data for a first install. Choose Repair to update an existing native installation using its verified maps without another ISO.
|
||||
2. **Connect your Frame over USB-C or network SSH.** Enable Developer Mode, set the Frame's user password, and follow the installer's connection guide.
|
||||
3. **Save and close games, then start installation.** Keep Steam Home and SteamVR running, and leave the Frame awake and connected. Building on the Frame can take tens of minutes.
|
||||
|
||||
**1.4.1 updates the native game in place** and keeps **Halo: Combat Evolved VR (Native)** in Steam. Existing 1.4.0 profiles and saves stay in `save-v1.4`. When upgrading from 1.3.5, old saves are retained but its checkpoints are incompatible; create a **new profile and campaign**.
|
||||
|
||||
[Setup & co-op](docs/SETUP.md) · [Controls](docs/CONTROLS.md) · [Release notes](docs/RELEASE_NOTES.md) · [Sources](docs/SOURCES.md) · [Third-party notices](docs/THIRD_PARTY.md)
|
||||
|
||||
<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">
|
||||
|
||||
*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.*
|
||||
|
||||
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.
|
||||
|
||||
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.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.
|
||||
|
||||
[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)
|
||||
No retail executable, disc image, maps, keys or soundtrack is included or downloaded. Halo artwork and third-party components retain their rights and notices.
|
||||
+8
-6
@@ -1,6 +1,6 @@
|
||||
# Steam Frame controls — 1.4.0
|
||||
# Steam Frame controls — 1.4.1
|
||||
|
||||
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.
|
||||
1.4.1 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 |
|
||||
| --- | --- |
|
||||
@@ -22,16 +22,18 @@
|
||||
| Both grips held | Recenter the headset view; release a held foregrip first |
|
||||
| Left grip near a long gun's foregrip | Aim the gun with both hands |
|
||||
|
||||
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.
|
||||
Install and Repair apply controller aiming, standing height and snap turning. A fresh install uses 30-degree snap turns, 72 Hz, resolution scale 1.0 and two-handed aiming. Repair keeps your other preferences, including snap angle, player height and refresh rate. Gesture melee is disabled so **B** remains the predictable melee input. You can change supported options in **Settings > VR Setup** or the game's `config.toml`; smooth turning remains available. 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.
|
||||
When a weapon is equipped, 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 this behavior in the 1.4.0 baseline. 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.
|
||||
|
||||
Before a weapon is equipped, the flashlight also follows the rendered aiming hand with bounded elevation. Its presentation uses current light bounds without changing the simulation's light state. Equipped weapons keep their original flashlight markers.
|
||||
|
||||
## Local changes to upstream
|
||||
|
||||
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.
|
||||
2. Before a weapon is equipped, the unarmed/default reticle follows the aiming hand. Look at the first mission's technician with your headset. The five calibration dots also recognize pointing with the hand reticle, using the original five-degree target cone. Cryopod look calibration accepts physical headset movement and legitimate snap/smooth stick turns, while ignoring tracking jitter and recenter/menu/cinematic transitions. Follow the technician's instructions and look up, down, left or right as requested. These tutorial inputs do not add head rotation to the equipped weapon's aim or change the campaign 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).
|
||||
@@ -40,7 +42,7 @@ The game's Steam shortcut also suppresses Steam's duplicate virtual Xbox gamepad
|
||||
|
||||
## First launch
|
||||
|
||||
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.
|
||||
Keep Steam Home and the headset runtime awake during setup. Launch **Halo: Combat Evolved VR (Native)** from the Frame's library. 1.4.1 keeps existing 1.4.0 profiles and saves in `~/Games/HaloCENativeVR/save-v1.4`. When upgrading from 1.3.5, create a new profile and campaign because its old checkpoints are incompatible and setup does not migrate those 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.
|
||||
|
||||
|
||||
+19
-10
@@ -1,18 +1,27 @@
|
||||
# Halo CE Steam Frame VR Mod 1.4.0
|
||||
# Halo CE Steam Frame VR Mod 1.4.1
|
||||
|
||||
[**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.
|
||||
[**Download Halo-Steam-Frame-Mod-Setup-1.4.1.exe**](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.1/Halo-Steam-Frame-Mod-Setup-1.4.1.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.1); matching installer source is in the [v1.4.1 tag](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.4.1).
|
||||
|
||||
- **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.
|
||||
This patch fixes VR tutorial behavior, unarmed lighting and setup completion:
|
||||
|
||||
**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.
|
||||
- **Aim before receiving a weapon:** the unarmed/default reticle follows the tracked aiming hand. Equipped weapons retain the existing nominal bullet-hit alignment and render-rate preview.
|
||||
- **Look at the technician and panel:** person checks use headset gaze. The five tutorial dots also accept the unarmed hand reticle within their original five-degree target cone.
|
||||
- **Cryopod look calibration:** look tests reset their tracking correctly and accept headset movement and legitimate snap or smooth stick turns. Recentring, menus and cinematics do not count as tutorial progress; AI and remote players retain their behavior.
|
||||
- **Standing and snap defaults:** Install and Repair apply standing mode and snap turning. Other preferences, including snap angle and refresh rate, remain intact.
|
||||
- **Inversion prompt buttons:** tutorial Back no longer leaks into melee or other mapped gameplay actions while pressed or held. The reported crash still needs confirmation in the headset test; the profile's inversion-setting and save logic remain intact.
|
||||
- **Tracking loss:** a failed or partial OpenXR pose query cannot reuse an old valid head or hand pose to complete a tutorial target.
|
||||
- **Unarmed flashlight:** the beam follows the current rendered controller pose with bounded aim and matching render bounds, instead of the slower simulation attachment. Equipped-weapon flashlight markers retain their behavior.
|
||||
- **Native build fix:** tutorial button handling uses the engine's existing VR-session function, avoiding the missing-symbol failure at the final linking step. Long build errors retain their failure summary in the setup dialog.
|
||||
- **Completion display:** an optional Steam shortcut icon that has not loaded no longer holds a successful Install or Repair at 99%. Setup shows Complete at 100%; icon details remain in the activity log.
|
||||
|
||||
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.
|
||||
**Update an existing native installation with Repair.** Save and close Halo, then choose Repair to rebuild the patched game using its verified Xbox maps. The game stays at `~/Games/HaloCENativeVR` with the Steam title **Halo: Combat Evolved VR (Native)**. Existing 1.4.0 profiles and checkpoints remain in `save-v1.4`; this patch does not change the save format.
|
||||
|
||||
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.
|
||||
Upgrading from 1.3.5 retains its old saves and backs up the original config/program. The 1.4 engine cannot load old checkpoints from 1.3.5, so that upgrade requires a new profile and campaign. Setup does not migrate those checkpoints or profiles. The separate 1.4.0a2–a5 Experimental installations remain untouched.
|
||||
|
||||
Source remains pinned to `2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4`, native peer protocol **17**. LAN/online campaign, head-directed walking, Xbox-style buttons, stereo sun glow and automatic Steam artwork/Notes are retained. Use matching builds/maps for co-op; retail Halo clients cannot join. Restrictive NAT can prevent Internet connections because there is no gameplay relay.
|
||||
|
||||
The earlier headset report verified the 1.4.0 baseline, not these new tutorial or flashlight changes. This patch needs its own headset verification; automated checks do not establish tutorial behavior or Frame performance on hardware.
|
||||
|
||||
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.
|
||||
|
||||
[Setup guide](SETUP.md) - [Sources and credits](SOURCES.md) - [Third-party notices](THIRD_PARTY.md)
|
||||
[Setup guide](SETUP.md) · [Controls](CONTROLS.md) · [Sources and credits](SOURCES.md) · [Third-party notices](THIRD_PARTY.md) · [Previous 1.4.0 release](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/tag/v1.4.0)
|
||||
+14
-14
@@ -1,12 +1,12 @@
|
||||
# Halo CE Steam Frame VR mod — setup 1.4.0
|
||||
# Halo CE Steam Frame VR mod — setup 1.4.1
|
||||
|
||||
[Back to the project README](../README.md)
|
||||
|
||||
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.
|
||||
1.4.1 updates the native installation at `~/Games/HaloCENativeVR`, keeping the Steam title **Halo: Combat Evolved VR (Native)**. Existing 1.4.0 profiles and saves remain in `save-v1.4`. When upgrading from 1.3.5, its old saves are retained but you need a new profile/campaign for the 1.4 save format.
|
||||
|
||||
## Before you begin
|
||||
|
||||
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).
|
||||
Download and run the single [**Halo-Steam-Frame-Mod-Setup-1.4.1.exe**](https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.1/Halo-Steam-Frame-Mod-Setup-1.4.1.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.1).
|
||||
|
||||
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.
|
||||
|
||||
@@ -14,7 +14,7 @@ Halo artwork and trademarks remain copyrighted to their respective owners, and t
|
||||
|
||||
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 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 headset tester confirmed the 1.4.0 baseline's reticle motion and LAN/online campaign co-op. The tutorial and unarmed-flashlight changes in 1.4.1 need their own headset verification; the earlier report does not validate them. 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.
|
||||
|
||||
@@ -22,9 +22,9 @@ The executable uses the classic Halo PC Master Chief helmet-and-shoulders icon.
|
||||
|
||||
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.
|
||||
1.4.1 uses **`~/Games/HaloCENativeVR`** and the Steam title **Halo: Combat Evolved VR (Native)**. It keeps the 1.4.0 save format and **`~/Games/HaloCENativeVR/save-v1.4`**; existing 1.4.0 profiles and checkpoints do not require migration. 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 1.4 engine's larger game-state capacity makes 1.3.5 checkpoints incompatible. When upgrading from 1.3.5, create a new profile and campaign; setup does not automatically migrate those 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.
|
||||
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.1 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.
|
||||
|
||||
@@ -34,7 +34,7 @@ The build stages `brokers.txt` for internet invite/server discovery. The relativ
|
||||
|
||||
## 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.
|
||||
Both players should use **the same 1.4.1 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.
|
||||
@@ -76,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 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:
|
||||
Use **Halo: Combat Evolved for the original Xbox**. **USA Rev 2 was validated with the stable installer.** 1.4.1 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 |
|
||||
| --- | --- |
|
||||
@@ -88,9 +88,9 @@ These builds come from the [pinned port's compatibility table](https://github.co
|
||||
|
||||
### Already have Halo installed?
|
||||
|
||||
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.
|
||||
To update an existing installation, close **Halo: Combat Evolved VR (Native)** and choose **Repair**. Setup rebuilds the native program and applies the 1.4.1 patch, reusing verified Xbox maps without another ISO. Install also recognizes a verified 1.3.5 installation and upgrades its program in place. Setup replaces the native program, SDL library, broker list, patch, and notices. 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 update keeps existing 1.4.0 profiles/checkpoints in `save-v1.4` and backs up the original config and program in the owned build run's `previous-program-files` folder. Upgrading from 1.3.5 retains its old saves at their original location and selects `save-v1.4` for a new profile/campaign; there is no automatic migration of 1.3.5 checkpoints or profiles. Repairs keep 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.
|
||||
|
||||
@@ -127,7 +127,7 @@ With the client running, Steam controls artwork cleanup for the removed entry; s
|
||||
| Menu / View | Pause / scoreboard |
|
||||
| Both grips held | Recenter |
|
||||
|
||||
Motion aiming stays enabled. Smooth turning defaults to 90 degrees/second, refresh rate to 72 Hz, and render scale to 1.0. Repairs refresh the VR/control defaults and keep unrelated settings and saves. Head movement also satisfies the first-level look tutorial. The patch does not skip the campaign or increase combat difficulty.
|
||||
Install and Repair apply motion aiming, standing mode and snap turning. Fresh installs use 30-degree snap turns, 72 Hz and render scale 1.0. Repairs keep your other settings and saves. Head movement and deliberate stick turns satisfy the first-level look tutorial; the calibration dots also accept pointing with the unarmed hand reticle. The patch does not skip the campaign or increase combat difficulty.
|
||||
|
||||
The native shortcut uses this **per-game** launch option to prevent the same physical controller from being read through both OpenXR and Steam's virtual gamepad:
|
||||
|
||||
@@ -141,7 +141,7 @@ See [controls and patch details](CONTROLS.md).
|
||||
|
||||
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 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 left unchanged and recorded in Show activity. This optional icon does not block completion: successful Install and Repair show Complete at 100%.
|
||||
|
||||
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.
|
||||
|
||||
@@ -179,7 +179,7 @@ Full source attribution, decompilation lineage, multiplayer references, and thir
|
||||
- **Flat window instead of VR:** wake the headset and start SteamVR before launching. The port falls back to a flat window if no usable OpenXR session is available.
|
||||
- **Controls doubled:** check the native shortcut's launch option above. Steam Input may otherwise add a second virtual controller stream.
|
||||
- **Library entry absent:** click **Add to Steam again** to retry registration through the running client. If its library interface remains unavailable, use Steam's **Add a Non-Steam Game** with the executable and launch option above. Enable **Include in VR Library** and leave forced compatibility tools disabled in its properties. Setup's direct library-file edits require Steam to be already closed.
|
||||
- **Artwork or description absent:** Install and Repair add these automatically. If setup reports pending Steam information, click **Add to Steam again** after the run. The description is under **Notes**, not the non-Steam About panel. Pending artwork, icon, or Notes is reported separately from the shortcut; manual Notes includes copyable text. Existing custom artwork and notes are kept. Direct file edits require Steam to be already closed; Notes needs the running client's interface.
|
||||
- **Artwork or description absent:** Install and Repair add these automatically. If setup reports pending Steam information, click **Add to Steam again** after the run. The description is under **Notes**, not the non-Steam About panel. Pending artwork or Notes is reported separately from the shortcut; manual Notes includes copyable text. Optional shortcut-icon details stay in the activity log. Existing custom artwork and notes are kept. Direct file edits require Steam to be already closed; Notes needs the running client's interface.
|
||||
- **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.
|
||||
|
||||
@@ -195,7 +195,7 @@ python -m venv .venv
|
||||
.\.venv\Scripts\python scripts/build_release.py
|
||||
```
|
||||
|
||||
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.
|
||||
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.1 Git tag](https://github.com/ClassicWasTaken/HaloSteamFrameMod/tree/v1.4.1), 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.
|
||||
|
||||
|
||||
+18
-4
@@ -2,13 +2,13 @@
|
||||
|
||||
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-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.
|
||||
Sources were checked on 2026-10-06 against a local checkout at the exact engine revision. Release 1.4.1 adds tutorial, unarmed flashlight and setup-completion fixes to the 1.4.0 baseline. The earlier headset report confirmed the baseline's reticle motion and LAN/online campaign co-op; it does not verify the new tutorial or flashlight fixes. That 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.
|
||||
|
||||
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.
|
||||
Those contributions remain in 1.4.1. The 1.4.0 release changed the engine pin and movement patch; 1.4.1 retains that pin. Automated regression coverage does not replace physical Frame verification of the tutorial, interruption, cancellation, and full uninstall.
|
||||
|
||||
## Native game and VR
|
||||
|
||||
@@ -22,9 +22,23 @@ Those contributions remain in 1.4.0. This release separately changes the engine
|
||||
| [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/2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4/port/include/xdk/README.md) | The port supplies its own declarations; installing the proprietary Xbox SDK is unnecessary. |
|
||||
|
||||
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.
|
||||
The installer applies [its documented controls, reticle, tutorial, flashlight and campaign-menu patch](../resources/frame-controls.patch) to that pinned revision. The 1.4.1 patch's SHA256 is `1c1f8f7961831bdc543a82dd1f1ce95475a942e29c3c0fbe41b74d82439ab7f6`. The 1.4.0 baseline patch's SHA256 was `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
|
||||
### 1.4.1 tutorial changes
|
||||
|
||||
The local patch adds a controller-directed unarmed/default reticle without replacing the equipped weapon's nominal firing preview. It uses local headset gaze for look-at-person checks. Only the five named first-mission calibration panels also accept the unarmed hand-reticle ray within their original five-degree cone. Cryopod look calibration resets across tracking/test transitions and accepts headset movement and legitimate snap or smooth view turns, while preserving recenter, menu, cinematic, AI and remote-player guards. Install and Repair apply standing mode and snap turning while keeping other preferences. These changes require their own headset validation; the 1.4.0 report does not cover them.
|
||||
|
||||
Tutorial Back consumes the gameplay actions mapped to B until the physical button is released, while keeping the original inversion-setting and profile-save logic. Production C checks reproduce the prior melee leak; the reported headset crash has not been reproduced locally. OpenXR pose queries clear old validity before querying and require both position and orientation validity, so failed or partial tracking cannot complete a gaze/hand target with an old pose. Focused tests cover those failure and recovery paths using the checked-in Khronos headers.
|
||||
|
||||
The tutorial-button regression includes the actual `vr.h` declaration and links the extracted production `halo_vr_running` definition from a separate translation unit. This avoids a test stub masking a missing session-query function. An offline ARM64 relocatable-link check also verifies the new tutorial bridges across the compiled guest objects; it is not a fully linked game or hardware test.
|
||||
|
||||
### 1.4.1 unarmed flashlight and build verification
|
||||
|
||||
The unarmed flashlight uses the current cached rendered controller camera for its origin and roll, with the same bounded direction as the unarmed reticle. A rendering-only light copy supplies its visibility and diffuse/specular bounds. The active local flashlight is included despite stale tick cluster membership, with scene-list capacity checks and a zero-intensity guard. Equipped weapon markers, remote lights, simulation attachment/reconnection and actual shot calculations retain their behavior. Dynamic object-light selection remains tick-based; physical beam placement and appearance need a headset test.
|
||||
|
||||
`tools/test_vr_flashlight.py` compiles the actual presentation and light-bounds functions and checks render poses between simulation ticks, elevation, roll, eligibility, stale culling, capacity and unchanged armed/tick math. The release workflow also runs [the full native ARM64 VR build check](../scripts/check-native-build.py), using the exact shipped source pin, patch, build script and rootless container flags. It exports JSON proof only, without game data or a native game binary. A successful compile/link does not establish headset appearance or measured frame rate.
|
||||
|
||||
### Retained 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.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "halo-steam-frame-installer"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
description = "Community Halo CE VR mod installer for Steam Frame; original Xbox game data must be supplied separately"
|
||||
requires-python = ">=3.11"
|
||||
license = "MIT"
|
||||
|
||||
@@ -28,8 +28,12 @@ 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...'
|
||||
phase configure 'Checking Xbox controls, VR tutorial, unarmed flashlight, render-rate reticle, stereo sun glow and LAN/online campaign menus...'
|
||||
python3 tools/test_vr_locomotion.py --cc clang
|
||||
python3 tools/test_vr_tutorial.py --cc clang
|
||||
python3 tools/test_vr_tracking.py --cc clang
|
||||
python3 tools/test_vr_tutorial_buttons.py --cc clang
|
||||
python3 tools/test_vr_flashlight.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
|
||||
|
||||
+2423
-75
File diff suppressed because it is too large.
Load diff
@@ -106,13 +106,13 @@
|
||||
},
|
||||
{
|
||||
"name": "halo-steam-frame-installer",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"license_expression": "MIT",
|
||||
"declared_license": null,
|
||||
"project_urls": [],
|
||||
"notices": [
|
||||
{
|
||||
"path": "packages/halo-steam-frame-installer/halo_steam_frame_installer-1.4.0.dist-info/licenses/LICENSE",
|
||||
"path": "packages/halo-steam-frame-installer/halo_steam_frame_installer-1.4.1.dist-info/licenses/LICENSE",
|
||||
"sha256": "04a51df2edd89917e3099e2f6f171c36891f1d6ac234015867b57a1d10f928d5",
|
||||
"bytes": 1343
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +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.
|
||||
@@ -949,12 +949,13 @@ def merge_config(original, migrate=False):
|
||||
configured_save_path(settings)
|
||||
else:
|
||||
validate_save_paths(settings)
|
||||
required = {"vr": {"enabled": True, "aim": "controller", "movement": "head", "melee_gesture": False},
|
||||
required = {"vr": {"enabled": True, "aim": "controller", "movement": "head", "melee_gesture": False,
|
||||
"turn": "snap", "height": "standing"},
|
||||
"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}
|
||||
defaults = {"refresh_rate": 72.0, "resolution_scale": 1.0, "sun_glow_strength": 0.5,
|
||||
"smooth_turn_speed": 90.0, "two_handed": True, "depth": False}
|
||||
vr = settings.get("vr", {})
|
||||
for key, default in defaults.items():
|
||||
if key not in vr:
|
||||
@@ -1099,8 +1100,8 @@ def finalize(value, repair=False, adopt=False):
|
||||
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')
|
||||
'aim = "controller"\nmovement = "head"\nturn = "snap"\nsmooth_turn_speed = 90.0\n'
|
||||
'melee_gesture = false\ntwo_handed = true\nheight = "standing"\ndepth = false\n')
|
||||
if existing:
|
||||
original = ordinary(GAME / "config.toml").read_text() if (GAME / "config.toml").exists() else ""
|
||||
config = merge_config(original, migrate=existing["sourceCommit"] != SOURCE_COMMIT)
|
||||
@@ -1133,7 +1134,7 @@ def finalize(value, repair=False, adopt=False):
|
||||
"files": {name: digest(stage / name) for name in ("halo", "libSDL3.so.0", "brokers.txt", "frame-controls.patch")},
|
||||
"maps": {"files": 24, "bytes": manifest["totalBytes"]},
|
||||
"controls": "Head-directed walking, Xbox buttons, motion aim, LB grenade change, RB flashlight",
|
||||
"installerVersion": "1.4.0", "networkProtocol": 17, "saveRoot": str(save_root()),
|
||||
"installerVersion": "1.4.1", "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")
|
||||
|
||||
@@ -9,7 +9,7 @@ import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIST = ROOT / "dist"
|
||||
VERSION = "1.4.0"
|
||||
VERSION = "1.4.1"
|
||||
EXE_NAME = f"Halo-Steam-Frame-Mod-Setup-{VERSION}.exe"
|
||||
|
||||
def allowed_source_files():
|
||||
|
||||
@@ -31,6 +31,9 @@ PATCH_FILES = frozenset((
|
||||
"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",
|
||||
"source/hs/hs_library_external.c", "port/linux/arm64/host_vr.c",
|
||||
"tools/test_vr_tutorial.py", "tools/test_vr_tracking.py", "tools/test_vr_tutorial_buttons.py",
|
||||
"source/objects/object_lights.c", "tools/test_vr_flashlight.py",
|
||||
))
|
||||
|
||||
|
||||
@@ -271,17 +274,25 @@ def check_nominal_projectile_reticle(source):
|
||||
"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")
|
||||
pose = c_function(renderer, "vr_render_pointing_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",
|
||||
for integration in ("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.")
|
||||
require("vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose)"
|
||||
in c_function(renderer, "vr_render_projectile_pose"),
|
||||
"The equipped reticle must retain the current render pose and yaw.")
|
||||
unarmed = c_function(renderer, "vr_render_unarmed_ray")
|
||||
require("unit_get_weapon_count(unit_index) != 0" in unarmed
|
||||
and "_object_dead_bit" in unarmed and "unit->unit.player_index != player_index" in unarmed
|
||||
and "unit_inventory_get_weapon" in unarmed and "vr_render_pointing_pose(" in unarmed,
|
||||
"An unarmed reticle must be limited to a living owned player without an equipped weapon.")
|
||||
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.")
|
||||
@@ -292,6 +303,98 @@ def check_nominal_projectile_reticle(source):
|
||||
"Unit preview must share the original camera/muzzle projection math.")
|
||||
|
||||
|
||||
def check_unarmed_flashlight(source):
|
||||
read = lambda path: (source / path).read_text(encoding="utf-8")
|
||||
renderer = read("port/linux/game/vr_render.c")
|
||||
pose = c_function(renderer, "vr_render_player_flashlight_pose")
|
||||
for guard in ("!vr_render.active", "!vr_render.weapon_camera_set",
|
||||
"!vr_render.weapon_camera_valid", "render.local_player_index != 0",
|
||||
"director_camera_scripted && *director_camera_scripted",
|
||||
"!halo_vr_aiming()", "!halo_vr_gaze_tracking_valid()",
|
||||
"vr_render_unarmed_ray("):
|
||||
require(guard in pose, "Missing unarmed flashlight render-pose guard: " + guard)
|
||||
require("vr_render.weapon_camera.position" in pose and "vr_render.weapon_camera.up" in pose
|
||||
and "dot_product3d(" in pose and "normalize3d(" in pose
|
||||
and "halo_vr_view(" not in pose,
|
||||
"The flashlight must use the cached rendered hand pose with a perpendicular up vector.")
|
||||
lights = read("source/objects/object_lights.c")
|
||||
override = c_function(lights, "light_vr_unarmed_flashlight_pose")
|
||||
for guard in ("#ifdef HALO_VR", "light->parent_light_index == NONE",
|
||||
"light->object_index != NONE", "_point_light_dynamic_bit",
|
||||
"_light_definition_is_first_person_flashlight_bit",
|
||||
"vr_render_player_flashlight_pose("):
|
||||
require(guard in override, "Missing local flashlight light-definition guard: " + guard)
|
||||
bounds = c_function(lights, "light_compute_render_bounding_sphere")
|
||||
require("struct light_datum effective = *light_get(light_index)" in bounds
|
||||
and "&effective.position" in bounds and "&effective.forward" in bounds
|
||||
and "light_compute_bounding_sphere_from_basis(&effective" in bounds,
|
||||
"Render flashlight bounds must use a copy of the simulation light.")
|
||||
simulation = c_function(lights, "light_compute_bounding_sphere")
|
||||
reconnect = c_function(lights, "light_reconnect_to_map")
|
||||
require("light_compute_bounding_sphere_from_basis(light_get(light_index)" in simulation
|
||||
and "light_compute_render_bounding_sphere(" not in reconnect
|
||||
and "light_vr_unarmed_flashlight_pose(" not in reconnect,
|
||||
"Flashlight presentation must not move the simulation light's BSP partition.")
|
||||
include = c_function(lights, "lights_vr_include_unarmed_flashlights")
|
||||
require("light_unmarked(light_index)" in include and "object_get_function_value(" in include
|
||||
and "intensity <= 0.0f" in include and "slot < MAXIMUM_RENDERED_LIGHTS" in include
|
||||
and "MAXIMUM_RENDERED_LIGHTS - 1" in include,
|
||||
"Adding a local flashlight outside stale visibility must respect scene-list capacity.")
|
||||
preprocess = c_function(lights, "lights_preprocess_scene")
|
||||
require(0 <= preprocess.find("structure_visibility_find_objects(")
|
||||
< preprocess.find("lights_vr_include_unarmed_flashlights()")
|
||||
< preprocess.find("light_marker_end()"),
|
||||
"The current flashlight must be included before finishing scene-light visibility.")
|
||||
require("light_vr_unarmed_flashlight_pose(light, &light_parameters.position" in preprocess
|
||||
and "first_person_weapon_center_flashlight(" in preprocess,
|
||||
"The local unarmed override must be integrated without replacing armed flashlight markers.")
|
||||
for name in ("light_get_bounding_sphere", "lights_render_diffuse", "lights_render_specular"):
|
||||
require("light_compute_render_bounding_sphere(" in c_function(lights, name),
|
||||
"Flashlight visibility and drawing must share rendered bounds: " + name)
|
||||
for name in ("lights_render_diffuse", "lights_render_specular"):
|
||||
require("light_render_skips_clusters(light)" in c_function(lights, name),
|
||||
"The unarmed beam must not use stale tick cluster restrictions: " + name)
|
||||
require((source / "tools/test_vr_flashlight.py").is_file(),
|
||||
"Missing production C flashlight regression.")
|
||||
|
||||
|
||||
def check_tutorial(source):
|
||||
read = lambda path: (source / path).read_text(encoding="utf-8")
|
||||
player = read("source/game/player_control.c")
|
||||
require("halo_vr_turn_control(" in c_function(player, "handle_one_player_input")
|
||||
and "halo_vr_tutorial_reset(" in c_function(player, "player_control_action_test_reset")
|
||||
and "halo_vr_tutorial_turn(" in c_function(player, "player_control_action_test_note"),
|
||||
"The script action tests must share actual VR turns and reset their head anchor.")
|
||||
buttons = c_function(player, "player_control_action_test_check_reset_input_blob")
|
||||
require("halo_vr_running()" in buttons and "halo_vr_active" not in buttons
|
||||
and "vr_controls" in buttons
|
||||
and "_unit_control_use_equipment_bit, FALSE" in c_function(player, "player_control_action_test_check_reset_input_blob"),
|
||||
"A consumed VR tutorial Back button must suppress its mapped melee action.")
|
||||
vr = read("port/linux/src/vr.c")
|
||||
c_function(vr, "halo_vr_running")
|
||||
require("int halo_vr_running(void);" in read("port/linux/src/vr.h"),
|
||||
"The tutorial VR-running query must use the real engine API.")
|
||||
units = c_function(read("source/units/units.c"), "unit_can_see_point")
|
||||
require("vr_render_player_gaze(" in units and "local_player_index == 0" in units
|
||||
and "_object_dead_bit" in units,
|
||||
"Headset gaze must be limited to the living local primary player.")
|
||||
hs = read("source/hs/hs_library_external.c")
|
||||
panel = c_function(hs, "hs_vr_calibration_panel")
|
||||
require("degrees != 5.f" in panel and "levels\\\\a10\\\\a10" in panel
|
||||
and "_object_type_scenery" in panel,
|
||||
"Hand-reticle acceptance must retain the original named a10 panel gate.")
|
||||
require("hs_vr_calibration_panel(" in c_function(hs, "hs_unit_can_see_object")
|
||||
and "vr_render_player_pointing_ray(" in c_function(hs, "hs_unit_can_see_object"),
|
||||
"The unarmed presentation ray must be integrated into the panel check.")
|
||||
locate = c_function(read("port/linux/arm64/host_vr.c"), "host_vr_locate")
|
||||
require(0 <= locate.find("views->head.valid = 0") < locate.find("if (!vr.frame_begun)")
|
||||
and "XR_VIEW_STATE_POSITION_VALID_BIT" in locate
|
||||
and "XR_SPACE_LOCATION_POSITION_VALID_BIT" in locate,
|
||||
"Fresh full-pose queries must invalidate stale data before any failure return.")
|
||||
for script in ("tools/test_vr_tutorial.py", "tools/test_vr_tracking.py", "tools/test_vr_tutorial_buttons.py"):
|
||||
require((source / script).is_file(), "Missing tutorial production C regression: " + script)
|
||||
|
||||
|
||||
GRAPH_SMOKE = r'''
|
||||
import io
|
||||
from pathlib import Path
|
||||
@@ -374,6 +477,8 @@ def check_source(source: Path):
|
||||
check_controls(projection)
|
||||
check_stereo_glow_and_online_menu(projection)
|
||||
check_nominal_projectile_reticle(projection)
|
||||
check_tutorial(projection)
|
||||
check_unarmed_flashlight(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:])
|
||||
@@ -381,9 +486,11 @@ def check_source(source: Path):
|
||||
"protocolVersion": 17, "rendererTwoPassBinding": True, "campaignCoopIntegration": True,
|
||||
"headDirectedMovementIntegration": True, "xboxControlsPreserved": True,
|
||||
"stereoSunGlowIntegration": True, "boundedSunGlowStrength": True, "vrInternetCampaignMenu": True,
|
||||
"nominalProjectileReticleIntegration": True,
|
||||
"nominalProjectileReticleIntegration": True, "renderRateUnarmedFlashlightIntegration": True,
|
||||
"renderRateProjectilePreviewIntegration": True,
|
||||
"networkCampaignOnlyIntegration": True,
|
||||
"vrTutorialIntegration": True,
|
||||
"freshFullPoseTrackingIntegration": True,
|
||||
"offlineArm64NinjaGraph": True, "arm64LinkedBuild": False, "hardwareValidated": False}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Fully link the shipped native ARM64 VR build without retail game data.
|
||||
|
||||
Run as a regular aarch64 Linux user with rootless Podman. The source pin,
|
||||
container arguments, local patch and build script come from the installer.
|
||||
Only a JSON build proof is exported; the temporary game binary is discarded.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAX_BUILD_SECONDS = 45 * 60
|
||||
MAX_EXECUTABLE_BYTES = 1024 ** 3
|
||||
|
||||
|
||||
class NativeBuildError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def load_helper():
|
||||
spec = importlib.util.spec_from_file_location("native_check_remote_helper", ROOT / "resources/remote_install.py")
|
||||
helper = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = helper
|
||||
spec.loader.exec_module(helper)
|
||||
if (not re.fullmatch(r"[a-f0-9]{40}", helper.SOURCE_COMMIT)
|
||||
or helper.SOURCE_URL != "https://github.com/startupfoundry/halo-ce-universal.git"):
|
||||
raise NativeBuildError("The installer source URL or exact revision is unsupported by this check.")
|
||||
return helper
|
||||
|
||||
|
||||
def remaining(deadline, maximum=None):
|
||||
seconds = deadline - time.monotonic()
|
||||
if seconds <= 0:
|
||||
raise NativeBuildError("The native build check exceeded its time limit.")
|
||||
return min(seconds, maximum) if maximum is not None else seconds
|
||||
|
||||
|
||||
def command(arguments, deadline, maximum=300):
|
||||
return subprocess.run(arguments, check=True, text=True, stdout=subprocess.PIPE,
|
||||
timeout=remaining(deadline, maximum)).stdout.strip()
|
||||
|
||||
|
||||
def regular_file(path, parent):
|
||||
"""Refuse links or files outside this check's private build workspace."""
|
||||
path = Path(path)
|
||||
relative = path.relative_to(parent)
|
||||
if ".." in relative.parts:
|
||||
raise NativeBuildError("The native output path escapes the private workspace.")
|
||||
cursor = parent
|
||||
if parent.is_symlink() or parent.resolve() != parent:
|
||||
raise NativeBuildError("The native build workspace is not an ordinary private directory.")
|
||||
for component in relative.parts:
|
||||
cursor /= component
|
||||
if cursor.is_symlink():
|
||||
raise NativeBuildError("A native build output path contains a symbolic link.")
|
||||
details = path.stat()
|
||||
if (not stat.S_ISREG(details.st_mode) or details.st_nlink != 1
|
||||
or details.st_uid != os.getuid() or not 64 <= details.st_size <= MAX_EXECUTABLE_BYTES):
|
||||
raise NativeBuildError("The native build did not produce a private ordinary output file.")
|
||||
return details
|
||||
|
||||
|
||||
def native_elf_proof(path, parent):
|
||||
details = regular_file(path, parent)
|
||||
checksum = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(64)
|
||||
if (len(header) != 64 or header[:6] != b"\x7fELF\x02\x01"
|
||||
or struct.unpack_from("<H", header, 16)[0] not in (2, 3)
|
||||
or struct.unpack_from("<H", header, 18)[0] != 183):
|
||||
raise NativeBuildError("The output is not a little-endian ELF64 AArch64 executable/library.")
|
||||
checksum.update(header)
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
checksum.update(block)
|
||||
if path.stat().st_size != details.st_size:
|
||||
raise NativeBuildError("The native output changed during verification.")
|
||||
return {"bytes": details.st_size, "sha256": checksum.hexdigest(),
|
||||
"elfMachine": 183, "elfClass": "ELF64", "byteOrder": "little", "architecture": "aarch64"}
|
||||
|
||||
|
||||
def stop_owned_container(helper, identifier):
|
||||
"""Stop only this invocation's labeled container, never shared containers."""
|
||||
name = "halo-frame-installer-" + identifier
|
||||
inspected = subprocess.run(["podman", "container", "inspect", name], text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
|
||||
if inspected.returncode:
|
||||
# --rm may already have removed it, or the image pull never created it.
|
||||
return
|
||||
records = json.loads(inspected.stdout)
|
||||
if not isinstance(records, list) or len(records) != 1 or not isinstance(records[0], dict):
|
||||
raise NativeBuildError("Cannot verify the native check container before cleanup.")
|
||||
record = records[0]
|
||||
config = record.get("Config")
|
||||
labels = config.get("Labels") if isinstance(config, dict) else None
|
||||
container_id = record.get("Id")
|
||||
if (not isinstance(labels, dict) or labels.get("org.halo-frame-installer.owner") != helper.OWNER
|
||||
or labels.get("org.halo-frame-installer.run") != identifier
|
||||
or not isinstance(container_id, str) or not re.fullmatch(r"[a-f0-9]{64}", container_id)):
|
||||
raise NativeBuildError("Refusing to stop a container without this check's exact ownership labels.")
|
||||
# The immutable inspected ID avoids a name being reassigned during cleanup.
|
||||
subprocess.run(["podman", "stop", "--time", "10", container_id],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20)
|
||||
subprocess.run(["podman", "rm", "--force", container_id],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
|
||||
|
||||
|
||||
def build_container(helper, identifier, directory, deadline):
|
||||
args = helper.build_container_args(identifier, directory)
|
||||
process = subprocess.Popen(args, start_new_session=True)
|
||||
try:
|
||||
status = process.wait(timeout=remaining(deadline))
|
||||
if status:
|
||||
raise NativeBuildError(f"The shipped native ARM64 VR build failed (exit {status}).")
|
||||
except BaseException:
|
||||
try:
|
||||
stop_owned_container(helper, identifier)
|
||||
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error:
|
||||
print("Owned container cleanup: " + str(error), file=sys.stderr, flush=True)
|
||||
finally:
|
||||
# Includes a still-running image pull before a container exists.
|
||||
helper.stop_build_process(process)
|
||||
raise
|
||||
|
||||
|
||||
def write_proof(path, value):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + "." + uuid.uuid4().hex + ".tmp")
|
||||
try:
|
||||
with temporary.open("x", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, indent=2)
|
||||
stream.write("\n")
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--report", type=Path, default=ROOT / "native-build-proof.json")
|
||||
parser.add_argument("--timeout-seconds", type=int, default=MAX_BUILD_SECONDS)
|
||||
options = parser.parse_args(argv)
|
||||
if not 1 <= options.timeout_seconds <= MAX_BUILD_SECONDS:
|
||||
parser.error("--timeout-seconds must be between 1 and 2700 (45 minutes)")
|
||||
if sys.platform != "linux" or os.getuid() == 0:
|
||||
raise NativeBuildError("Run this check as a regular Linux user with rootless Podman.")
|
||||
if platform.machine().lower() not in ("aarch64", "arm64"):
|
||||
raise NativeBuildError("This full native build check requires an AArch64 Linux runner.")
|
||||
if any(shutil.which(tool) is None for tool in ("git", "podman")):
|
||||
raise NativeBuildError("The native build check requires Git and rootless Podman.")
|
||||
report = options.report.absolute()
|
||||
if report.suffix.lower() != ".json" or report.exists() or report.is_symlink():
|
||||
raise NativeBuildError("Choose a new JSON proof path so a stale report cannot imply build success.")
|
||||
started = time.monotonic()
|
||||
deadline = started + options.timeout_seconds
|
||||
helper = load_helper()
|
||||
info = json.loads(command(["podman", "info", "--format", "json"], deadline, 60))
|
||||
if not info.get("host", {}).get("security", {}).get("rootless", False):
|
||||
raise NativeBuildError("This check requires rootless Podman.")
|
||||
patch = ROOT / "resources/frame-controls.patch"
|
||||
script = ROOT / "resources/build-native.sh"
|
||||
patch_bytes, script_bytes = patch.read_bytes(), script.read_bytes()
|
||||
helper_sha = hashlib.sha256((ROOT / "resources/remote_install.py").read_bytes()).hexdigest()
|
||||
identifier = uuid.uuid4().hex
|
||||
print("Full native ARM64 VR link check: " + helper.SOURCE_COMMIT, flush=True)
|
||||
with tempfile.TemporaryDirectory(prefix="halo-native-link-check-") as scratch:
|
||||
directory = Path(scratch).resolve()
|
||||
source = directory / "src"
|
||||
command(["git", "init", str(source)], deadline, 30)
|
||||
command(["git", "-C", str(source), "remote", "add", "origin", helper.SOURCE_URL], deadline, 30)
|
||||
command(["git", "-C", str(source), "fetch", "--depth", "1", "origin", helper.SOURCE_COMMIT], deadline)
|
||||
command(["git", "-C", str(source), "checkout", "--detach", "FETCH_HEAD"], deadline, 30)
|
||||
if command(["git", "-C", str(source), "rev-parse", "HEAD"], deadline, 30) != helper.SOURCE_COMMIT:
|
||||
raise NativeBuildError("The fetched native engine does not match the shipped source pin.")
|
||||
staged_patch = directory / "frame-controls.patch"
|
||||
staged_patch.write_bytes(patch_bytes)
|
||||
command(["git", "-C", str(source), "apply", "--check", str(staged_patch)], deadline, 30)
|
||||
command(["git", "-C", str(source), "apply", str(staged_patch)], deadline, 30)
|
||||
(directory / "build-native.sh").write_bytes(script_bytes)
|
||||
build_container(helper, identifier, directory, deadline)
|
||||
binary = native_elf_proof(source / "build/linux_arm64/halo", directory)
|
||||
library = native_elf_proof(source / "build/linux_arm64/libSDL3.so.0", directory)
|
||||
proof = {"schemaVersion": 1, "result": "passed", "sourceUrl": helper.SOURCE_URL,
|
||||
"sourceCommit": helper.SOURCE_COMMIT,
|
||||
"patchSha256": hashlib.sha256(patch_bytes).hexdigest(),
|
||||
"buildScriptSha256": hashlib.sha256(script_bytes).hexdigest(),
|
||||
"remoteHelperSha256": helper_sha, "target": "linux_arm64", "vrEnabled": True,
|
||||
"releaseBuild": True, "fullNativeLink": True, "rootless": True,
|
||||
"hostArchitecture": platform.machine(), "nativeExecutable": binary,
|
||||
"sdlLibrary": library, "elapsedSeconds": round(time.monotonic() - started, 1),
|
||||
"hardwareValidated": False, "retailGameDataUsed": False}
|
||||
write_proof(report, proof)
|
||||
print(json.dumps(proof, separators=(",", ":")), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
def interrupted(signum, frame):
|
||||
raise NativeBuildError("The native build check was interrupted; stopping its owned container.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGTERM, interrupted)
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (NativeBuildError, OSError, ValueError, subprocess.SubprocessError, KeyboardInterrupt) as error:
|
||||
print("Native build check failed: " + str(error), file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -1,2 +1,2 @@
|
||||
"""Guided installer for the native ARM64 Halo CE Steam Frame VR port."""
|
||||
__version__ = "1.4.0"
|
||||
__version__ = "1.4.1"
|
||||
@@ -965,7 +965,6 @@ class App(tk.Tk):
|
||||
steam = self.last_result.steam if self.last_result is not None else {}
|
||||
pending_steam = (bool(self.last_result and self.last_result.requires_manual_steam_step)
|
||||
or steam.get("artwork", {}).get("status") == "manual"
|
||||
or steam.get("artwork", {}).get("icon", {}).get("status") == "manual"
|
||||
or steam.get("notes", {}).get("status") == "manual")
|
||||
allow_steam_retry = self.last_result is not None and pending_steam and self.mode.get() != "uninstall"
|
||||
self.retry_button.configure(state="normal" if not value and allow_steam_retry else "disabled")
|
||||
@@ -1234,7 +1233,16 @@ class App(tk.Tk):
|
||||
artwork = result.steam.get("artwork", {})
|
||||
artwork_pending = artwork.get("status") == "manual"
|
||||
icon = artwork.get("icon", {})
|
||||
icon_pending = icon.get("status") == "manual"
|
||||
# The shortcut icon is cosmetic. Keep its actual result in
|
||||
# activity without making a successful game operation pending.
|
||||
if icon:
|
||||
self._append("Steam shortcut icon (optional): " + str(icon.get("status", "unknown")))
|
||||
if icon.get("reason"):
|
||||
self._append("Steam shortcut icon details: " + icon["reason"])
|
||||
if icon.get("instructions"):
|
||||
self._append("Steam shortcut icon instructions: " + icon["instructions"])
|
||||
elif icon.get("status") == "manual":
|
||||
self._append("Steam shortcut icon instructions: Click Add to Steam again to retry the library icon.")
|
||||
notes = result.steam.get("notes", {})
|
||||
if notes:
|
||||
# The copyable fallback remains safe after the worker clears
|
||||
@@ -1243,7 +1251,7 @@ class App(tk.Tk):
|
||||
for key, value in notes.items()}
|
||||
result.steam["notes"] = notes
|
||||
notes_pending = notes.get("status") == "manual"
|
||||
info_pending = artwork_pending or icon_pending or notes_pending
|
||||
info_pending = artwork_pending or notes_pending
|
||||
if library_update and info_pending and not result.requires_manual_steam_step:
|
||||
done = "Native Halo VR was kept; some Steam library info needs one more step. Your game and saves are unchanged."
|
||||
self.progress_state.finish(pending=result.requires_manual_steam_step or info_pending or cleanup_pending)
|
||||
@@ -1251,9 +1259,6 @@ class App(tk.Tk):
|
||||
if artwork_pending:
|
||||
done += "\n\nHalo library art needs one more step.\n" + artwork.get("reason", "")
|
||||
done += "\n" + artwork.get("instructions", STEAM_ART_NOTE)
|
||||
if icon_pending:
|
||||
done += "\n\nHalo's library icon needs one more step.\n" + icon.get("reason", "")
|
||||
done += "\n" + icon.get("instructions", "Click Add to Steam again to retry the library icon.")
|
||||
if notes_pending:
|
||||
done += "\n\nThe game description in Steam Notes needs one more step.\n"
|
||||
done += notes.get("message", STEAM_NOTES_NOTE)
|
||||
@@ -1263,14 +1268,16 @@ class App(tk.Tk):
|
||||
done += "\n\nYour existing custom game note was kept."
|
||||
if cleanup_pending:
|
||||
done += "\n\nThe game operation finished; connection cleanup needs attention.\n" + cleanup_warning
|
||||
if info_pending:
|
||||
if info_pending or result.requires_manual_steam_step:
|
||||
self.retry_button.pack(anchor="w", pady=(11, 0))
|
||||
else:
|
||||
self.retry_button.pack_forget()
|
||||
done = self._redact(done)
|
||||
self.phase.set("One more step in Steam" if result.requires_manual_steam_step
|
||||
else "Ready to play · Steam info pending" if info_pending
|
||||
else "Connection cleanup needs attention" if cleanup_pending
|
||||
else "Steam info updated" if library_update
|
||||
else "You're ready to play")
|
||||
else "Complete")
|
||||
self.status.set(done)
|
||||
self._append(done)
|
||||
extra = "" if library_update else "\n\nXbox controls and motion aiming are enabled. The native port uses its own multiplayer protocol; legacy PC servers and PC saves are incompatible."
|
||||
|
||||
@@ -8,7 +8,8 @@ import subprocess
|
||||
def smoke_test(resources: Path) -> dict:
|
||||
from . import __version__
|
||||
from .gui import App, APP_TITLE, CONTENT_DISCLOSURE
|
||||
from .install import SOURCE_COMMIT
|
||||
from .install import InstallResult, SOURCE_COMMIT
|
||||
from . import gui
|
||||
from .ssh import Settings
|
||||
names = ('remote_install.py', 'steam_shortcut.py', 'steam_live.py', 'steam_notes.py', 'build-native.sh',
|
||||
'frame-controls.patch', 'ui/app-icon.png', 'ui/app-icon.ico',
|
||||
@@ -80,6 +81,29 @@ def smoke_test(resources: Path) -> dict:
|
||||
if ('Steam artwork and game Notes are added automatically' not in guidance
|
||||
or hasattr(app, 'library_card')):
|
||||
raise RuntimeError('Packaged install/repair lost automatic Steam info setup')
|
||||
# Exercise the reported icon-readback case in the packaged GUI,
|
||||
# without opening a dialog or connecting to a device.
|
||||
dialogs = []
|
||||
showinfo = gui.messagebox.showinfo
|
||||
gui.messagebox.showinfo = lambda title, message, **kwargs: dialogs.append((title, message))
|
||||
try:
|
||||
for repaired in (False, True):
|
||||
app.operation = 'install'
|
||||
app.progress_state.reset()
|
||||
app.events.put(('complete', InstallResult(
|
||||
'/home/steamos/Games/HaloCENativeVR', True,
|
||||
{'status': 'added', 'artwork': {'status': 'unchanged',
|
||||
'icon': {'status': 'manual',
|
||||
'reason': 'Steam shortcut icon has not loaded yet.'}},
|
||||
'notes': {'status': 'added'}}, None, repaired=repaired)))
|
||||
app._drain()
|
||||
if (float(app.progress['value']) != 100 or app.phase.get() != 'Complete'
|
||||
or app.retry_button.winfo_manager()
|
||||
or dialogs[-1][0] != 'Setup complete'
|
||||
or 'icon' in dialogs[-1][1].lower()):
|
||||
raise RuntimeError('Packaged successful setup was blocked by its optional Steam icon')
|
||||
finally:
|
||||
gui.messagebox.showinfo = showinfo
|
||||
finally:
|
||||
for callback in app.tk.call('after', 'info'):
|
||||
app.after_cancel(callback)
|
||||
@@ -95,5 +119,6 @@ def smoke_test(resources: Path) -> dict:
|
||||
'usbTransferAvailable': True,
|
||||
'steamInfoAutomaticOnInstall': True,
|
||||
'steamDescriptionUsesNotes': True,
|
||||
'optionalSteamIconNonBlocking': True,
|
||||
'usbToolsBundled': usb_bundled,
|
||||
'passwordReprRedacted': True, 'networkConnections': 0}
|
||||
@@ -300,8 +300,18 @@ class SSHConnection:
|
||||
marker = safe.find("HFI_ERROR ")
|
||||
if marker >= 0:
|
||||
safe = safe[marker + len("HFI_ERROR "):]
|
||||
safe = safe.replace(self.settings.password, "[redacted]")
|
||||
raise SSHError(safe[-12000:].strip() or f"Remote step failed (exit {status}).")
|
||||
if self.settings.password:
|
||||
safe = safe.replace(self.settings.password, "[redacted]")
|
||||
safe = safe.strip()
|
||||
if len(safe) > 12000:
|
||||
# Keep the helper's summary as well as the final compiler
|
||||
# diagnostics. A tail-only slice hid "Native build failed"
|
||||
# and made the popup begin in an unrelated object filename.
|
||||
summary = safe.splitlines()[0][:500]
|
||||
separator = "\n... [earlier details omitted]\n"
|
||||
remaining = 12000 - len(summary) - len(separator)
|
||||
safe = summary + separator + safe[-remaining:]
|
||||
raise SSHError(safe or f"Remote step failed (exit {status}).")
|
||||
return result
|
||||
except (paramiko.SSHException, OSError, EOFError):
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
|
||||
@@ -80,10 +80,10 @@ def test_protocol_11_engine_cannot_be_mislabeled_as_matching_experimental_coop(t
|
||||
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 "1.4.1" in text and "Halo-Steam-Frame-Mod-Setup-1.4.1.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
|
||||
assert "https://github.com/ClassicWasTaken/HaloSteamFrameMod/releases/download/v1.4.1/Halo-Steam-Frame-Mod-Setup-1.4.1.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
|
||||
|
||||
@@ -100,4 +100,6 @@ def test_actual_exact_pinned_engine_and_shipped_patch_when_checkout_is_supplied(
|
||||
assert result["nominalProjectileReticleIntegration"]
|
||||
assert result["renderRateProjectilePreviewIntegration"]
|
||||
assert result["networkCampaignOnlyIntegration"]
|
||||
assert result["vrTutorialIntegration"] and result["freshFullPoseTrackingIntegration"]
|
||||
assert result["renderRateUnarmedFlashlightIntegration"]
|
||||
assert not result["arm64LinkedBuild"] and not result["hardwareValidated"]
|
||||
+90
-4
@@ -1030,16 +1030,14 @@ def test_pending_steam_retry_does_not_read_game_data_or_build_and_clears_credent
|
||||
|
||||
|
||||
@WINDOWS_GUI
|
||||
@pytest.mark.parametrize('pending', ['icon', 'notes', 'artwork'])
|
||||
@pytest.mark.parametrize('pending', ['notes', 'artwork'])
|
||||
def test_steam_info_partial_results_stay_actionable_without_claiming_complete_update(app, pending):
|
||||
from halo_frame_installer.install import InstallResult
|
||||
app.operation = 'library'
|
||||
app.password_to_redact = 'offline-secret'
|
||||
steam = {'status': 'added', 'artwork': {'status': 'added', 'icon': {'status': 'added'}},
|
||||
'notes': {'status': 'added', 'title': 'About Halo', 'content': 'Campaign and controls.'}}
|
||||
if pending == 'icon':
|
||||
steam['artwork']['icon'] = {'status': 'manual', 'reason': 'Icon not persisted. offline-secret'}
|
||||
elif pending == 'notes':
|
||||
if pending == 'notes':
|
||||
steam['notes'].update(status='manual', message='Open Steam Notes. offline-secret')
|
||||
else:
|
||||
steam['artwork']['status'] = 'manual'
|
||||
@@ -1058,6 +1056,93 @@ def test_steam_info_partial_results_stay_actionable_without_claiming_complete_up
|
||||
assert bool(app.notes_button.winfo_manager()) is (pending == 'notes')
|
||||
|
||||
|
||||
@WINDOWS_GUI
|
||||
@pytest.mark.parametrize('operation', ['install', 'repair', 'library'])
|
||||
@pytest.mark.parametrize('reason', [
|
||||
"Steam's current shortcut icon has not loaded yet.",
|
||||
'Steam has not yet persisted the Halo icon selection.',
|
||||
"Steam's current shortcut icon could not be verified as empty.",
|
||||
])
|
||||
def test_optional_shortcut_icon_does_not_block_success_or_show_stale_retry(app, operation, reason):
|
||||
from halo_frame_installer.install import InstallResult
|
||||
app.operation = operation
|
||||
app.mode.set('repair' if operation == 'repair' else 'install')
|
||||
app.password_to_redact = 'offline-secret'
|
||||
icon = {'status': 'manual', 'reason': reason + ' offline-secret',
|
||||
'instructions': "Open Halo's library page before retrying the icon. offline-secret"}
|
||||
steam = {'status': 'added', 'artwork': {'status': 'added', 'icon': icon},
|
||||
'notes': {'status': 'added'}}
|
||||
result = InstallResult('/home/steamos/Games/HaloCENativeVR', False, steam, None,
|
||||
repaired=operation == 'repair')
|
||||
# A previous required Steam step may have left this action visible.
|
||||
app.retry_button.pack(anchor='w')
|
||||
app.events.put(('complete', result))
|
||||
_drain_events(app)
|
||||
assert float(app.progress['value']) == 100
|
||||
assert app.phase.get() == ('Steam info updated' if operation == 'library' else 'Complete')
|
||||
assert not app.retry_button.winfo_manager()
|
||||
expected_result = {'install': 'installed', 'repair': 'repaired', 'library': 'has been updated'}[operation]
|
||||
assert expected_result in app.status.get()
|
||||
popup_title, popup_message = app._test_dialogs[-1]
|
||||
assert popup_title == ('Steam info updated' if operation == 'library' else 'Setup complete')
|
||||
assert 'icon' not in app.status.get().lower() and 'icon' not in popup_message.lower()
|
||||
assert 'needs one more step' not in popup_message
|
||||
activity = '\n'.join(app.log_lines)
|
||||
assert 'Steam shortcut icon (optional): manual' in activity
|
||||
assert reason in activity and "Open Halo's library page before retrying the icon." in activity
|
||||
assert 'offline-secret' not in activity and '[redacted]' in activity
|
||||
assert result.steam['artwork']['icon'] == icon
|
||||
app.events.put(('clear_password',))
|
||||
app.events.put(('idle',))
|
||||
_drain_events(app)
|
||||
assert not app.retry_button.winfo_manager() and _disabled(app.retry_button)
|
||||
|
||||
|
||||
@WINDOWS_GUI
|
||||
@pytest.mark.parametrize('operation', ['install', 'repair', 'library'])
|
||||
@pytest.mark.parametrize('pending', ['shortcut', 'artwork', 'notes', 'cleanup'])
|
||||
def test_optional_icon_does_not_hide_required_pending_work(app, operation, pending):
|
||||
from halo_frame_installer.install import InstallResult
|
||||
app.operation = operation
|
||||
app.password_to_redact = 'offline-secret'
|
||||
steam = {'status': 'added',
|
||||
'artwork': {'status': 'added', 'icon': {'status': 'manual',
|
||||
'reason': 'Optional icon has not loaded. offline-secret'}},
|
||||
'notes': {'status': 'added'}}
|
||||
cleanup_warning = None
|
||||
if pending == 'shortcut':
|
||||
steam.update(status='manual', reason='Required Steam entry is missing. offline-secret')
|
||||
elif pending == 'artwork':
|
||||
steam['artwork'].update(status='manual', reason='Required banner is pending. offline-secret')
|
||||
elif pending == 'notes':
|
||||
steam['notes'].update(status='manual', message='Required Notes need a step. offline-secret')
|
||||
else:
|
||||
cleanup_warning = 'Connection cleanup is pending. offline-secret'
|
||||
app.events.put(('complete', InstallResult('/home/steamos/Games/HaloCENativeVR', False,
|
||||
steam, None, repaired=operation == 'repair', cleanup_warning=cleanup_warning)))
|
||||
app.events.put(('clear_password',))
|
||||
app.events.put(('idle',))
|
||||
_drain_events(app)
|
||||
assert float(app.progress['value']) == 99
|
||||
assert app.phase.get() == ('One more step in Steam' if pending == 'shortcut'
|
||||
else 'Connection cleanup needs attention' if pending == 'cleanup'
|
||||
else 'Ready to play · Steam info pending')
|
||||
assert bool(app.retry_button.winfo_manager()) is (pending != 'cleanup')
|
||||
assert 'Optional icon' not in app.status.get()
|
||||
assert 'Optional icon' not in app._test_dialogs[-1][1]
|
||||
expected_warning = {'shortcut': 'Required Steam entry is missing.',
|
||||
'artwork': 'Required banner is pending.',
|
||||
'notes': 'Required Notes need a step.',
|
||||
'cleanup': 'Connection cleanup is pending.'}[pending]
|
||||
assert expected_warning in app.status.get() and expected_warning in app._test_dialogs[-1][1]
|
||||
activity = '\n'.join(app.log_lines)
|
||||
assert 'Optional icon has not loaded.' in activity
|
||||
assert 'Click Add to Steam again to retry the library icon.' in activity
|
||||
assert 'offline-secret' not in app.status.get()
|
||||
assert 'offline-secret' not in app._test_dialogs[-1][1]
|
||||
assert 'offline-secret' not in '\n'.join(app.log_lines)
|
||||
|
||||
|
||||
@WINDOWS_GUI
|
||||
def test_manual_steam_description_is_copyable_and_never_overrides_about_game(app):
|
||||
import tkinter as tk
|
||||
@@ -1110,6 +1195,7 @@ def test_preserved_steam_info_is_successful_without_manual_pending(app, icon_sta
|
||||
assert float(app.progress['value']) == 100
|
||||
assert app.phase.get() == 'Steam info updated'
|
||||
assert 'existing custom game note was kept' in app.status.get()
|
||||
assert 'Steam shortcut icon (optional): ' + icon_status in '\n'.join(app.log_lines)
|
||||
assert not app.notes_button.winfo_manager()
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -550,6 +550,14 @@ def test_remote_detects_changed_original_xbox_header(remote, tmp_path):
|
||||
remote.verify_maps(directory, manifest)
|
||||
|
||||
|
||||
def test_repair_applies_standing_snap_without_resetting_other_vr_preferences(remote):
|
||||
old = '[vr]\nheight="seated"\nturn="smooth"\nsnap_turn_angle=45.0\nrefresh_rate=90.0\nplayer_height=1.8\n'
|
||||
updated = remote.tomllib.loads(remote.merge_config(old))
|
||||
assert updated['vr']['height'] == 'standing' and updated['vr']['turn'] == 'snap'
|
||||
assert updated['vr']['snap_turn_angle'] == 45.0
|
||||
assert updated['vr']['refresh_rate'] == 90.0 and updated['vr']['player_height'] == 1.8
|
||||
|
||||
|
||||
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))
|
||||
@@ -579,6 +587,7 @@ def test_new_config_missing_paths_gets_versioned_defaults(remote):
|
||||
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['vr']['turn'] == 'snap' and updated['vr']['height'] == 'standing'
|
||||
assert updated['network']['coop_enemies_mode'] == 'none'
|
||||
|
||||
|
||||
@@ -829,7 +838,7 @@ def test_verified_stable_upgrade_keeps_old_checkpoints_and_backs_up_config(remot
|
||||
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["installerVersion"] == "1.4.1" 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.
|
||||
|
||||
@@ -147,6 +147,33 @@ def test_remote_failure_shows_diagnostic_without_progress_protocol(monkeypatch,
|
||||
channel.close.assert_called_once()
|
||||
|
||||
|
||||
def test_long_remote_failure_keeps_summary_and_final_linker_diagnostic(monkeypatch):
|
||||
summary = 'Native build failed. The existing game was kept.'
|
||||
output = ('HFI_ERROR ' + summary + '\n'
|
||||
+ '[1372/1613] LINUX ARM64 CC intermediate_radiosity.o\n' * 300
|
||||
+ 'ld.lld: error: undefined symbol: halo_vr_active\n'
|
||||
+ 'compiler diagnostic private\n').encode()
|
||||
client = Mock()
|
||||
channel = Mock()
|
||||
states = iter([True, False, False])
|
||||
channel.recv_ready.side_effect = lambda: next(states)
|
||||
channel.recv.return_value = output
|
||||
channel.exit_status_ready.return_value = True
|
||||
channel.recv_exit_status.return_value = 1
|
||||
client.exec_command.return_value = (Mock(), Mock(channel=channel), Mock())
|
||||
monkeypatch.setattr(paramiko, 'SSHClient', lambda: client)
|
||||
connection = SSHConnection(Settings('frame', 'private'))
|
||||
with pytest.raises(SSHError) as error:
|
||||
connection.run(['python3', 'helper.py', 'build'])
|
||||
message = str(error.value)
|
||||
assert message.startswith(summary + '\n')
|
||||
assert 'ld.lld: error: undefined symbol: halo_vr_active' in message
|
||||
assert '[earlier details omitted]' in message
|
||||
assert '[redacted]' in message and 'private' not in message
|
||||
assert len(message) <= 12000 and 'HFI_ERROR' not in message
|
||||
channel.close.assert_called_once()
|
||||
|
||||
|
||||
def test_long_live_activity_is_forwarded_but_response_memory_is_bounded(monkeypatch):
|
||||
from collections import deque
|
||||
line = b'HFI_LOG {"stage":"compile","message":"' + b'x' * 1024 + b'"}\n'
|
||||
|
||||
Reference in new issue
Block a user