diff --git a/README.md b/README.md index 6dd32be..cc952d0 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ Frame's software fits together, all checked against a real headset and labelled | [Sideloading Linux and Windows games](docs/sideloading.md) | A .zip, folder or .exe as a Steam Devkit Game, runtime detection | | [Install links for websites](docs/web-install.md) | `frame-control://install` links and manifests, the rules, a button to paste | | [Steam games](docs/steam-games.md) · [VR video](docs/vr-video.md) · [WebXR in Chromium](docs/webxr-chromium.md) | Installing and buying, watching VR180/360, the Chromium build | +| [VR mods and custom songs](docs/mods.md) | Per-game feasibility, real-Frame results and blockers; no installer yet | | [SSH](docs/ssh.md) · [Streaming](docs/streaming.md) · [Files](docs/file-transfer.md) · [Panels](docs/panels.md) · [Tailscale](docs/tailscale.md) | Topic notes | | [Frame Control for iPhone](docs/iphone.md) | The iPhone and iPad app, how it runs the server on the Frame, pairing | | [Recovery and OS images](docs/recovery-and-images.md) | Where to download the Frame's OS, what's inside, testing without the headset | diff --git a/docs/evidence/mods-2026-09-28.md b/docs/evidence/mods-2026-09-28.md new file mode 100644 index 0000000..bc791f8 --- /dev/null +++ b/docs/evidence/mods-2026-09-28.md @@ -0,0 +1,128 @@ +# Mod feasibility test, 2026-09-28 + +**Verified observations**, with limits below. Device: `aarch64`, SteamOS +`VERSION_ID=0.4.1`, `BUILD_ID=20260925.6191901`; Proton version file: +`1788505046 proton-11.0-2c-arm64`; `vrcmd --stats`: SteamVR `2.18.1`. +Times below are the Frame's AEST clock. + +## Inventory and allowed content + +`ssh frame 'python3 - owned' < ui/frame_steam.py` returned 868 games before +the test. Beat Saber (620980) and Skyrim VR (611670) were absent. Half-Life 2: +VR Mod – Episode One (2177750) was installed. Hogwarts Legacy (990080), +Horizon Zero Dawn (1151640) and Horizon Forbidden West (2420110) were listed +but not installed. The full personal library is not committed. + +**Documented, owner report after these tests:** Alex has Beat Saber on a +Quest 2, which is charging. That copy was not accessed or inspected during +this test. Its version, transfer path and Frame compatibility remain unknown; +Steam ownership is still not established. + +Gravitas's public Steam metadata reported `is_free: true`, developer Galaxy +Shark Studios, Windows only. Frame Control's existing Steam helper requested +its install. Steam first returned free-license state 3, then config state 7, +then queued the download. The helper did not accept state 3; a later read +found state 7. The source of that transition was not observed. A later +`install 1067310` returned `state: installed`. No purchase occurred. + +UEVR was downloaded from the author's [1.05 release](https://github.com/praydog/UEVR/releases/tag/1.05), +not a mirror. `UEVR.zip` was 7,399,455 bytes. Its SHA-256 matched the author's +`UEVR.zip.sha256` (a UTF-16 text file): + +```text +af4f2f91306802d7ee4e8497d483a547ac8e9a3067dbafb81324100524215d3c +``` + +Microsoft's Windows x64 .NET runtime and Windows Desktop runtime 6.0.36 ZIPs +were downloaded using URLs in the official release metadata. Both SHA-512 +hashes matched that metadata. They were extracted into a test-only user +directory, not installed globally. No other mod manager was used. + +## Half-Life 2 VR: visible setup, not gameplay + +Launched the already-installed Episode One using +`steam steam://rungameid/2177750`. The existing manifest recorded build +25413453 and a shared base depot from app 658920, build 25413418. + +Selected fresh Steam / SteamVR log lines: + +```text +22:07:50 proton waitforexitandrun .../Half-Life 2 VR/ep1vr.exe +22:08:03 SetApplicationPid: Setting app steam.app.2177750 PID to 27990 +22:08:03 Successfully loaded binding file '.../hlvr/cfg/steamvr/bindings_frame.json' for app 'steam.app.2177750'. +``` + +The game's `episodicvr/console.log` reached `Creating VR hand HUD...`, +`Creating VR weapon HUD...` and `Calibrating VR base position`. It also +contained missing material/weapon warnings. SteamVR logged a missing +`frame_hmd` binding as well as the successful controller binding load; +controller input was not exercised. + +`ui/frame_vrshot.py` produced a stereo capture showing the mod's +“First time setup” and “Dominant hand” dialog. This establishes visible +startup, not a played level or comfortable performance. The capture includes +room passthrough and is deliberately not published. The test's `hl2.exe` +process was terminated; the pre-existing game installation was preserved. + +## Gravitas and UEVR: injection unverified + +The game was launched through Steam. The injector was then started in the +same `steamapps/compatdata/1067310` prefix using the shipped +`SteamLinuxRuntime_4-arm64/_v2-entry-point` and Proton 11 ARM64. The first +attempt reported: + +```text +Application: UEVRInjector.exe +Message: You must install .NET to run this application. +Architecture: x64 +App host version: 6.0.35 +.NET location: Not found +``` + +With `DOTNET_ROOT` and `DOTNET_ROOT_X64` pointing at the test-only Windows +runtime directory, Proton loaded `Microsoft.NETCore.App/6.0.36` and +`Microsoft.WindowsDesktop.App/6.0.36`. A 25-second launcher timeout was too +short to establish whether the UI worked. A longer attempt produced a +625×372 `UEVR` X11 window on display `:1`. This is window creation evidence, +not a successful injection. That launcher returned exit 0; this was not +treated as proof that a mod worked. + +A combined launch set `PROTON_REMOTE_DEBUG_CMD` to `UEVRInjector.exe` and +ran Gravitas's `Drop.exe` in the same runtime/prefix. The process +`SkyArk/Binaries/Win64/Drop-Win64-Shipping.exe` and a 1600×900 window titled +`SkyArk (64-bit, PCD3D_SM5)` appeared. The launcher exited **1** before an +injection or stereo game scene could be verified. Output included: + +```text +Proton: Error while copying to ".../windows/system32/amdxcffx64.dll": No such file or directory +Error [GENERAL | xrCreateInstance | OpenXR-Loader] : xrCreateInstance failed +X Error of failed request: BadWindow (invalid Window parameter) + Major opcode of failed request: 10 (X_UnmapWindow) +X Error of failed request: XI_BadDevice (invalid Device parameter) + Minor opcode of failed request: 28 (X_GetDeviceButtonMapping) +``` + +These errors do **not** establish an ARM64/FEX incompatibility. Other work +was launching apps on the shared Frame. SteamVR's PIDs changed during the +experiment; `ps` recorded the replacement `vrserver` and `vrcompositor` +starting at **22:13:03**, corroborated by `steamvr.service` journal startup +lines. This test did not request a Steam/SteamVR restart. The combined +attempt ran at 22:14:17–22:14:27, after that restart. A stable, coordinated +session is needed to distinguish launcher/environment problems from game or +mod incompatibility. + +## Cleanup and remaining checks + +- No game executable or OpenVR DLL was replaced. No global runtime or power + setting was changed by this test. No R.E.A.L., OpenComposite or Beat Saber + payload was installed. +- Removed the test-only UEVR/.NET directory and downloaded ZIPs from the + Frame. Final process checks found no `UEVRInjector.exe`, + `Drop-Win64-Shipping.exe`, `ep1vr.exe` or `hl2.exe`. SteamVR was running. +- Gravitas and its Steam-created prefix remain installed for a repeat test; + the pre-existing HL2 VR install remains. Steam may retain normal shader + caches, logs and prefix temporary files. +- Still unverified: UEVR injection and removal, R.E.A.L. releases and + permissions, OpenComposite, Beat Saber playback on either build, and our + own manager's end-to-end install/uninstall. No installer UI is justified + by these results. See the [support table and next checks](../mods.md). diff --git a/docs/mods.md b/docs/mods.md new file mode 100644 index 0000000..27a7c2a --- /dev/null +++ b/docs/mods.md @@ -0,0 +1,101 @@ +# Flat-to-VR mods and Beat Saber songs + +**Status: feasibility work, not an installer.** Frame Control does not yet +manage mods or Beat Saber songs. [Issue #26](https://github.com/saphid/frame-control/issues/26) +stays open: neither UEVR injection nor Beat Saber custom-song playback has +been verified on this Frame. Alex has an owned copy on a Quest 2; that copy +has not been inspected. There is no Mods button until the underlying +install, playback and removal have been checked. + +The mod manager must be Frame Control's own implementation. Mods and songs +are permitted third-party content; BSManager, ModsBeforeFriday, MO2 and other +managers must not be dependencies. Steam, Proton and SteamVR remain platform +dependencies. No game purchases, entitlement bypasses, withdrawn builds or +unofficial mod mirrors are part of this work. + +## Per-game support + +Checked 2026-09-28 on SteamOS **0.4.1**, BUILD_ID **20260925.6191901**, aarch64, +with **Proton 11.0-2c ARM64** and SteamVR **2.18.1**. “Verified” describes only +the observation stated, not a promise that the game is playable. “Documented” +means an upstream source describes it; “inferred” means it still needs a test. + +| Game / build | Mod or content | Evidence and support status | Next check | +|---|---|---|---| +| Half-Life 2: VR Mod – Episode One, Steam 2177750, build 25413453 | Official Steam community mod, shared base depot 658920 build 25413418 | **Verified: startup only.** Already installed; launched through Proton ARM64. The stereo headset capture showed its first-time setup, and SteamVR loaded `bindings_frame.json`. Gameplay, controller interaction, fresh installation and removal are unverified. | Complete first-time setup and play a level before offering a tested install shortcut. | +| Gravitas, Steam 1067310, Windows | UEVR 1.05 | **Verified: prerequisites and windows only.** Free Steam install completed. The game produced a `SkyArk (64-bit, PCD3D_SM5)` window. UEVR needed .NET; with official .NET 6.0.36 libraries it produced a `UEVR` window. A combined run exited 1 with X11 errors before injection was verified. **Inferred: compatibility remains unknown**, not proven broken. | Retry during a stable headset session; verify injection, stereo scene output, controls and removal. | +| Beat Saber, Steam 620980, Windows / Proton | Basic custom songs; later SongCore and version-matched mods | **Verified: absent from the 868-game library returned by this Frame.** Store metadata lists Windows, not Linux. **Documented:** the PC game reads basic maps from `Beat Saber_Data/CustomLevels` without a mod manager. Playback on Frame is unverified. | An already-owned, legitimately installed copy is required. Do not buy it as part of this task. | +| Beat Saber, claimed native ARM64 build | Custom songs / native mods | **Inferred: unverified.** The research mentions this build but supplies no verified official distributable or tested layout. CPU architecture alone does not identify Android versus Linux, the game version or the mod ABI. | Establish official provenance, ownership, binary type and version before touching files. Do not apply Quest patches to an unidentified build. | +| Beat Saber, Alex's Quest 2 copy | Custom songs / Android mods | **Documented: owner-reported copy on Quest 2**, currently charging. No APK, version or installed mods inspected; no Frame playback verified. This does not establish ownership of the Steam build. | When the Quest is available, inspect the owned copy's version and supported transfer path, then test Lepton/OpenXR compatibility without bypassing entitlement checks. | +| Hogwarts Legacy, Steam 990080 | R.E.A.L. | **Verified: listed in this Frame's owned library, not installed.** Official release access and redistribution permission were not established; the referenced author Patreon page returned HTTP 403. No archive downloaded or game tested. | Obtain a current free release from the author and confirm its terms before any test. A news report saying “free” is not a redistribution grant. | +| Horizon Zero Dawn, Steam 1151640; Horizon Forbidden West, Steam 2420110 | R.E.A.L. | **Verified: both listed as owned, neither installed.** Same source/permission blocker as above; runtime support is unverified. | Check each game's supported version against an accessible official release. | +| Half-Life 2 VR / other OpenVR games | OpenComposite, per-game replacement | **Documented:** forwards OpenVR calls to OpenXR. **Inferred: Frame compatibility unknown.** Not installed or tested. HL2 VR reached setup with the shipped OpenVR path already. | Test a specific game and replacement DLL only if needed; preserve its original DLL. Never switch the shared headset's runtime globally. | +| Doom / Quake / Half-Life Team Beef ports | Author's VR ports plus separately owned or free game data | **Inferred: untested.** Android ARM64 support does not establish OpenXR extension or controller compatibility on Lepton. | Choose an official release and legally usable data set, then test that exact port. | +| Skyrim VR, Steam 611670 | SKSEVR / HIGGS / PLANCK stack | **Verified: Skyrim VR is absent from this library.** Owning flat Skyrim or Special Edition is not the VR game's entitlement. Runtime and mod support are unverified. | An already-owned VR copy and version-matched official mod releases are required. | + +The [test record](evidence/mods-2026-09-28.md) distinguishes process startup, +visible output and failures. It also records a SteamVR restart during the +shared session, which prevents attributing the failed UEVR attempt to FEX. + +## Beat Saber: songs first + +**Documented:** the [BSMG PC guide](https://bsmg.wiki/pc-modding.html) +describes extracting each map into its own directory below +`Beat Saber/Beat Saber_Data/CustomLevels`. Basic custom songs do not require +SongCore; maps that require mod features need their matching dependencies. +This is a candidate for our own file manager, not a verified Frame feature. + +Alex's Quest 2 copy is a separate Android candidate. Its ownership does not +make the PC `CustomLevels` layout applicable. Until the actual build is +inspected, neither a direct song-copy recipe nor APK patching is justified. + +Only maps whose music and chart are permitted for distribution may be used +as test fixtures or bundled content. A public download alone does not establish +those rights. Start with an original or explicitly licensed basic map. + +**Documented:** [ModsBeforeFriday](https://github.com/Lauriethefish/ModsBeforeFriday) +targets Quest Beat Saber over WebUSB/ADB. It is not a generic native ARM64 +modding protocol. [BSManager](https://github.com/Zagrios/bs-manager/releases/tag/v1.6.0) +publishes an aarch64 Flatpak, but its architecture says nothing about Beat +Saber or its plugins running on Frame. Neither app is an installation step +or dependency for Frame Control. + +## Requirements for our manager + +These are **planned**, not implemented or verified: + +1. Resolve the selected Steam game's real library, installed build, executable + architecture and Proton prefix. Confirm ownership through Steam; a directory + or app manifest alone is not proof. Keep downloading, installed and playable + as separate states. +2. Download a pinned mod version from the author's official release. Record + the URL, version, license and digest. Verify the published digest when + available; an upstream SHA-256 detects corruption but is not a signature. + Do not treat “free to download” as permission to redistribute. +3. Stage and validate archives before writing into the game or prefix. Reject + path traversal, links escaping the destination, archive bombs and unexpected + executable content in song packs. Check song metadata and its referenced + files, not just the `.zip` suffix. +4. Refuse changes while the game is running. Back up originals and journal + every managed file and digest. Apply changes atomically where possible and + roll back partial failures. Keep runtime prerequisites scoped to this game. +5. Uninstall only files still matching our receipt; restore originals without + overwriting later user edits. Preserve saves, unrelated mods and songs. + Song removal must target one managed map, never the whole CustomLevels tree. +6. Expose one-click actions beside the game only after real-Frame install, + playback and uninstall pass. Test filesystem and download failure handling + with fake-Frame fixtures; those cannot prove FEX injection or VR rendering. + +## Sources + +- [UEVR 1.05 official release](https://github.com/praydog/UEVR/releases/tag/1.05) + and [author's usage instructions](https://github.com/praydog/UEVR#getting-started). +- [Microsoft .NET 6 release metadata](https://builds.dotnet.microsoft.com/dotnet/release-metadata/6.0/releases.json), + including the SHA-512 hashes used for the test runtimes. +- [OpenComposite's OpenXR branch](https://gitlab.com/znixian/OpenOVR/-/tree/openxr), + including per-game installation and the need to preserve original DLLs. +- [R.E.A.L. author post referenced by the research](https://www.patreon.com/realvr/posts/but-wheres-link-165840151) + (HTTP 403 from this environment; contents not verified). +- [Half-Life 2 VR official site](https://halflife2vr.com/) and + [Episode One on Steam](https://store.steampowered.com/app/2177750/). +- [Beat Saber store metadata](https://store.steampowered.com/api/appdetails?appids=620980). diff --git a/docs/steam-games.md b/docs/steam-games.md index 0d2e192..068a153 100644 --- a/docs/steam-games.md +++ b/docs/steam-games.md @@ -4,6 +4,10 @@ Frame Control's **Get games** section lists the games you own with each one's Steam Frame rating, installs them on the Frame, and searches the Steam store. This page covers how it works underneath, so you can do the same from a shell. +For flat-to-VR mods and Beat Saber custom songs, see the +[per-game feasibility table](mods.md). Mod support is separate from Steam's +Frame rating; there is no mod installer yet. + ## How it works The Frame's Steam client runs with `-cef-enable-debugging`. So its UI, a diff --git a/tests/test_steam.py b/tests/test_steam.py index d29d975..1faff89 100644 --- a/tests/test_steam.py +++ b/tests/test_steam.py @@ -13,6 +13,7 @@ ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "ui")) import frame_store # noqa: E402 +import frame_steam # noqa: E402 import test_server # noqa: E402 (not `from … import`, or unittest runs ServerGuards twice) @@ -49,6 +50,39 @@ class FrameSteamHelper(unittest.TestCase): self.assertEqual(out.returncode, 1, args) self.assertIn("error", json.loads(out.stdout), args) + def test_installed_game_is_not_reinstalled(self): + with mock.patch.object(frame_steam, "Page") as page, \ + mock.patch.object(frame_steam, "steam_url") as launch: + page.return_value.eval.return_value = {"name": "Gravitas", "installed": True} + self.assertEqual(frame_steam.install(1067310)["state"], "installed") + launch.assert_not_called() + + def test_license_and_eula_wait_for_headset(self): + # Seen during #26's free Gravitas install: state 3 is not permission + # to click through the license. The same guard applies to an EULA. + for state in (3, 8): + with self.subTest(state=state), \ + mock.patch.object(frame_steam, "Page") as page, \ + mock.patch.object(frame_steam, "steam_url") as launch, \ + mock.patch.object(frame_steam.time, "sleep"): + page.return_value.eval.side_effect = [ + None, {"app": 1067310, "state": state, "need": 1, "free": 100}] + self.assertEqual(frame_steam.install(1067310)["state"], "headset") + launch.assert_called_once_with("steam://install/1067310") + self.assertNotIn(mock.call("SteamClient.Installs.ContinueInstall()"), + page.return_value.eval.call_args_list) + + def test_insufficient_space_leaves_options_open(self): + with mock.patch.object(frame_steam, "Page") as page, \ + mock.patch.object(frame_steam, "steam_url"), \ + mock.patch.object(frame_steam.time, "sleep"): + page.return_value.eval.side_effect = [ + {"name": "Gravitas", "installed": False}, + {"app": 1067310, "state": 7, "need": 100, "free": 10}] + self.assertEqual(frame_steam.install(1067310)["state"], "headset") + self.assertNotIn(mock.call("SteamClient.Installs.ContinueInstall()"), + page.return_value.eval.call_args_list) + class HelperErrors(unittest.TestCase): def test_json_error_found_despite_ssh_stderr(self):