Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 a950de4b2b Release-ready: README for players, third-party notices, issue template
- README.md: what the mod is (a VR mod for Halo: Combat Evolved), what is
  needed (a disc image of one's own, purchased copy), simple installation
  for the Quest 3 / 3S (SideQuest) and the Steam Frame (Desktop Mode, the
  installer), playing, reporting problems, network use, credits (OpenCE,
  the decompilation, HaloCE-Quest-VR, HaloCE-VR, extract-xiso) and the
  legal notes (unofficial, not affiliated with Microsoft and others).
- THIRD_PARTY_NOTICES.md: the libraries and fonts and their licenses.
- Releases: the APK itself (RingfallMjolnirVR.apk) for sideloading, notes
  that say which file is for which headset, the project's name as title.
- An issue template (headset, build, log; no game files).
- docs/OPENCE_README.md marked as the base port's; the disc images'
  wording everywhere: made from one's own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:05:29 +02:00
Stefan MaierandClaude Opus 5.5 1984996d04 VR roadmap: comfort, buttons, import, installer and the new name done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:46 +02:00
Stefan MaierandClaude Opus 5.5 93b96cf5b8 Merge feature/rename-ringfall: Ringfall: Mjolnir VR, comfort and buttons, Frame installer, release signing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:25 +02:00
Stefan MaierandClaude Opus 5.5 f68db3b03e VR: the flashlight by tapping the head taken out again
Revert of c2aceb64: on the Quest 3 the tap did not toggle the flashlight.
The flashlight's button stays; the on-foot vignette that came after it is
kept. The roadmap and README no longer name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:53:48 +02:00
Stefan MaierandClaude Opus 5.5 bd08baacac Steam Frame installer: the shortcut added by Steam itself
On the Frame Desktop Mode runs inside Gaming Mode's session: closing Steam
to write shortcuts.vdf ended the session, and the installer with it, before
it wrote the shortcut (the library kept "Halo VR"). Steam now adds the
game itself (steam://addnonsteamgame, from a desktop file beside the game,
as SteamOS's "Add to Steam" does) and picks its id, which the installer
reads back for the library pictures. An old "Halo VR" shortcut is named
for the player to remove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:49 +02:00
Stefan MaierandClaude Opus 5.5 f39126e019 Steam Frame installer: Steam found by its pid file, an install log
Steam's client on SteamOS is not always a process named steam: the
installer may have written the shortcut while Steam ran, and Steam wrote its
own list back when it quit (the library kept "Halo VR"). Steam is now
found by ~/.steam/steam.pid or its ubuntu12_32/steam process, given time
after it ends, and the shortcut checked once written; what the installer
does goes to install.log beside the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:28:50 +02:00
Stefan MaierandClaude Opus 5.5 f649bd1a3e Release as Ringfall: Mjolnir VR
The product no longer carries Halo in its name; the game it plays is named
only to say which disc image it needs.

- The app: "Ringfall: Mjolnir VR", application id
  io.github.cesarus85.ringfall (the Java classes keep the decompilation's
  package), its update provider's authority from the id; the launcher's,
  updater's, host's and VR options' texts; the OpenXR application name.
- New icon and Steam library pictures, a tilted, broken ring with a falling
  streak (tools/ringfall_art.py, from shapes and Overpass; the helmet and
  the HALO logo gone).
- The Steam Frame: ~/Documents/RingfallMjolnirVR, RingfallMjolnirVR.apk,
  ringfall-mjolnir-vr.sh, Install-Ringfall-Mjolnir-VR.desktop and
  ringfall-mjolnir-vr-steam-frame.zip; the installer renames the folder of
  the builds before (~/Documents/HaloCE) and replaces their "Halo VR"
  shortcut and pictures.
- Releases: only the Android builds, as ringfall-mjolnir-vr-android-*.zip
  (the updater's names), and the Frame's installer; the repository
  Cesarus85/ringfall-mjolnir-vr.
- The signing key's default alias ringfall.
- README.md is the project's own; the port's moved to docs/OPENCE_README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:16:31 +02:00
37 changed files with 841 additions and 556 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: Problem report
description: Something does not work as it should
body:
- type: markdown
attributes:
value: |
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
- type: dropdown
id: headset
attributes:
label: Headset
options:
- Meta Quest 3
- Meta Quest 3S
- Steam Frame
- Other
validations:
required: true
- type: input
id: build
attributes:
label: Build number
description: The release you installed, e.g. "build 180".
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened
description: What you did, what you expected and what happened instead.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: |
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: How to install
url: https://github.com/Cesarus85/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
+20 -7
View File
@@ -252,13 +252,18 @@ jobs:
- name: Package
run: |
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
# (Ringfall: Mjolnir VR publishes its Android builds only, under
# its own name: Updater.java looks for them; the desktop ports are
# built to keep them building)
for config in debug release; do
(cd "artifacts/halo-android-$config" &&
zip -qr "$GITHUB_WORKSPACE/release/ringfall-mjolnir-vr-android-$config.zip" .)
done
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
# bundle with the release APK, and the desktop file that fetches it
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
# the APK itself, for sideloading on the Quest (SideQuest, adb)
cp artifacts/halo-android-release/app-release.apk release/RingfallMjolnirVR.apk
ls -l release
- name: Publish
@@ -270,10 +275,18 @@ jobs:
run: |
tag="build-$GITHUB_RUN_NUMBER"
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
{
printf '%s\n\n' "$subject"
printf '**Ringfall: Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
printf '| Meta Quest 3 / 3S | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
printf '| Steam Frame | `Install-Ringfall-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
printf 'The other files are for the built-in updater and the Steam Frame installer.\n\n'
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
} > notes.md
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Keep the last five
@@ -328,7 +341,7 @@ jobs:
title="Build failed"; color=15548997
fi
latest="$SERVER/$REPOSITORY/releases/latest"
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
if [ -n "$RELEASE_TAG" ]; then
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
else
+145 -104
View File
@@ -1,140 +1,181 @@
# Halo: Combat Evolved for Linux, Windows and Android
<p align="center">
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
</p>
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
# Ringfall: Mjolnir VR
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows and Android. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
multiplayer in full 6DoF virtual reality, with your hands in the game.
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
Ringfall: Mjolnir VR runs on your headset by itself, without a PC. It
brings no game files: it uses a disc image of **your own, purchased copy**
of the game.
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
> **Unofficial fan project.** Ringfall: Mjolnir VR is a free,
> non-commercial fan project. It is not affiliated with, endorsed,
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
> Bungie. See [Legal](#legal).
## Download
## Contents
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
- [What you get](#what-you-get)
- [What you need](#what-you-need)
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
- [Install on Steam Frame](#install-on-steam-frame)
- [Playing](#playing)
- [Problems and questions](#problems-and-questions)
- [Credits](#credits)
- [Legal](#legal)
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
## What you get
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
the HUD, menus, scope and crosshair float as layers of their own.
- **Your hands in the game**: the weapon sits in your hand and fires
where you point it; arms and fingers follow your controllers;
two-handed weapons, melee by swinging, grenades thrown with your hand,
crouching and leaning with your body.
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
turrets with handles.
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
left-handed mode, buttons remapped freely, graphics presets, refresh
rate and resolution, all in the in-game VR options (**Y**).
- **Multiplayer**: the PC version's menus and server browser, join codes,
password lobbies, co-op campaign and bots, up to 128 players;
cross-play with the PC and phone builds of
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates).
## What you need
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
original Xbox**, made from **a disc you own**. Any region and version
works.
## Game data
This project does not include, link to or help find game files. Please
do not ask for or share disc images in issues or discussions; such posts
are deleted.
The port does not include the game data. Download an Xbox disc image
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
## Install on Meta Quest 3 / 3S
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
You install the game once with a computer (Windows, macOS or Linux); after
that it updates itself.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md).
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
app on your phone, open your headset's settings, then *Developer
Mode* (Meta asks you to create a free developer account first).
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
"Advanced Installer") on your computer and connect the Quest with a
USB cable. In the headset, allow USB debugging for this computer.
3. **Download `RingfallMjolnirVR.apk`** from the
[latest release](../../releases/latest) and drag it onto the SideQuest
window. It installs the game.
4. **Copy your disc image** to the Quest's `Download` folder: in
SideQuest, open the file manager (the folder icon), go to `Download`
and drag the `.iso` file in.
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
your file in `Download`. The game copies what it needs (about 1.8 GB,
a minute or two) and starts.
## Platforms
You can delete the disc image from `Download` afterwards. When a new
version comes out, the game asks you at start whether to update.
Each platform has its own instructions:
## Install on Steam Frame
| Platform | Instructions |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
No computer and no keyboard needed.
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
*Switch to Desktop*.
2. **Put your disc image** in the `Downloads` folder (for example from a
USB stick or memory card with the file manager, Dolphin).
3. **Download `Install-Ringfall-Mjolnir-VR.desktop`** from the
[latest release](../../releases/latest) with the web browser.
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
If you are asked whether to run it, choose to continue.
5. **Answer the dialogs.** The installer finds your disc image, copies
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
library and offers to switch back to Gaming Mode.
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
## Multiplayer
To update, open the same file in Desktop Mode again; your settings,
saves and game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
The game can play system link games on a local network and on the internet:
## Playing
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines can play in the same game.
- An invite link lets a machine join a game on the internet. No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](port/linux/NETCODE.md).
- **Y** opens the VR options in the main and pause menus: turning,
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
*Quit the game* is on its last page.
- In the menus, point with a controller and pull the trigger to click.
- The controller layout is on the VR options' *Controller buttons* page.
## Build the game
## Problems and questions
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
Please open an [issue](../../issues) and include:
To build the game:
- your headset and the build number of the release you installed;
- what you did and what happened;
- the game's log, `debug.txt`:
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
(SideQuest's file manager);
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
`logcat.txt` and `install.log` beside it).
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
**Network use.** The game connects to the internet only for what you
use: the update check (GitHub), online games (to other players
directly), and join codes and lobby lists through public MQTT brokers
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
telemetry.
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
## Credits
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
Ringfall: Mjolnir VR stands on the work of others:
### Build options
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
Community Edition): the port of the decompiled game to Linux, Windows
and Android, with its renderer, netcode, PC menus and multiplayer, that
this project builds on and follows.
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
by moistman42069 (CC0): the arm IK, the forearm's twist and the
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
left-handed mode and the graphics presets follow its ideas.
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
trace markers) are used here.
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
image readers follow its format handling.
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
Give these options to `configure.py`:
Thank you all.
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
## Building it
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
and NDK needed). See [port/android/README.md](port/android/README.md) for
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
port it is built on.
### Optimization profiles
## Legal
The builds use profiles of the game to optimize the code:
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
It is not affiliated with, endorsed, sponsored or approved by Microsoft
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
here only to say which game this mod works with. Meta Quest is a trademark
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
Corporation.
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
No game data, art, music or other content of the game is included or
distributed. You need your own, legally purchased copy of the game. If
you enjoy this mod, please support the creators of the original by buying
their games.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
This project's own code is released under [CC0 1.0](LICENSE.md), as the
decompilation it builds on. Third-party code and fonts keep their own
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+46
View File
@@ -0,0 +1,46 @@
# Third-party notices
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
It includes or builds on the following, under their own licenses; each
license's full text is in the file named.
## Code
| Component | Used for | License | Text |
| --- | --- | --- | --- |
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) |
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
The disc image readers include this acknowledgement, as extract-xiso's
license asks: *This product includes software developed by in
&lt;in@fishtank.com&gt;.*
## Fonts
| Font | Used for | License | Text |
| --- | --- | --- | --- |
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
each font is used.
## Not included
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
executables) is part of this repository or its releases. The game reads
them from the player's own disc image.
+145
View File
@@ -0,0 +1,145 @@
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
> the port Ringfall: Mjolnir VR is built on, kept for its build and
> development notes. Its download links are OpenCE's. For the VR mod, see
> the [main README](../README.md).
# Halo: Combat Evolved for Linux, Windows and Android
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows and Android. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
## Download
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](../port/linux/README.md#updates).
Each build of the `main` branch that passes on all three platforms is a new
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
## Game data
The port does not include the game data. It needs an Xbox disc image
(`.xiso` or `.iso`) made from your own copy of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](../port/android/README.md).
## Platforms
Each platform has its own instructions:
| Platform | Instructions |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](../port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](../port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](../port/android/README.md) |
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
## Multiplayer
The game can play system link games on a local network and on the internet:
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines can play in the same game.
- An invite link lets a machine join a game on the internet. No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](../port/linux/NETCODE.md).
## Build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](../port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
+14 -12
View File
@@ -1,5 +1,7 @@
# Android
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
its Java classes keep the decompilation's package, `com.halo.decomp`).
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
`ninja android_apk` makes an app from it:
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
@@ -41,8 +43,8 @@ ninja) and these items:
## Game data
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
`.iso`) of any version of the game. The app extracts `maps/` from the disc
image. The app keeps the data in `/sdcard/Android/data/com.halo.decomp/files`.
`.iso`) made from your own copy of the game, any version. The app extracts `maps/` from the disc
image. The app keeps the data in `/sdcard/Android/data/io.github.cesarus85.ringfall/files`.
To install the data with the app:
@@ -57,16 +59,16 @@ To install the data with the app:
To install the data from a computer:
1. Start the app one time. The app makes its folders.
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
| --- | --- |
| Saved games (`z:\` and `u:\`) | `save` |
| Log | `debug.txt` |
| Settings | `config.toml` |
To make a copy of the saved games, enter
`adb pull /sdcard/Android/data/com.halo.decomp/files/save`.
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
## Controls
@@ -100,9 +102,9 @@ not accept touch input.
The settings are in `config.toml` in the data folder of the app. To change
them:
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
2. Change the file.
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
At the first start, the game writes the file with the default values. To
get the default values again, delete the file.
@@ -148,7 +150,7 @@ select "Yes":
1. The app downloads the new version.
2. The package installer of Android opens. At the first update, Android asks
you to let Halo install apps. Allow it.
you to let the game install apps. Allow it.
3. Select "Update". Android replaces the app.
4. Select "Open" to start the new version.
@@ -158,9 +160,9 @@ signature. GitHub Actions signs each build with the key in the
repository. A computer that builds for its own devices signs with the same
key when `port/android/signing.properties` (not in the repository) names it:
storeFile=/path/to/halo-vr-release.jks
storeFile=/path/to/ringfall-release.jks
password=<the keystore's password>
keyAlias=halo
keyAlias=ringfall
Keep the keystore and its password safe: a lost key means every player must
remove the app before the next version. If you installed a build that has a different signature, remove
@@ -190,7 +192,7 @@ instead.
Controls in play (in the menus the buttons are the Xbox's by position):
| Controller | Halo |
| Controller | Game |
| --- | --- |
| right trigger / left trigger | fire / throw grenade |
| right grip / left grip | melee / switch weapon |
@@ -238,7 +240,7 @@ through a shader that decodes to linear, and the stored bytes are the
game's. Camera shake is off in VR.
A file named `xr_test` in the data folder
(`adb shell touch /sdcard/Android/data/com.halo.decomp/files/xr_test`)
(`adb shell touch /sdcard/Android/data/io.github.cesarus85.ringfall/files/xr_test`)
starts a self-test instead of the game, which needs no game data: each eye
in its own colour (red while the right trigger is held), a checkerboard
screen in the room (its top-left square red), and the controllers' buttons
+27 -19
View File
@@ -7,28 +7,36 @@ What differs is found at run time, not by a build of its own.
## Installing it (players)
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
1. Put a disc image of Halo: Combat Evolved for the Xbox (`.iso` or
1. Put your own disc image of Halo: Combat Evolved for the original Xbox (`.iso` or
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
card).
2. Download `Install-Halo-VR.desktop` from the latest release and open it
2. Download `Install-Ringfall-Mjolnir-VR.desktop` from the latest release and open it
(double-click; confirm running it).
3. Answer the dialogs: the installer finds the disc image (or asks for it),
copies the game data out of it, adds "Halo VR" to the Steam library and
switches back to Gaming Mode. Start Halo VR from the library there.
copies the game data out of it, adds "Ringfall: Mjolnir VR" to the Steam library
and switches back to Gaming Mode. Start it from the library there.
Opening the same file again updates the game (settings, saves and game data
stay).
The desktop file (`steam_frame_installer.desktop`) downloads the latest
release's `halo-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
release's `ringfall-mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
the release job of `.github/workflows/build.yml`) and runs its
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
- `~/Documents/HaloCE`: the launcher (`halo-vr.sh`) and the release APK;
- `~/Documents/RingfallMjolnirVR`: the launcher (`ringfall-mjolnir-vr.sh`)
and the release APK (`RingfallMjolnirVR.apk`); the folder of the builds
before the name, `~/Documents/HaloCE`, renamed to it, and their "Halo VR"
shortcut replaced;
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
- the Steam shortcut (`shortcuts.vdf` as below, with `OpenVR` set, and the
library pictures), unless it is there: Steam is closed for that, as it
writes its shortcuts back when it quits.
- the Steam shortcut, unless it is there: added by Steam itself
(`steam://addnonsteamgame`, as SteamOS's "Add to Steam" does) from
`Ringfall Mjolnir VR.desktop` beside the game, then the library pictures
under the id Steam gave it. Steam is not closed: on the Frame, Desktop
Mode runs inside Gaming Mode's session, and closing Steam ends it (and the
installer with it). An old "Halo VR" shortcut is left for the player to
remove (Steam would write it back);
- what it did in `install.log` beside the game.
`HALO_VR_UNATTENDED=1` (and `HALO_VR_IMAGE=<image>`) runs it without
dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
@@ -44,7 +52,7 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
connected as well.
- Install with `adb install -r --no-streaming`. It prints "Lepton post
installation failed due to APP_PATH being unset", but installs. Start the
game with `am start -n com.halo.decomp/.LauncherActivity`.
game with `am start -n io.github.cesarus85.ringfall/com.halo.decomp.LauncherActivity`.
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
data must live elsewhere (stage 2). `/sdcard/Documents` and
`/sdcard/Download` are the SteamOS user's folders and are kept.
@@ -62,7 +70,7 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
| # | Stage | State |
| --- | --- | --- |
| 1 | Base: the fixes above on the current main | done |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/RingfallMjolnirVR` |
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
@@ -72,10 +80,10 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
A button in the Steam library ("Ringfall: Mjolnir VR", a non-Steam game)
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
`lepton waitforexitandrun -- ~/Documents/RingfallMjolnirVR/RingfallMjolnirVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
@@ -90,7 +98,7 @@ What the script does that Steam would for an Android title:
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
not Steam's): quit it with the VR options' "Quit the game" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
@@ -105,14 +113,14 @@ beside Lepton's `<frame-ip>:5555`.
Lepton uninstalls the app and empties its folders whenever it starts again.
The game's data lives in the SteamOS user's Documents instead: when
`/home/steamos/Documents` can be written, the launcher makes
`/home/steamos/Documents/HaloCE` and the launcher and the game take it as
`/home/steamos/Documents/RingfallMjolnirVR` and the launcher and the game take it as
their data folder (config.toml, saves and debug.txt go there too), so a
disc image imported in the app stays as well. The installer above fills it;
by hand, once:
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/RingfallMjolnirVR/maps
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/RingfallMjolnirVR/maps/
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/RingfallMjolnirVR
After a Lepton restart only the APK is installed again. (Use the path under
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
+13 -31
View File
@@ -1,6 +1,6 @@
# VR roadmap
What the VR port (Quest 3, Steam Frame) has, and what comes next, in the
What Ringfall: Mjolnir VR (Quest 3, Steam Frame) has, and what comes next, in the
agreed order. Updated with every step: a step is **done** once the user has
tried it in the headset and it is merged into `main`; **built** means in a
branch, not yet tried in the headset.
@@ -16,7 +16,7 @@ named where code is taken over.
| Area | What |
| --- | --- |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/HaloCE` |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/RingfallMjolnirVR` |
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
@@ -35,11 +35,12 @@ named where code is taken over.
| --- | --- | --- |
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **built** (branch `feature/vr-comfort`, headset test pending): buttons remapped, flashlight by tapping the head, finer turning and vignette choices |
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **done** (in `main` 2026-10-06; Quest 3 good): buttons remapped, finer turning and vignette choices; the flashlight by tapping the head taken out (did not work) |
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **built** (branch `feature/release-install`): the app's import (Quest; on the Frame now kept in `~/Documents/HaloCE`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`), a release signing key; test pending |
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Sprint by arm swing still open |
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
## Step 2, first part (done)
@@ -136,14 +137,15 @@ To try in the headset (Quest 3 and Steam Frame):
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
on the Quest; the Frame's row.
## Step 2, second part (built)
## Step 2, second part (done)
On branch `feature/vr-comfort`, not yet tried in the headset. The VR
In `main` 2026-10-06; tried on the Quest 3 (with the rename): good. The flashlight by tapping the head (after HaloCE-Quest-VR)
did not work in the headset and was taken out again; the button stays.
The VR
options (Y) have four pages: PLAY ("Weapon hand", "Mirrored controls",
laser, crosshair, motion tracker, body, fingers, the Warthog's wheel),
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons, the
flashlight's tap, "Reset buttons to defaults", the controller buttons
page) and MORE (graphics, refresh rate, resolution, Quit).
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons,
"Reset buttons to defaults", the controller buttons page) and MORE (graphics, refresh rate, resolution, Quit).
Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`):
- Jump, the flashlight (held: switch grenades), action/reload, switch
@@ -163,19 +165,6 @@ Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`):
wheel press nothing, whatever the grip does.
- The controller buttons page shows each action where its button is.
Flashlight by tapping the head (`vr.c` `flashlight_gesture_update`,
after HaloCE-Quest-VR, CC0): either hand to the side or the top of the
head and away, as a tap, toggles it; the button still does. A zone in
the head's axes beside or over the head, never more than 6 cm in front
of the eyes (a scope at the eye, a two-handed weapon, a punch and the
wrist's tracker are in front of the face); the hand comes from 35 cm
away, slower than 3 m/s, stays 40 to 600 ms, alone, and leaves; a second
between toggles. None in the menus, with a grenade held or just thrown,
just after a melee, with both hands on the weapon, the weapon hand while
the scope is drawn, a hand on the wheel. A short rumble in the tapping
hand. Its logic was run in a harness off the headset; the feel is the
headset's to judge.
Comfort (COMFORT page): snap turns 10 to 45 degrees by 5, 60, 75, 90;
smooth turning 30 to 150 degrees a second by 15, then to 300 by 30; the
vehicle vignette Off, 15 to 100%; the on-foot vignette (off by default)
@@ -195,7 +184,6 @@ below).
| `vr.button_crouch` | `"move_stick"` (default) |
| `vr.button_zoom` | `"turn_stick"` (default) |
| `vr.button_melee` | `"grip"` (default) |
| `vr.flashlight_gesture` | `true` (default): a tap of the head toggles the flashlight |
| `vr.foot_vignette` | `"off"` (default), `"turning"` (smooth turns), `"moving"` (and moving) |
| `vr.foot_vignette_strength` | 0.15 to 1, `0.7` (default) |
| `debug.vr_options` | `false` (default); `true` shows the VR options off the headset (the emulator) |
@@ -216,17 +204,11 @@ To try in the headset (Quest 3 and Steam Frame):
the wheel. Reset: all back.
3. The same left-handed with mirrored controls (on the Frame the left
D-pad), and with `Mirrored controls: Off`.
4. Flashlight tap with either hand, beside the head and over it; the
rumble; the flashlight button still works. No toggle while: aiming
through the sniper's and the pistol's scope, holding the weapon with
both hands, holding and throwing a grenade (overhand past the ear),
punching and swinging, putting on or adjusting the headset with both
hands or one, looking at the wrist's tracker, at the wheel.
5. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
4. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
100%; on-foot vignette "Smooth turns" (nothing with snap turns),
"Moving, turning", strength 15% and 100%; no vignette in a vehicle
from the on-foot one, none in the menus.
6. A game of 30 and more players: effects, glass and water stay; the log
5. A game of 30 and more players: effects, glass and water stay; the log
(`adb logcat -s halo`) says `rasterizer: N transparent geometry groups
in a window` once a window passes 384; frame time as before.
+4 -3
View File
@@ -20,14 +20,14 @@ def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
// debug key of the computer that builds
def signingKey = System.getenv("HALO_ANDROID_KEYSTORE")
def signingPassword = System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "ringfall"
def signingProperties = new File(rootDir, "signing.properties")
if (!signingKey && signingProperties.isFile()) {
def properties = new Properties()
signingProperties.withInputStream { properties.load(it) }
signingKey = properties.getProperty("storeFile")
signingPassword = properties.getProperty("password")
signingAlias = properties.getProperty("keyAlias", "halo")
signingAlias = properties.getProperty("keyAlias", "ringfall")
}
android {
@@ -35,7 +35,8 @@ android {
compileSdk 35
defaultConfig {
applicationId "com.halo.decomp"
// (the app's own id; the Java classes keep the decompilation's package)
applicationId "io.github.cesarus85.ringfall"
minSdk 28
targetSdk 35
versionCode Math.max(1, buildNumber)
@@ -93,7 +93,7 @@
<!-- the downloaded new version, for the package installer (Updater) -->
<provider
android:name=".UpdateProvider"
android:authorities="com.halo.decomp.update"
android:authorities="${applicationId}.update"
android:exported="false"
android:grantUriPermissions="true" />
@@ -27,7 +27,7 @@ import java.nio.channels.FileChannel;
* Starts the game once its data is in place.
*
* The game reads the Xbox game data (the folder holding maps/) from the
* app's external files directory, /sdcard/Android/data/com.halo.decomp/files.
* app's external files directory, /sdcard/Android/data/<application id>/files.
* If it is missing, this screen lets the player pick an Xbox disc image of
* the game (.xiso or .iso, any version) with the system file picker, and
* copies its maps folder there (XisoExtractor), as the desktop games do; or
@@ -35,7 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
private File dataRoot;
private TextView status;
@@ -147,15 +147,15 @@ public class LauncherActivity extends Activity {
layout.setBackgroundColor(Color.rgb(12, 16, 20));
TextView title = new TextView(this);
title.setText("Halo needs its game data");
title.setText("Ringfall: Mjolnir VR needs game data");
title.setTextColor(Color.WHITE);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
title.setGravity(Gravity.CENTER);
layout.addView(title);
TextView message = new TextView(this);
message.setText("Choose an Xbox disc image of Halo: Combat Evolved (an .iso or .xiso file, any "
+ "version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
message.setText("Choose your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or "
+ ".xiso file, any version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
+ "and you can delete the image afterwards.\n\n"
+ "You can also copy a maps folder from a computer:\n"
+ "adb push <folder with maps>/. " + (dataRoot != null ? dataRoot.getAbsolutePath() : "") + "/");
@@ -13,13 +13,13 @@ import java.io.FileNotFoundException;
/**
* Hands Android's package installer the downloaded new version of the app
* (Updater), read-only, the one file content://com.halo.decomp.update/halo.apk.
* (Updater), read-only, the one file content://<application id>.update/update.apk.
* (The app has no AndroidX, whose FileProvider does the same.)
*/
public class UpdateProvider extends ContentProvider {
static final String AUTHORITY = "com.halo.decomp.update";
static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".update";
static final String DIRECTORY = "update";
static final String APK = "halo.apk";
static final String APK = "update.apk";
private File apk() {
return new File(new File(getContext().getCacheDir(), DIRECTORY), APK);
@@ -49,7 +49,8 @@ import java.util.zip.ZipInputStream;
final class Updater {
/* the VR port's own releases: the original's, made without VR, must
never replace it (they share the package name) */
private static final String REPOSITORY = "Cesarus85/halo-ce-vr";
private static final String APP_NAME = "Ringfall: Mjolnir VR";
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
private static final String USER_AGENT = "halo-ce-universal-updater";
private static final int TIMEOUT_MILLISECONDS = 20000;
@@ -202,8 +203,8 @@ final class Updater {
if (activity.isFinishing())
return;
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setMessage("A new version of Halo was detected (build " + latest + "; this is build "
.setTitle(APP_NAME + ": new version")
.setMessage("A new version of " + APP_NAME + " was detected (build " + latest + "; this is build "
+ BuildConfig.HALO_BUILD_NUMBER + ").\n\nDo you want to update? The game will close and start "
+ "the new version.")
.setCancelable(false)
@@ -215,7 +216,7 @@ final class Updater {
private static void confirmNever(Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("Stop asking about new versions?\n\nTo ask again, set auto = true in the [update] section "
+ "of config.toml.")
.setCancelable(false)
@@ -232,7 +233,7 @@ final class Updater {
/* ---------- updating */
private static void update(Activity activity, int latest) {
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
String asset = "ringfall-mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
File directory = new File(activity.getCacheDir(), UpdateProvider.DIRECTORY);
LinearLayout layout = new LinearLayout(activity);
TextView status = new TextView(activity);
@@ -247,7 +248,7 @@ final class Updater {
layout.addView(status);
layout.addView(bar);
AlertDialog progress = new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setView(layout)
.setCancelable(false)
.show();
@@ -275,7 +276,7 @@ final class Updater {
activity.runOnUiThread(() -> {
progress.dismiss();
new AlertDialog.Builder(activity)
.setTitle("Halo: new version")
.setTitle(APP_NAME + ": new version")
.setMessage("The update failed:\n\n" + e.getMessage())
.setPositiveButton("OK", null)
.show();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Halo</string>
<string name="app_name">Ringfall: Mjolnir VR</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+4 -4
View File
@@ -63,7 +63,7 @@ void host_fatal(const char *format, ...)
vsnprintf(message, sizeof(message), format, arguments);
va_end(arguments);
__android_log_write(ANDROID_LOG_FATAL, "halo", message);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Halo", message, NULL);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Ringfall: Mjolnir VR", message, NULL);
_exit(1);
}
@@ -91,7 +91,7 @@ int host_errno(void)
static char data_root[512];
static char save_root[512];
/* where a device that empties the app's folders keeps the game's data */
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
#define SHARED_DATA_ROOT "/home/steamos/Documents/RingfallMjolnirVR"
void host_android_path(int which, char *buffer, uint32_t size)
{
@@ -292,7 +292,7 @@ static void *game_main(void *unused)
}
if (!directory_has_maps(data_root))
{
host_fatal("The Halo game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
host_fatal("The game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
"the folder that contains maps, into\n%s\nor import it from the launcher screen.", data_root);
}
@@ -400,7 +400,7 @@ int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
host_logf(HOST_LOG_INFO, "Halo for Android starting");
host_logf(HOST_LOG_INFO, "Ringfall: Mjolnir VR starting");
host_install_signal_handlers();
if (host_native_thread_create(game_main, NULL, MAIN_STACK_SIZE) != 0)
host_fatal("cannot start the game thread");
+2 -2
View File
@@ -1232,7 +1232,7 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
android.applicationVM = machine;
android.applicationActivity = activity;
instance.next = &android;
strcpy(instance.applicationInfo.applicationName, "Halo VR");
strcpy(instance.applicationInfo.applicationName, "Ringfall Mjolnir VR");
instance.applicationInfo.applicationVersion = 1;
strcpy(instance.applicationInfo.engineName, "halo-ce-universal");
instance.applicationInfo.engineVersion = 1;
@@ -2167,7 +2167,7 @@ void host_xr_test(void)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
window = SDL_CreateWindow("Halo", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
window = SDL_CreateWindow("Ringfall: Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
context = window ? SDL_GL_CreateContext(window) : NULL;
if (!context || !SDL_GL_MakeCurrent(window, context))
host_fatal("VR test: no OpenGL ES context: %s", SDL_GetError());
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 71 KiB

+162 -140
View File
@@ -1,31 +1,65 @@
#!/bin/bash
# Halo VR's installer for the Steam Frame, run in its Desktop Mode from the
# bundle the release's "Install Halo VR" desktop file downloads
# (steam_frame_installer.desktop; the bundle: this script, HaloVR.apk,
# halo-vr.sh = steam_frame_launch.sh, steam_frame_xiso.py and the library
# pictures in grid/). Everything is answered with dialogs, no keyboard:
# - the game in ~/Documents/HaloCE (port/android/STEAM_FRAME.md), its APK
# replaced (Lepton installs a newer one at the next start), so running the
# installer again updates the game;
# - the game data: the maps folder copied out of a disc image of Halo
# (.iso or .xiso) found in Downloads, Documents, the Desktop or on a
# memory card, or chosen, unless it is there already;
# - "Halo VR" in the Steam library, with its pictures, unless it is there
# already: Steam is closed for that (it keeps its shortcuts in memory and
# writes them back when it quits), and started again in Gaming Mode.
# HALO_VR_UNATTENDED=1 answers every question with its default and prints
# instead of showing dialogs (HALO_VR_IMAGE: the disc image to use), for
# Ringfall: Mjolnir VR's installer for the Steam Frame, run in its Desktop
# Mode from the bundle the release's "Install Ringfall: Mjolnir VR" desktop
# file downloads (steam_frame_installer.desktop; the bundle: this script,
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
# steam_frame_xiso.py and the library pictures in grid/). Everything is
# answered with dialogs, no keyboard:
# - the game in ~/Documents/RingfallMjolnirVR (port/android/STEAM_FRAME.md),
# its APK replaced (Lepton installs a newer one at the next start), so
# running the installer again updates the game; the folder of the builds
# before the name, ~/Documents/HaloCE, becomes it;
# - the game data: the maps folder copied out of a disc image of the
# original Xbox game (.iso or .xiso) found in Downloads, Documents, the
# Desktop or on a memory card, or chosen, unless it is there already;
# - the game in the Steam library, with its pictures, unless it is there
# already: added by Steam itself (steam://addnonsteamgame, as SteamOS's
# "Add to Steam" does), from a desktop file beside the game. Steam is not
# closed: it keeps its shortcuts in memory and writes them back, and on the
# Steam Frame Desktop Mode runs inside Gaming Mode's session, which ends
# with Steam. The shortcut of the builds before the name ("Halo VR") is
# left for the player to remove (Steam would write it back).
# RINGFALL_UNATTENDED=1 answers every question with its default and prints
# instead of showing dialogs (RINGFALL_IMAGE: the disc image to use), for
# testing over adb; it never closes Steam.
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
HALO_DIR="$HOME/Documents/HaloCE"
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
STEAM_DIR="$HOME/.local/share/Steam"
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
TITLE="Halo VR"
NAME="Halo VR"
EXE="\"$HALO_DIR/halo-vr.sh\""
TITLE="Ringfall: Mjolnir VR"
NAME="Ringfall: Mjolnir VR"
APK_NAME="RingfallMjolnirVR.apk"
LAUNCHER="ringfall-mjolnir-vr.sh"
EXE="\"$GAME_DIR/$LAUNCHER\""
# (the desktop file Steam adds the game from)
NAME_FILE="Ringfall Mjolnir VR"
# the builds before the name: their folder and Steam shortcut
OLD_DIR="$HOME/Documents/HaloCE"
OLD_NAME="Halo VR"
unattended() { [ "${HALO_VR_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
# what the installer did, for looking into problems: written to /tmp while
# it runs, then kept beside the game as install.log
LOG="/tmp/ringfall-mjolnir-vr-install.log"
: > "$LOG"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
trap keep_log EXIT
log "installer from $BUNDLE"
# whether Steam runs: its pid file (~/.steam/steam.pid), or its client's
# process (ubuntu12_32/steam: the process is not always named "steam")
steam_running()
{
local pid
pid="$(cat "$HOME/.steam/steam.pid" 2>/dev/null)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
}
unattended() { [ "${RINGFALL_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
info()
{
@@ -34,6 +68,7 @@ info()
fail()
{
log "error: $1"
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
exit 1
}
@@ -49,12 +84,12 @@ ask()
choose_image()
{
if unattended; then return 1; fi
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved (.iso or .xiso)" \
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved for the Xbox (.iso or .xiso)" \
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
--file-filter="All files | *"
}
# the disc images near at hand, largest first (a Halo image is a few GB;
# the disc images near at hand, largest first (the game's image is a few GB;
# anything under 500 MB is not one)
find_images()
{
@@ -68,10 +103,10 @@ extract_maps()
local image="$1" log
log="$(mktemp)"
if unattended; then
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$HALO_DIR" 2>"$log" | grep '^#' | tail -n 1
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" | grep '^#' | tail -n 1
status=${PIPESTATUS[0]}
else
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$HALO_DIR" 2>"$log" |
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" |
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
--text="Copying the game data out of the disc image..."
status=${PIPESTATUS[0]}
@@ -84,39 +119,36 @@ extract_maps()
rm -f "$log"
}
# the shortcut's id as Steam makes it for a non-Steam game: its pictures in
# config/grid are named after it
# the Steam users' shortcut files (userdata/0 is no user)
shortcut_files()
{
local config
for config in "$STEAM_DIR"/userdata/*/config; do
case "$config" in */userdata/0/*) continue ;; esac
[ -f "$config/shortcuts.vdf" ] && echo "$config/shortcuts.vdf"
done
}
# whether a Steam user's library holds the launcher given
have_shortcut_to()
{
local file
while read -r file; do
grep -qaF "$1" "$file" && return 0
done < <(shortcut_files)
return 1
}
have_shortcut() { have_shortcut_to "$GAME_DIR/$LAUNCHER"; }
# the id Steam gave the shortcut whose Exe is $2, in shortcuts.vdf $1 (Steam
# picks it; its library pictures in config/grid are named after it)
shortcut_id()
{
python3 -c 'import sys, zlib; print(zlib.crc32((sys.argv[1] + sys.argv[2]).encode()) | 0x80000000)' "$EXE" "$NAME"
}
python3 - "$1" "$2" <<'PYTHON'
import struct, sys
# whether every Steam user's shortcuts.vdf already holds the game
have_shortcut()
{
local found=1 config
for config in "$STEAM_DIR"/userdata/*/config; do
[ -d "$config" ] || continue
case "$config" in */userdata/0/*) continue ;; esac
grep -qaF "$HALO_DIR/halo-vr.sh" "$config/shortcuts.vdf" 2>/dev/null || return 1
found=0
done
return $found
}
# "Halo VR" added to every Steam user's shortcuts.vdf (Steam closed), as a VR
# title, with its pictures
add_shortcut()
{
local id config
id="$(shortcut_id)"
for config in "$STEAM_DIR"/userdata/*/config; do
[ -d "$config" ] || continue
case "$config" in */userdata/0/*) continue ;; esac
python3 - "$config/shortcuts.vdf" "$id" "$NAME" "$EXE" "\"$HALO_DIR/\"" <<'PYTHON' || return 1
import os, struct, sys
path, appid, name, exe, start = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4], sys.argv[5]
path, exe = sys.argv[1], sys.argv[2].encode()
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
def read_map(data, at):
@@ -125,7 +157,7 @@ def read_map(data, at):
kind = data[at]; at += 1
if kind == 0x08:
return items, at
end = data.index(b"\0", at); key = data[at:end]; at = end + 1
end = data.index(b"\0", at); key = data[at:end].lower(); at = end + 1
if kind == 0x00:
value, at = read_map(data, at)
elif kind == 0x01:
@@ -134,88 +166,82 @@ def read_map(data, at):
value = struct.unpack_from("<I", data, at)[0]; at += 4
else:
raise ValueError("unknown VDF type %d" % kind)
items.append((kind, key, value))
items.append((key, value))
def write_map(items):
out = b""
for kind, key, value in items:
out += bytes([kind]) + key + b"\0"
if kind == 0x00:
out += write_map(value)
elif kind == 0x01:
out += value + b"\0"
else:
out += struct.pack("<I", value)
return out + b"\x08"
shortcuts = []
if os.path.exists(path):
with open(path, "rb") as file:
data = file.read()
if data:
top, _ = read_map(data, 0)
for kind, key, value in top:
if key.lower() == b"shortcuts" and kind == 0x00:
shortcuts = [entry for entry in value if entry[0] == 0x00]
# (an older entry of the game's replaced)
shortcuts = [entry for entry in shortcuts
if not any(k.lower() == b"exe" and v == exe.encode() for _, k, v in entry[2])]
def text(key, value):
return (0x01, key.encode(), value.encode())
def number(key, value):
return (0x02, key.encode(), value)
entry = [
number("appid", appid), text("AppName", name), text("Exe", exe), text("StartDir", start),
text("icon", ""), text("ShortcutPath", ""), text("LaunchOptions", ""), number("IsHidden", 0),
number("AllowDesktopConfig", 1), number("AllowOverlay", 1), number("OpenVR", 1), number("Devkit", 0),
text("DevkitGameID", ""), number("DevkitOverrideAppID", 0), number("LastPlayTime", 0),
text("FlatpakAppID", ""), text("sortas", ""), (0x00, b"tags", []),
]
shortcuts.append((0x00, b"", entry))
shortcuts = [(0x00, str(index).encode(), value) for index, (_, _, value) in enumerate(shortcuts)]
data = write_map([(0x00, b"shortcuts", shortcuts)])
with open(path + ".tmp", "wb") as file:
file.write(data)
os.replace(path + ".tmp", path)
with open(path, "rb") as file:
top, _ = read_map(file.read(), 0)
for key, shortcuts in top:
if key == b"shortcuts":
for _, entry in shortcuts:
fields = dict(entry)
if fields.get(b"exe") == exe and b"appid" in fields:
print(fields[b"appid"])
sys.exit(0)
sys.exit(1)
PYTHON
mkdir -p "$config/grid"
cp "$BUNDLE/grid/library.png" "$config/grid/${id}.png"
cp "$BUNDLE/grid/libraryp.png" "$config/grid/${id}p.png"
cp "$BUNDLE/grid/library_hero.png" "$config/grid/${id}_hero.png"
cp "$BUNDLE/grid/library_logo.png" "$config/grid/${id}_logo.png"
done
}
# the game added to the library of the Steam user logged in, by Steam (it
# must run), from a desktop file beside the game; then its pictures
add_shortcut()
{
local desktop="$GAME_DIR/$NAME_FILE.desktop" url file id
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
log "asking Steam: $url"
# (Steam's own "Add to Steam" touches this first: steamos-add-to-steam)
touch /tmp/addnonsteamgamefile
steam "$url" >/dev/null 2>&1 &
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
have_shortcut || return 1
while read -r file; do
id="$(shortcut_id "$file" "$EXE")" || continue
log "shortcut $id in $file"
mkdir -p "$(dirname "$file")/grid"
cp "$BUNDLE/grid/library.png" "$(dirname "$file")/grid/${id}.png"
cp "$BUNDLE/grid/libraryp.png" "$(dirname "$file")/grid/${id}p.png"
cp "$BUNDLE/grid/library_hero.png" "$(dirname "$file")/grid/${id}_hero.png"
cp "$BUNDLE/grid/library_logo.png" "$(dirname "$file")/grid/${id}_logo.png"
done < <(shortcut_files)
return 0
}
# --- the game
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
for file in HaloVR.apk halo-vr.sh steam_frame_xiso.py grid/library.png; do
for file in "$APK_NAME" "$LAUNCHER" steam_frame_xiso.py grid/library.png; do
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
done
if [ -f "$HALO_DIR/HaloVR.apk" ]; then
ask "Halo VR is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
else
ask "This installs Halo VR on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
# the builds before the name: their folder becomes the game's (settings,
# saves and game data kept), without their APK and launcher
if [ -d "$OLD_DIR" ] && [ ! -e "$GAME_DIR" ]; then
log "renaming $OLD_DIR to $GAME_DIR"
mv "$OLD_DIR" "$GAME_DIR" || fail "Could not rename $OLD_DIR to $GAME_DIR."
rm -f "$GAME_DIR/HaloVR.apk" "$GAME_DIR/halo-vr.sh"
fi
mkdir -p "$HALO_DIR/maps" || fail "Could not create $HALO_DIR."
if [ -f "$GAME_DIR/$APK_NAME" ] || [ -f "$GAME_DIR/maps/ui.map" ]; then
ask "$NAME is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
else
ask "This installs $NAME on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
fi
mkdir -p "$GAME_DIR/maps" || fail "Could not create $GAME_DIR."
# (the game runs in Lepton's container as a user of its own)
chmod 777 "$HALO_DIR" "$HALO_DIR/maps"
cp "$BUNDLE/halo-vr.sh" "$HALO_DIR/halo-vr.sh.tmp" && chmod 755 "$HALO_DIR/halo-vr.sh.tmp" &&
mv -f "$HALO_DIR/halo-vr.sh.tmp" "$HALO_DIR/halo-vr.sh" || fail "Could not write $HALO_DIR/halo-vr.sh."
chmod 777 "$GAME_DIR" "$GAME_DIR/maps"
cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCHER.tmp" &&
mv -f "$GAME_DIR/$LAUNCHER.tmp" "$GAME_DIR/$LAUNCHER" || fail "Could not write $GAME_DIR/$LAUNCHER."
# (newer than the installed one: Lepton installs it at the next start)
cp "$BUNDLE/HaloVR.apk" "$HALO_DIR/HaloVR.apk.tmp" && chmod 666 "$HALO_DIR/HaloVR.apk.tmp" &&
mv -f "$HALO_DIR/HaloVR.apk.tmp" "$HALO_DIR/HaloVR.apk" && touch "$HALO_DIR/HaloVR.apk" ||
fail "Could not write $HALO_DIR/HaloVR.apk (is the storage full?)."
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
# --- the game data
if [ ! -f "$HALO_DIR/maps/ui.map" ]; then
image="${HALO_VR_IMAGE:-}"
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
image="${RINGFALL_IMAGE:-}"
if [ -z "$image" ]; then
mapfile -t images < <(find_images)
for candidate in "${images[@]}"; do
@@ -231,36 +257,32 @@ if [ ! -f "$HALO_DIR/maps/ui.map" ]; then
fi
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
fi
log "extracting $image"
extract_maps "$image"
chmod -R a+rwX "$HALO_DIR/maps"
chmod -R a+rwX "$GAME_DIR/maps"
image_note="\n\nThe disc image itself is no longer needed:\n$image"
fi
# --- Steam
steam_closed=0
if ! have_shortcut; then
if unattended; then
echo "info: the Steam shortcut is missing (not added unattended)"
else
info "Halo VR is now added to your Steam library. Steam closes for a moment for that."
if pgrep -x steam >/dev/null; then
steam -shutdown >/dev/null 2>&1 &
for _ in $(seq 60); do pgrep -x steam >/dev/null || break; sleep 1; done
pgrep -x steam >/dev/null && fail "Steam did not close. Close it (Steam menu > Exit) and run the installer again."
fi
steam_closed=1
add_shortcut || fail "Could not add Halo VR to the Steam library."
fi
if have_shortcut; then
log "the Steam shortcut is there"
elif unattended; then
echo "info: the Steam shortcut is missing (not added unattended)"
elif ! steam_running; then
info "Steam is not running, so $NAME could not be added to your library.\n\nStart Steam and run the installer again."
else
add_shortcut || fail "Steam did not add $NAME to your library. Run the installer again; if it happens again, add\n$GAME_DIR/$NAME_FILE.desktop\nwith Steam's \"Add a Non-Steam Game\"."
fi
if unattended; then
echo "info: installed in $HALO_DIR"
echo "info: installed in $GAME_DIR"
exit 0
fi
if ask "Halo VR is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start Halo VR from your library there." yes; then
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
fi
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
steamos-session-select gamescope >/dev/null 2>&1 ||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
elif [ "$steam_closed" = 1 ]; then
setsid steam >/dev/null 2>&1 < /dev/null &
fi
+8 -7
View File
@@ -1,13 +1,14 @@
[Desktop Entry]
# Halo VR's installer for the Steam Frame (port/android/STEAM_FRAME.md): a
# release asset, "Install-Halo-VR.desktop", opened in Desktop Mode. It
# downloads the latest release's bundle (tools/steam_frame_bundle.py, which
# fills in the repository) and runs its installer, steam_frame_install.sh.
# Ringfall: Mjolnir VR's installer for the Steam Frame
# (port/android/STEAM_FRAME.md): a release asset,
# "Install-Ringfall-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
# the repository) and runs its installer, steam_frame_install.sh.
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
# escaped)
Type=Application
Name=Install Halo VR
Comment=Installs Halo VR for the Steam Frame, or updates it
Name=Install Ringfall: Mjolnir VR
Comment=Installs Ringfall: Mjolnir VR for the Steam Frame, or updates it
Icon=steam
Terminal=false
Exec=bash -c "if curl -fL --retry 3 -o /tmp/halo-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/halo-vr-steam-frame.zip && rm -rf /tmp/halo-vr-install && python3 -m zipfile -e /tmp/halo-vr-steam-frame.zip /tmp/halo-vr-install; then exec bash /tmp/halo-vr-install/install.sh; else zenity --error --width=460 --title='Halo VR' --text='Halo VR could not be downloaded. Is the Steam Frame online?'; fi"
Exec=bash -c "if curl -fL --retry 3 -o /tmp/ringfall-mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/ringfall-mjolnir-vr-steam-frame.zip && rm -rf /tmp/ringfall-mjolnir-vr-install && python3 -m zipfile -e /tmp/ringfall-mjolnir-vr-steam-frame.zip /tmp/ringfall-mjolnir-vr-install; then exec bash /tmp/ringfall-mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Ringfall: Mjolnir VR' --text='Ringfall: Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
+7 -6
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# Ringfall: Mjolnir VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/RingfallMjolnirVR.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
@@ -14,10 +14,11 @@ export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
DATA="$HOME/Documents/RingfallMjolnirVR"
APK="$DATA/RingfallMjolnirVR.apk"
LOG="$DATA/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
QUIT="$DATA/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
@@ -49,7 +50,7 @@ setsid bash -c '
# the game's log while it runs (Lepton's logcat of its container), for
# looking into problems: logcat.txt, anew each start
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
LOGCAT="$DATA/logcat.txt"
: > "$LOGCAT"
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do
-3
View File
@@ -162,9 +162,6 @@ enum
int halo_vr_body_mode(void);
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
int halo_vr_melee(void);
/* a hand has just tapped the head (vr.flashlight_gesture): the flashlight's
button, for a moment (xinput_sdl.c) */
int halo_vr_flashlight_tap(void);
/* the weight of the melee attack under way, from the swing's speed (0..1;
0 with none): the game scales melee damage by the player's run instead,
and in VR a player punches standing (units.c) */
-7
View File
@@ -251,13 +251,6 @@ static const struct config_setting config_settings[] =
{ "vr.arm_ik", _config_boolean, "true", "HALO_VR_ARM_IK", _environment_value, _platform_android,
"The free hand's arm bends at the elbow to reach it from the shoulder;\n"
"false keeps the arm's shape as on the weapon, pointed at the shoulder." },
{ "vr.flashlight_gesture", _config_boolean, "true", "HALO_VR_FLASHLIGHT_GESTURE", _environment_value,
_platform_android,
"Tapping the side or the top of your head with either hand (to it and\n"
"away again) turns the flashlight on or off; the flashlight's button\n"
"still does. Not while aiming through a scope, holding the weapon with\n"
"both hands, holding or throwing a grenade, just after a melee, or with\n"
"both hands at the head." },
{ "vr.melee_swing_speed", _config_real, "2.0", "HALO_VR_MELEE_SWING", _environment_value, _platform_android,
"Thrusting a hand forward faster than this, in metres per second, is a\n"
"melee attack (the weapon hand's grip is too); 0 turns it off." },
-162
View File
@@ -212,7 +212,6 @@ static struct
captured, and where its disc is in LOCAL space */
int scope_pass;
int scope_captured;
int scope_last_frame; /* the scope was drawn the frame before */
struct halo_xr_pose scope_pose;
int motion_tracker_on_wrist;
float motion_tracker_size;
@@ -257,20 +256,6 @@ static struct
unsigned long long melee_aim_until_us;
int melee_button_down;
float melee_strength; /* 0..1, the last attack's swing */
/* the flashlight by tapping the head (flashlight_gesture_update,
vr.flashlight_gesture): each hand where it was (the head's axes) and
whether it has been away from the head since its last tap; the hand at
the head now and since when (-1: none); when the next toggle may come,
and until when the game sees the flashlight's button */
int flashlight_gesture;
float tap_last[HALO_XR_HAND_COUNT][3];
int tap_last_valid[HALO_XR_HAND_COUNT];
int tap_armed[HALO_XR_HAND_COUNT];
int tap_hand;
unsigned long long tap_entered_us;
unsigned long long tap_last_us;
unsigned long long tap_ready_us;
unsigned long long flashlight_until_us;
unsigned long long melee_jab_start_us; /* the melee button's attack: the weapon jabs */
/* the laser along the barrel, for the frame it was set in */
unsigned long frame_number;
@@ -554,7 +539,6 @@ static void vr_read_live_settings(void)
!strcmp(foot, "turning") ? FOOT_VIGNETTE_TURNING : FOOT_VIGNETTE_OFF;
}
vr.foot_vignette_strength = (float)config_real("vr.foot_vignette_strength");
vr.flashlight_gesture = config_boolean("vr.flashlight_gesture");
vr.fingers = config_boolean("vr.fingers");
vr.arm_ik = config_boolean("vr.arm_ik");
{
@@ -602,7 +586,6 @@ void halo_vr_initialize(void)
if (vr.active || !getenv("HALO_VR"))
return;
vr_read_live_settings();
vr.tap_hand = -1;
vr.cinematics_3d = config_boolean("vr.cinematics_in_3d");
vr.hud_distance = (float)config_real("vr.hud_distance");
vr.hud_width = 2.0f * vr.hud_distance * tanf((float)config_real("vr.hud_field_of_view") * PI / 360.0f);
@@ -1125,148 +1108,6 @@ static void melee_update(void)
}
}
/* The flashlight by tapping the head (vr.flashlight_gesture; after
HaloCE-Quest-VR, CC0, whose off hand brought near the head's middle
toggles it): either hand brought to the side or the top of the head and
away again, as a tap, toggles the flashlight; its button still does.
The zone is in the head's axes (metres from between the eyes; x right,
y up, z back): beside the head, from 7 to 24 cm out, 10 cm below the eyes
to 18 cm above them; or over it, within 12 cm of its middle line, 8 to
30 cm up; either way no further forward than 6 cm before the eyes, so a
scope held to the eye, a two-handed weapon and a punch, all in front of
the face, are not in it. A tap is a hand coming from 35 cm or more from
the head's middle into the zone, slower than 3 m/s (a grenade's throw
past the ear is faster), staying 40 to 600 ms (adjusting the headset
takes longer) alone (both hands at the head adjust it), and leaving it 4
cm past its edges; toggles are a second apart. No taps while the menus
are up, while a grenade is held or just thrown, after a melee, with both
hands on the weapon, with the weapon hand at the scope, or with a hand on
a Warthog's wheel. */
#define TAP_SPEED 3.0f
#define TAP_AWAY 0.35f
#define TAP_MARGIN 0.04f
#define TAP_DWELL_SHORTEST 40000ULL
#define TAP_DWELL_LONGEST 600000ULL
#define TAP_COOLDOWN 1000000ULL
static int tap_zone(const float local[3], float margin)
{
float x = fabsf(local[0]), y = local[1], z = local[2];
if (z < -0.06f - margin || z > 0.18f + margin)
return 0;
if (x >= 0.07f - margin && x <= 0.24f + margin && y >= -0.10f - margin && y <= 0.18f + margin)
return 1;
return x <= 0.12f + margin && y >= 0.08f - margin && y <= 0.30f + margin;
}
static void flashlight_gesture_update(void)
{
unsigned long long now = now_us();
float seconds = vr.tap_last_us ? (float)(now - vr.tap_last_us) * 1e-6f : 0.0f;
float inverse[4];
int blocked, hand;
vr.tap_last_us = now;
inverse[0] = -vr.frame.head.orientation[0];
inverse[1] = -vr.frame.head.orientation[1];
inverse[2] = -vr.frame.head.orientation[2];
inverse[3] = vr.frame.head.orientation[3];
blocked = !vr.flashlight_gesture || !vr.frame.focused || vr.last_overlay_mode == HALO_XR_OVERLAY_SCREEN ||
vr.recentre_frames > 0 || vr.grenade_held || now - vr.grenade_released_us < 600000 ||
now < vr.melee_ready_us || vr.two_hand_latched;
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
{
const struct halo_xr_hand *state = &vr.frame.hands[hand];
float relative[3], local[3], away[3], speed = 0.0f;
int axis, inside, outside;
if (!state->active || !(state->flags & HALO_XR_POSE_VALID))
{
vr.tap_last_valid[hand] = 0;
vr.tap_armed[hand] = 0;
if (vr.tap_hand == hand)
vr.tap_hand = -1;
continue;
}
for (axis = 0; axis < 3; axis++)
relative[axis] = state->grip.position[axis] - vr.frame.head.position[axis];
quaternion_rotate(inverse, relative, local);
if (vr.tap_last_valid[hand] && seconds > 0.0f && seconds < 0.1f)
{
float moved[3];
for (axis = 0; axis < 3; axis++)
moved[axis] = local[axis] - vr.tap_last[hand][axis];
speed = sqrtf(moved[0] * moved[0] + moved[1] * moved[1] + moved[2] * moved[2]) / seconds;
}
memcpy(vr.tap_last[hand], local, sizeof(vr.tap_last[hand]));
vr.tap_last_valid[hand] = 1;
away[0] = local[0];
away[1] = local[1];
away[2] = local[2] - 0.10f;
if (sqrtf(away[0] * away[0] + away[1] * away[1] + away[2] * away[2]) > TAP_AWAY)
vr.tap_armed[hand] = 1;
inside = tap_zone(local, 0.0f);
outside = !tap_zone(local, TAP_MARGIN);
if (blocked || (vr.wheel_hands & (1 << hand)) || (hand == weapon_hand() && vr.scope_last_frame))
{
/* (a hand that may be at the head for another reason taps
only once it has been away again) */
if (inside)
vr.tap_armed[hand] = 0;
if (vr.tap_hand == hand)
vr.tap_hand = -1;
continue;
}
if (vr.tap_hand == hand)
{
unsigned long long dwell = now - vr.tap_entered_us;
if (dwell > TAP_DWELL_LONGEST)
{
vr.tap_hand = -1;
}
else if (outside)
{
vr.tap_hand = -1;
if (dwell >= TAP_DWELL_SHORTEST && now >= vr.tap_ready_us)
{
vr.flashlight_until_us = now + 100000;
vr.tap_ready_us = now + TAP_COOLDOWN;
host_xr_haptic(hand, 0.5f, 0.05f);
platform_log("VR: flashlight, %s hand at the head for %llu ms", hand ? "right" : "left",
dwell / 1000);
}
}
}
else if (inside && vr.tap_armed[hand])
{
vr.tap_armed[hand] = 0;
if (vr.tap_hand >= 0)
{
/* (both hands at the head: the headset adjusted) */
vr.tap_hand = -1;
}
else if (speed <= TAP_SPEED)
{
vr.tap_hand = hand;
vr.tap_entered_us = now;
}
}
else if (inside && vr.tap_hand >= 0)
{
vr.tap_hand = -1;
}
}
}
int halo_vr_flashlight_tap(void)
{
return vr.active && now_us() < vr.flashlight_until_us;
}
/* The fingers, from where they rest on the controller (after
HaloCE-Quest-VR, CC0, its vr_frame.c): the thumb curled while it touches
the stick, a button or the rest, half curled where the controller does not
@@ -1845,7 +1686,6 @@ void halo_vr_frame_begin(void)
vr.frame_begun = 1;
vr.frame_number++;
vr.eyes_captured = 0;
vr.scope_last_frame = vr.scope_captured;
vr.scope_captured = 0;
vr.crosshair_captured = 0;
vr.name_count = 0;
@@ -1872,7 +1712,6 @@ void halo_vr_frame_begin(void)
look_update();
grenade_update();
melee_update();
flashlight_gesture_update();
fingers_update();
torso_update();
}
@@ -3440,7 +3279,6 @@ void halo_vr_debug_look(short local_player_index, const float unit_looking[3], i
(void)local_player_index; (void)unit_looking; (void)in_vehicle;
}
int halo_vr_melee(void) { return 0; }
int halo_vr_flashlight_tap(void) { return 0; }
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
{
(void)local_player_index; (void)hand; (void)curls;
+2 -7
View File
@@ -840,9 +840,7 @@ In play, laid out for VR shooters rather than by position:
right trigger fire left trigger grenade: held in the hand, thrown as it lets go (vr.c)
right grip melee (B) left grip hold the weapon with both hands (vr.c)
(a hand thrust forward, vr.melee_swing_speed, is a melee attack too;
a hand tapped on the side or top of the head, vr.flashlight_gesture,
the flashlight)
(a hand thrust forward, vr.melee_swing_speed, is a melee attack too)
A jump X action, reload (X), as the game's prompts say
B flashlight (white); held, switch grenade (black)
Y switch weapon (Y)
@@ -873,7 +871,6 @@ In the menus the buttons are the Xbox's by position: A accepts, B goes back. */
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus);
int halo_vr_wheel_held(void);
int halo_vr_flashlight_tap(void);
/* a click that stands for the pad's A (ui_widget.c: the screen where
controllers join by pressing it), held a moment */
@@ -951,9 +948,7 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
/* (the Steam Frame's bumpers: the left switches grenades, the right is
the flashlight) */
merge_button(pad, XINPUT_GAMEPAD_BLACK, (left->buttons & HALO_XR_BUTTON_BUMPER) != 0);
/* (and a tap of the head, vr.flashlight_gesture: vr.c) */
merge_button(pad, XINPUT_GAMEPAD_WHITE,
(right->buttons & HALO_XR_BUTTON_BUMPER) != 0 || halo_vr_flashlight_tap());
merge_button(pad, XINPUT_GAMEPAD_WHITE, (right->buttons & HALO_XR_BUTTON_BUMPER) != 0);
{
/* B: a tap is the flashlight, holding it switches grenades; the
game sees a short press of either */
+6 -17
View File
@@ -6669,7 +6669,6 @@ enum
/* (the game's actions on the buttons, HALO_VR_ACTION_*'s order) */
_vr_option_button_first,
_vr_option_button_last = _vr_option_button_first + HALO_VR_ACTION_COUNT - 1,
_vr_option_flashlight_gesture,
_vr_option_buttons_reset,
_vr_option_graphics,
_vr_option_refresh_rate,
@@ -6724,8 +6723,8 @@ static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAG
{ _vr_option_button_first + HALO_VR_ACTION_JUMP, _vr_option_button_first + HALO_VR_ACTION_LIGHT,
_vr_option_button_first + HALO_VR_ACTION_ACTION, _vr_option_button_first + HALO_VR_ACTION_WEAPON,
_vr_option_button_first + HALO_VR_ACTION_CROUCH, _vr_option_button_first + HALO_VR_ACTION_ZOOM,
_vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_flashlight_gesture, _vr_option_buttons_reset,
_vr_option_controls, NONE, NONE },
_vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_buttons_reset, _vr_option_controls,
NONE, NONE, NONE },
{ _vr_option_graphics, _vr_option_refresh_rate, _vr_option_resolution, _vr_option_quit,
NONE, NONE, NONE, NONE, NONE, NONE, NONE, NONE },
};
@@ -7102,9 +7101,6 @@ static void ui_vr_options_change(
case _vr_option_resolution:
ui_vr_options_step("vr.resolution_scale", vr_resolution_scales, NUMBEROF(vr_resolution_scales), change);
break;
case _vr_option_flashlight_gesture:
config_write_boolean("vr.flashlight_gesture", !config_boolean("vr.flashlight_gesture"));
break;
case _vr_option_buttons_reset:
ui_vr_options_buttons_reset();
break;
@@ -7553,8 +7549,6 @@ static void ui_vr_controls_render(
ui_vr_controls_add(&columns[1], "Menu", "Pause");
ui_vr_controls_note(320, wrist_note);
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
if (config_boolean("vr.flashlight_gesture"))
ui_vr_controls_note(360, "Tap the side or top of your head: the flashlight.");
break;
case _vr_controls_vehicles:
ui_vr_controls_add(&columns[move], "Stick", "Drive");
@@ -7602,8 +7596,6 @@ static void ui_vr_controls_render(
ui_vr_controls_note(320, wrist_note);
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
ui_vr_controls_note(360, "Hold the Meta button to recentre the view.");
if (config_boolean("vr.flashlight_gesture"))
ui_vr_controls_note(380, "Tap the side or top of your head: the flashlight.");
break;
case _vr_controls_vehicles:
ui_vr_controls_add(&columns[move], "Stick", "Drive");
@@ -7809,10 +7801,6 @@ static void ui_vr_options_render(
snprintf(value, sizeof(value), "%.0f%%", scale * 100.0f);
break;
}
case _vr_option_flashlight_gesture:
label = "Light by tapping the head";
snprintf(value, sizeof(value), "%s", config_boolean("vr.flashlight_gesture") ? "On" : "Off");
break;
case _vr_option_buttons_reset:
label = "Reset buttons to defaults";
unused = ui_vr_options_buttons_default();
@@ -7832,7 +7820,7 @@ static void ui_vr_options_render(
break;
case _vr_option_quit:
label = ui_vr_options_globals.quit_armed && ui_vr_options_globals.selection == _vr_option_quit ?
"Quit Halo? Once more to quit" : "Quit Halo";
"Quit the game? Once more to quit" : "Quit the game";
break;
}
if (option == ui_vr_options_globals.selection)
@@ -7859,10 +7847,11 @@ static void ui_vr_options_render(
if (page == _vr_options_page_buttons)
{
/* (what the buttons' rows do) */
ui_join_code_draw_text(128, 512, 320, "An action moved to a button swaps with the one there;", FALSE, 0,
ui_join_code_draw_text(128, 512, 302, "An action moved to a button swaps with the one there;", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "left-handed and mirrored, the buttons are mirrored too.", FALSE, 0,
ui_join_code_draw_text(128, 512, 320, "left-handed and mirrored, the buttons are mirrored too.", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "The menus keep their buttons.", FALSE, 0, &dim_color);
}
if (page == _vr_options_page_more)
{
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Ringfall: Mjolnir VR's pictures, drawn from nothing but shapes and the
port's Overpass font (port/assets/fonts, SIL Open Font License):
python3 tools/ringfall_art.py
writes
- port/android/art/android-icon.png, the launcher icon's artwork (then run
tools/android_icon.py for the app's icon);
- port/android/steam_frame_grid/*.png, the Steam library's pictures of the
Steam Frame's shortcut (port/android/STEAM_FRAME.md): library.png
(920x430), libraryp.png (600x900), library_hero.png (1920x620) and
library_logo.png (800x300, transparent).
The emblem: a tilted ring, broken where a streak falls through it.
"""
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parent.parent
FONT = ROOT / "port/assets/fonts/Overpass-900.ttf"
ICON = ROOT / "port/android/art/android-icon.png"
GRID = ROOT / "port/android/steam_frame_grid"
# the launcher icon's background (tools/android_icon.py keeps it)
ICON_BACKGROUND = (30, 83, 138)
LIGHT = (240, 246, 252)
ACCENT = (255, 176, 64)
TOP = (10, 34, 58)
BOTTOM = (6, 12, 24)
SUPERSAMPLE = 4
def gradient(size, top, bottom):
width, height = size
image = Image.new("RGBA", size)
draw = ImageDraw.Draw(image)
for y in range(height):
t = y / max(1, height - 1)
draw.line([(0, y), (width, y)], fill=tuple(int(a + (b - a) * t) for a, b in zip(top, bottom)) + (255,))
return image
def emblem(size, ring=LIGHT, streak=ACCENT):
"""the emblem on a transparent square of the size given"""
scale = size * SUPERSAMPLE
layer = Image.new("RGBA", (scale, scale), (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
center = scale / 2
radius_x, radius_y = scale * 0.40, scale * 0.17
thickness = scale * 0.055
tilt = math.radians(-18)
# the ring as a band of quads along a tilted ellipse, with a gap at the
# top right where the streak passes
steps = 360
gap = (math.radians(35), math.radians(75))
def point(angle, grow):
x = (radius_x + grow) * math.cos(angle)
y = (radius_y + grow * radius_y / radius_x) * math.sin(angle)
return (center + x * math.cos(tilt) - y * math.sin(tilt), center + x * math.sin(tilt) + y * math.cos(tilt))
for step in range(steps):
a0 = 2 * math.pi * step / steps
a1 = 2 * math.pi * (step + 1.2) / steps
# (angles measured clockwise from the right, screen y down: the gap
# at the upper right is at negative angles)
if gap[0] <= (2 * math.pi - a0) % (2 * math.pi) <= gap[1]:
continue
draw.polygon([point(a0, -thickness / 2), point(a1, -thickness / 2), point(a1, thickness / 2),
point(a0, thickness / 2)], fill=ring)
# the streak: a tapering bar falling from the upper right through the gap
start = (center + scale * 0.40, center - scale * 0.42)
end = (center + scale * 0.02, center + scale * 0.12)
dx, dy = end[0] - start[0], end[1] - start[1]
length = math.hypot(dx, dy)
nx, ny = -dy / length, dx / length
head = scale * 0.050
draw.polygon([start, (end[0] + nx * head, end[1] + ny * head), (end[0] - nx * head, end[1] - ny * head)],
fill=streak)
draw.ellipse([end[0] - head, end[1] - head, end[0] + head, end[1] + head], fill=streak)
return layer.resize((size, size), Image.LANCZOS)
def italic_text(text, height, fill):
"""the text in Overpass Black, slanted, cropped to itself"""
font = ImageFont.truetype(str(FONT), int(height * SUPERSAMPLE))
left, top, right, bottom = font.getbbox(text)
width = right - left + int(height * SUPERSAMPLE * 0.4)
layer = Image.new("RGBA", (width, bottom - top + 8), (0, 0, 0, 0))
ImageDraw.Draw(layer).text((-left + int(height * SUPERSAMPLE * 0.2), -top), text, font=font, fill=fill)
slant = 0.18
layer = layer.transform((layer.width + int(layer.height * slant), layer.height), Image.AFFINE,
(1, slant, -layer.height * slant, 0, 1, 0), Image.BICUBIC)
layer = layer.crop(layer.getbbox())
return layer.resize((max(1, layer.width // SUPERSAMPLE), max(1, layer.height // SUPERSAMPLE)), Image.LANCZOS)
def logo(width):
""""RINGFALL" over "MJOLNIR VR", as wide as asked, transparent"""
title = italic_text("RINGFALL", 200, LIGHT)
title = title.resize((width, int(title.height * width / title.width)), Image.LANCZOS)
subtitle = italic_text("MJOLNIR VR", 200, ACCENT)
sub_width = int(width * 0.62)
subtitle = subtitle.resize((sub_width, int(subtitle.height * sub_width / subtitle.width)), Image.LANCZOS)
gap = int(title.height * 0.16)
image = Image.new("RGBA", (width, title.height + gap + subtitle.height), (0, 0, 0, 0))
image.alpha_composite(title, (0, 0))
image.alpha_composite(subtitle, (width - subtitle.width, title.height + gap))
return image
def shadowed(base, layer, position, blur=12):
shadow = Image.new("RGBA", base.size, (0, 0, 0, 0))
mask = Image.new("RGBA", layer.size, (0, 0, 0, 170))
mask.putalpha(layer.getchannel("A").point(lambda a: a * 2 // 3))
shadow.alpha_composite(mask, position)
base.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(blur)))
base.alpha_composite(layer, position)
def faded(layer, opacity):
layer = layer.copy()
layer.putalpha(layer.getchannel("A").point(lambda a: int(a * opacity)))
return layer
def main():
# the launcher icon: the light emblem on its solid background
icon = Image.new("RGBA", (512, 512), ICON_BACKGROUND + (255,))
icon.alpha_composite(emblem(400, LIGHT, LIGHT), (56, 56))
ICON.parent.mkdir(parents=True, exist_ok=True)
icon.convert("RGB").save(ICON, optimize=True)
print(ICON.relative_to(ROOT))
GRID.mkdir(parents=True, exist_ok=True)
# the wide capsule
image = gradient((920, 430), TOP, BOTTOM)
image.alpha_composite(faded(emblem(520), 0.35), (500, -40))
shadowed(image, logo(600), (70, 120))
image.save(GRID / "library.png", optimize=True)
# the tall capsule
image = gradient((600, 900), TOP, BOTTOM)
image.alpha_composite(emblem(420), (90, 130))
shadowed(image, logo(480), (60, 560))
image.save(GRID / "libraryp.png", optimize=True)
# the hero, under the logo: the emblem large and faint at the right
image = gradient((1920, 620), TOP, BOTTOM)
image.alpha_composite(faded(emblem(900), 0.45), (1080, -140))
image.save(GRID / "library_hero.png", optimize=True)
# the logo, over the hero
image = Image.new("RGBA", (800, 300), (0, 0, 0, 0))
mark = logo(560)
image.alpha_composite(mark, ((800 - mark.width) // 2, (300 - mark.height) // 2))
image.save(GRID / "library_logo.png", optimize=True)
for name in ("library.png", "libraryp.png", "library_hero.png", "library_logo.png"):
print((GRID / name).relative_to(ROOT))
if __name__ == "__main__":
main()
+8 -8
View File
@@ -5,11 +5,11 @@ The Steam Frame's installer as release assets (port/android/STEAM_FRAME.md):
python3 tools/steam_frame_bundle.py <apk> <output folder> <owner/repository>
writes into the output folder
- halo-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
as install.sh), the APK (HaloVR.apk), the launcher (steam_frame_launch.sh,
as halo-vr.sh), the disc image reader (steam_frame_xiso.py, with
- ringfall-mjolnir-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
as install.sh), the APK (RingfallMjolnirVR.apk), the launcher
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh), the disc image reader (steam_frame_xiso.py, with
extract-xiso's license) and the library pictures (grid/);
- Install-Halo-VR.desktop (port/android/steam_frame_installer.desktop): what
- Install-Ringfall-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what
the player downloads and opens in Desktop Mode; it fetches the latest
release's zip from the repository named.
"""
@@ -31,14 +31,14 @@ def main():
files = [
(ANDROID / "steam_frame_install.sh", "install.sh", 0o755),
(ANDROID / "steam_frame_launch.sh", "halo-vr.sh", 0o755),
(ANDROID / "steam_frame_launch.sh", "ringfall-mjolnir-vr.sh", 0o755),
(ANDROID / "steam_frame_xiso.py", "steam_frame_xiso.py", 0o755),
(ROOT / "port/third_party/extract-xiso/LICENSE.TXT", "extract-xiso-LICENSE.txt", 0o644),
(apk, "HaloVR.apk", 0o644),
(apk, "RingfallMjolnirVR.apk", 0o644),
]
files += [(picture, "grid/" + picture.name, 0o644)
for picture in sorted((ANDROID / "steam_frame_grid").glob("*.png"))]
bundle = output / "halo-vr-steam-frame.zip"
bundle = output / "ringfall-mjolnir-vr-steam-frame.zip"
with zipfile.ZipFile(bundle, "w", zipfile.ZIP_DEFLATED) as archive:
for path, name, mode in files:
info = zipfile.ZipInfo.from_file(path, name)
@@ -48,7 +48,7 @@ def main():
print(f"{bundle.relative_to(Path.cwd()) if bundle.is_relative_to(Path.cwd()) else bundle}")
desktop = (ANDROID / "steam_frame_installer.desktop").read_text().replace("@REPOSITORY@", repository)
installer = output / "Install-Halo-VR.desktop"
installer = output / "Install-Ringfall-Mjolnir-VR.desktop"
installer.write_text(desktop)
installer.chmod(0o755)
print(installer)