Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f68db3b03e | ||
|
|
bd08baacac | ||
|
|
f39126e019 | ||
|
|
f649bd1a3e | ||
|
|
719563a48c | ||
|
|
32cc1cb206 | ||
|
|
adf220d344 | ||
|
|
32ad9ed6b0 | ||
|
|
cf19bcfdfa | ||
|
|
3b5796f1a0 | ||
|
|
68135a712d |
No files matched your search
@@ -252,9 +252,12 @@ 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
|
||||
@@ -328,7 +331,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
|
||||
|
||||
@@ -1,140 +1,93 @@
|
||||
# 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>
|
||||
|
||||
[](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`).
|
||||
Ringfall: Mjolnir VR plays **Halo: Combat Evolved** for the original Xbox in
|
||||
virtual reality, on the **Meta Quest 3 / 3S** and the **Steam Frame**. You
|
||||
play your own copy: the game data comes from your own disc image of the
|
||||
game, and this project includes none of it.
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
> **Unofficial fan project.** Ringfall: Mjolnir VR is not affiliated with,
|
||||
> endorsed or sponsored by Microsoft, Xbox Game Studios, Bungie or Halo
|
||||
> Studios. Halo is a trademark of Microsoft Corporation; it is named here
|
||||
> only to say which game the project works with. No game data, art or
|
||||
> music of the game is distributed.
|
||||
|
||||
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).
|
||||
## What it does
|
||||
|
||||
## Download
|
||||
- **Stereo 6DoF VR** at the headset's refresh rate (72 to 120 Hz), the HUD,
|
||||
menus, scope and crosshair as layers of their own.
|
||||
- **Your hands and arms**: the weapon in the hand, its shots from the
|
||||
muzzle, arms by inverse kinematics, fingers from the controllers;
|
||||
two-handed weapons, melee by swinging, grenades thrown with the hand,
|
||||
physical crouch and lean.
|
||||
- **Vehicles**: the Warthog's wheel turned with your hands, turrets with
|
||||
handles, first-person seats.
|
||||
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
|
||||
left-handed mode with mirrored controls, buttons remapped freely,
|
||||
graphics presets, refresh rate and resolution in the VR options.
|
||||
- **Multiplayer**: the PC version's menus and server browser, join codes,
|
||||
password lobbies, co-op campaign, bots that make room for people, up to
|
||||
128 players; cross-play with the desktop and phone builds of
|
||||
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
## Install
|
||||
|
||||
| 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) |
|
||||
You need your own Xbox disc image of Halo: Combat Evolved (`.iso` or
|
||||
`.xiso`, any version).
|
||||
|
||||
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.
|
||||
### Meta Quest 3 / 3S
|
||||
|
||||
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).
|
||||
1. Download `ringfall-mjolnir-vr-android-release.zip` from the
|
||||
[latest release](../../releases/latest) and unpack it.
|
||||
2. Install the APK with [SideQuest](https://sidequestvr.com/) or
|
||||
`adb install`.
|
||||
3. Copy your disc image to the Quest's `Download` folder (USB cable, then
|
||||
the Quest's storage on your computer).
|
||||
4. Start **Ringfall: Mjolnir VR** (Library > Unknown Sources), select
|
||||
**Choose disc image** and pick it. The game copies its maps out of the
|
||||
image (about 1.8 GB) and starts. You can delete the image afterwards.
|
||||
|
||||
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.
|
||||
The game looks for new releases when it starts and offers to update itself.
|
||||
|
||||
## Game data
|
||||
### Steam Frame
|
||||
|
||||
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.
|
||||
1. In Desktop Mode, put your disc image in `Downloads`.
|
||||
2. Download `Install-Ringfall-Mjolnir-VR.desktop` from the
|
||||
[latest release](../../releases/latest) and open it.
|
||||
3. Answer the dialogs: the installer finds the disc image, copies the game
|
||||
data out of it, adds **Ringfall: Mjolnir VR** to your Steam library and
|
||||
switches back to Gaming Mode.
|
||||
|
||||
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.
|
||||
Open the same file again to update. Details:
|
||||
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
|
||||
|
||||
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).
|
||||
## Documentation
|
||||
|
||||
## Platforms
|
||||
|
||||
Each platform has its own instructions:
|
||||
|
||||
| Platform | Instructions |
|
||||
| Topic | Where |
|
||||
| --- | --- |
|
||||
| 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 Android/VR app: build, controls, settings | [port/android/README.md](port/android/README.md) |
|
||||
| The Steam Frame | [port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md) |
|
||||
| What is done and what comes next | [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md) |
|
||||
| The port it is built on (desktop builds, netcode, build options) | [docs/OPENCE_README.md](docs/OPENCE_README.md) |
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
To build it: `python3 configure.py --release`, then `ninja android_apk`
|
||||
(Android SDK and NDK needed; see the Android README).
|
||||
|
||||
## Multiplayer
|
||||
## Credits
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
- [OpenCE](https://github.com/OpenCommunityEdition/OpenCE), the port of the
|
||||
decompilation to Linux, Windows and Android that this project follows.
|
||||
- The decompilation: [bnunu/halo-1](https://github.com/bnunu/halo-1), a fork
|
||||
of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
- Ideas for the arms, hands and left-handed mode from
|
||||
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and
|
||||
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (CC0).
|
||||
- The disc image readers follow [extract-xiso](https://github.com/XboxDev/extract-xiso)
|
||||
(its license: [port/third_party/extract-xiso](port/third_party/extract-xiso/LICENSE.TXT)).
|
||||
- Fonts: Overpass (SIL Open Font License).
|
||||
|
||||
- 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/`.
|
||||
The project's own code is released under [CC0](LICENSE.md), as the
|
||||
decompilation it builds on.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
|
||||
[](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. 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.
|
||||
|
||||
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/`.
|
||||
@@ -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`.
|
||||
@@ -42,7 +44,7 @@ ninja) and these items:
|
||||
|
||||
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`.
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. Left: 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 **built** (branch `feature/vr-comfort`; Quest 3 good 2026-10-06): 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 **built** (branches `feature/release-install`, `feature/rename-ringfall`): the app's import (Quest; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`), a release signing key; test pending |
|
||||
| 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 | **built** (branch `feature/rename-ringfall`); test pending |
|
||||
|
||||
## Step 2, first part (done)
|
||||
|
||||
@@ -106,7 +107,7 @@ the GPU's fill.
|
||||
|
||||
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
|
||||
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
|
||||
the controller buttons page, Quit). The refresh rate steps through what
|
||||
the controller buttons page, Quit); four since the second part (below). The refresh rate steps through what
|
||||
the runtime offers and changes at once; on the Steam Frame the row shows
|
||||
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
|
||||
restart. The controller buttons page shows the layout as set.
|
||||
@@ -136,6 +137,82 @@ 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)
|
||||
|
||||
On branch `feature/vr-comfort`; tried on the Quest 3 2026-10-06 (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,
|
||||
"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
|
||||
weapon, crouch, zoom and melee each go on one of seven buttons: A, B,
|
||||
X, Y, the move stick's click, the turning stick's, the weapon hand's
|
||||
grip. Moving an action to a button swaps it with the action there, so
|
||||
none is ever left without one.
|
||||
- The triggers, the free hand's grip, the menu button, the Steam Frame's
|
||||
bumpers, D-pad and View button keep theirs; the menus keep the buttons
|
||||
as they are.
|
||||
- Applied after the left-handed mirroring, on the gamepad's sides: the
|
||||
same on the Touch and the Frame controllers, and a left-handed player's
|
||||
buttons are the mirror image of the right-handed ones (on the Frame,
|
||||
mirrored, the left D-pad's). The defaults are the old layout, the
|
||||
host's bindings and the ABI unchanged.
|
||||
- The melee's aim and jab follow its button; the grips on a Warthog's
|
||||
wheel press nothing, whatever the grip does.
|
||||
- The controller buttons page shows each action where its button is.
|
||||
|
||||
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)
|
||||
while the stick turns smoothly, or also while the move stick moves, as
|
||||
far as the sticks are pushed, with its own strength. The old values are
|
||||
among the steps; defaults unchanged.
|
||||
|
||||
Transparent geometry: room for 2048 groups a window (the Xbox's 384,
|
||||
below).
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.button_jump` | `"a"` (default), `"b"`, `"x"`, `"y"`, `"move_stick"`, `"turn_stick"`, `"grip"`: as right-handed, mirrored with `vr.mirror_controls` |
|
||||
| `vr.button_light` | the flashlight, held switch grenades; `"b"` (default) |
|
||||
| `vr.button_action` | action, reload; `"x"` (default) |
|
||||
| `vr.button_weapon` | switch weapon; `"y"` (default) |
|
||||
| `vr.button_crouch` | `"move_stick"` (default) |
|
||||
| `vr.button_zoom` | `"turn_stick"` (default) |
|
||||
| `vr.button_melee` | `"grip"` (default) |
|
||||
| `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) |
|
||||
|
||||
Checked in the emulator (`debug.vr_options`): the four pages without
|
||||
overlaps, each row's values, swaps and reset, the controller buttons page
|
||||
right- and left-handed after swaps, the settings kept over a restart, a
|
||||
15-bot game as before.
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Defaults: every button as before (jump, light/held grenades, action,
|
||||
switch weapon, crouch, zoom, melee by grip and by swing), right- and
|
||||
left-handed, mirrored and not; the menus' A and B.
|
||||
2. BUTTONS: swap jump and melee (jump on the grip, melee on A): both work,
|
||||
the melee jabs and aims where the head looks; light on a stick click
|
||||
(tap: light, hold: grenades); the controller buttons page says so.
|
||||
At the Warthog's wheel with jump on the grip: no jump while holding
|
||||
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. 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.
|
||||
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.
|
||||
|
||||
## Step 3a (done)
|
||||
|
||||
Tried on the Quest 3 and the Steam Frame, in `main`. The arm IK,
|
||||
@@ -194,10 +271,11 @@ the left hand animated through reloads.
|
||||
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
|
||||
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
|
||||
not merged.
|
||||
- Transparent geometry groups: the Xbox's 384 a window overflow in games
|
||||
of 30 and more players.
|
||||
- The VR on-screen keyboard shows a password in the clear while it is
|
||||
typed.
|
||||
- Transparent geometry groups: the Xbox's 384 a window overflowed in
|
||||
games of 30 and more players (the last submitted, glass, water,
|
||||
particles, contrails, vanished). Raised to 2048 (320 KB) on
|
||||
`feature/vr-comfort`, to check in a large game (step 2, second part,
|
||||
test 6); upstream still has 384.
|
||||
|
||||
## Headset tests (2026-10-06, all three in `main`)
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 6.0 KiB |
|
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>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 21 KiB |
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 71 KiB |
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -252,6 +252,39 @@ int halo_vr_controller_layout(void);
|
||||
/* the hand that holds the weapon (HALO_XR_HAND_*: 1 the right, 0 the left,
|
||||
vr.handedness) */
|
||||
int halo_vr_weapon_hand(void);
|
||||
/* The game's actions on the controllers' buttons (vr.button_*; the VR
|
||||
options' BUTTONS page): each action on one of seven buttons, named as
|
||||
right-handed (A and B on the right controller, X and Y, the Quest's, on
|
||||
the left; the move stick's click, the turning stick's, the weapon hand's
|
||||
grip); left-handed with vr.mirror_controls, their mirror images. The
|
||||
actions are numbered by the button each has by default. The triggers, the
|
||||
free hand's grip, the menu button and the Steam Frame's bumpers, D-pad and
|
||||
View button keep theirs; the menus keep the buttons as they are. */
|
||||
enum
|
||||
{
|
||||
HALO_VR_INPUT_A = 0,
|
||||
HALO_VR_INPUT_B,
|
||||
HALO_VR_INPUT_X,
|
||||
HALO_VR_INPUT_Y,
|
||||
HALO_VR_INPUT_MOVE_STICK,
|
||||
HALO_VR_INPUT_TURN_STICK,
|
||||
HALO_VR_INPUT_GRIP,
|
||||
HALO_VR_INPUT_COUNT,
|
||||
|
||||
HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A,
|
||||
HALO_VR_ACTION_LIGHT = HALO_VR_INPUT_B, /* a tap the flashlight; held, switch grenades */
|
||||
HALO_VR_ACTION_ACTION = HALO_VR_INPUT_X, /* action, reload */
|
||||
HALO_VR_ACTION_WEAPON = HALO_VR_INPUT_Y, /* switch weapon */
|
||||
HALO_VR_ACTION_CROUCH = HALO_VR_INPUT_MOVE_STICK,
|
||||
HALO_VR_ACTION_ZOOM = HALO_VR_INPUT_TURN_STICK,
|
||||
HALO_VR_ACTION_MELEE = HALO_VR_INPUT_GRIP,
|
||||
HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT,
|
||||
};
|
||||
/* an action's setting ("vr.button_jump"), a button's value in it ("a"),
|
||||
and the button an action is on, as set */
|
||||
const char *halo_vr_action_setting(int action);
|
||||
const char *halo_vr_input_value(int input);
|
||||
int halo_vr_action_input(int action);
|
||||
/* the graphics preset (vr.graphics; HALO_VR_GRAPHICS_HIGH outside VR): which
|
||||
effects are drawn (port/linux/game/vr_graphics.c), and with "max" the
|
||||
models lit for each pixel and larger shadows (d3d8_gl.c) */
|
||||
|
||||
@@ -201,6 +201,28 @@ static const struct config_setting config_settings[] =
|
||||
"and the left one turns, X jumps and A reloads (on the Steam Frame the\n"
|
||||
"D-pad takes A, B, X and Y as their mirror image); false keeps them as\n"
|
||||
"right-handed. The menus keep their buttons either way." },
|
||||
{ "vr.button_jump", _config_string, "\"a\"", "HALO_VR_BUTTON_JUMP", _environment_value, _platform_android,
|
||||
"The button each of the game's actions is on (the VR options' BUTTONS\n"
|
||||
"page swaps them): \"a\", \"b\", \"x\", \"y\" (A and B on the right\n"
|
||||
"controller, X and Y the Quest's on the left; the Steam Frame has all\n"
|
||||
"four on the right), \"move_stick\" and \"turn_stick\" (their clicks),\n"
|
||||
"\"grip\" (the weapon hand's), named as right-handed: left-handed with\n"
|
||||
"mirror_controls, their mirror images. Jump," },
|
||||
{ "vr.button_light", _config_string, "\"b\"", "HALO_VR_BUTTON_LIGHT", _environment_value, _platform_android,
|
||||
"the flashlight (held: switch grenades)," },
|
||||
{ "vr.button_action", _config_string, "\"x\"", "HALO_VR_BUTTON_ACTION", _environment_value, _platform_android,
|
||||
"action and reload," },
|
||||
{ "vr.button_weapon", _config_string, "\"y\"", "HALO_VR_BUTTON_WEAPON", _environment_value, _platform_android,
|
||||
"switch weapon," },
|
||||
{ "vr.button_crouch", _config_string, "\"move_stick\"", "HALO_VR_BUTTON_CROUCH", _environment_value,
|
||||
_platform_android,
|
||||
"crouch," },
|
||||
{ "vr.button_zoom", _config_string, "\"turn_stick\"", "HALO_VR_BUTTON_ZOOM", _environment_value,
|
||||
_platform_android,
|
||||
"zoom" },
|
||||
{ "vr.button_melee", _config_string, "\"grip\"", "HALO_VR_BUTTON_MELEE", _environment_value, _platform_android,
|
||||
"and melee. The triggers, the free hand's grip and the menu button keep\n"
|
||||
"theirs, as do the menus." },
|
||||
{ "vr.laser", _config_boolean, "false", "HALO_VR_LASER", _environment_value, _platform_android,
|
||||
"A laser from the weapon to what it aims at (with aim_with = \"controller\")." },
|
||||
{ "vr.crosshair", _config_boolean, "true", "HALO_VR_CROSSHAIR", _environment_value, _platform_android,
|
||||
@@ -304,6 +326,13 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.comfort_vignette", _config_real, "0.7", "HALO_VR_COMFORT_VIGNETTE", _environment_value, _platform_android,
|
||||
"How far the view darkens from its edges while a vehicle turns you\n"
|
||||
"(0 never, 1 to a small window)." },
|
||||
{ "vr.foot_vignette", _config_string, "\"off\"", "HALO_VR_FOOT_VIGNETTE", _environment_value, _platform_android,
|
||||
"On foot, the view darkens from its edges too: \"turning\" while the stick\n"
|
||||
"turns you smoothly (smooth_turn), \"moving\" while a stick turns or moves\n"
|
||||
"you, as far as it is pushed; \"off\" never." },
|
||||
{ "vr.foot_vignette_strength", _config_real, "0.7", "HALO_VR_FOOT_VIGNETTE_STRENGTH", _environment_value,
|
||||
_platform_android,
|
||||
"How far it darkens then (as comfort_vignette)." },
|
||||
{ "vr.motion_tracker_on_wrist", _config_boolean, "true", "HALO_VR_MOTION_TRACKER_WRIST", _environment_value,
|
||||
_platform_android,
|
||||
"Show the motion tracker on the back of the left wrist, like a watch (turn\n"
|
||||
@@ -324,9 +353,11 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.smooth_turn", _config_boolean, "false", "HALO_VR_SMOOTH_TURN", _environment_value, _platform_android,
|
||||
"The right stick turns smoothly; false turns in steps (snap_turn_degrees)." },
|
||||
{ "vr.snap_turn_degrees", _config_real, "30.0", "HALO_VR_SNAP_TURN", _environment_value, _platform_android,
|
||||
"How far a flick of the right stick turns." },
|
||||
"How far a flick of the right stick turns, in degrees (the VR options step\n"
|
||||
"through 10 to 45 by 5, 60, 75 and 90)." },
|
||||
{ "vr.smooth_turn_speed", _config_real, "120.0", "HALO_VR_SMOOTH_TURN_SPEED", _environment_value, _platform_android,
|
||||
"Degrees per second the right stick turns at, fully pushed, with smooth_turn." },
|
||||
"Degrees per second the right stick turns at, fully pushed, with smooth_turn\n"
|
||||
"(the VR options step through 30 to 150 by 15, then to 300 by 30)." },
|
||||
{ "vr.cinematics_in_3d", _config_boolean, "false", "HALO_VR_CINEMATICS_3D", _environment_value, _platform_android,
|
||||
"Show cinematics around you, from the cinematic camera; false shows them\n"
|
||||
"on the screen in front of you (easier on the stomach)." },
|
||||
@@ -573,6 +604,9 @@ static const struct config_setting config_settings[] =
|
||||
"The headset's layer copies: 1 deferred (no waits for the GL thread or the\n"
|
||||
"GPU in the middle of the frame), 0 each one restored and released at once,\n"
|
||||
"-1 as the runtime needs (deferred on SteamVR). Read when the app starts." },
|
||||
{ "debug.vr_options", _config_boolean, "false", "HALO_DEBUG_VR_OPTIONS", _environment_value, _platform_android,
|
||||
"Off the headset (the emulator), show the VR options panel on the main\n"
|
||||
"and pause menus all the same, for checking its pages." },
|
||||
{ "debug.mirror_first_person", _config_boolean, "false", "HALO_MIRROR_FIRST_PERSON", _environment_value,
|
||||
_platform_android,
|
||||
"Off the headset (flat), the first-person weapon drawn mirrored, as the\n"
|
||||
|
||||
@@ -17,6 +17,28 @@ yaw plus the head's.
|
||||
|
||||
#include "halo_vr.h"
|
||||
|
||||
/* the actions' settings and the buttons' values in them (halo_vr.h's
|
||||
HALO_VR_ACTION_* and HALO_VR_INPUT_*), by number */
|
||||
static const char *const vr_action_settings[HALO_VR_ACTION_COUNT] =
|
||||
{
|
||||
"vr.button_jump", "vr.button_light", "vr.button_action", "vr.button_weapon", "vr.button_crouch",
|
||||
"vr.button_zoom", "vr.button_melee",
|
||||
};
|
||||
static const char *const vr_input_values[HALO_VR_INPUT_COUNT] =
|
||||
{
|
||||
"a", "b", "x", "y", "move_stick", "turn_stick", "grip",
|
||||
};
|
||||
|
||||
const char *halo_vr_action_setting(int action)
|
||||
{
|
||||
return action >= 0 && action < HALO_VR_ACTION_COUNT ? vr_action_settings[action] : "";
|
||||
}
|
||||
|
||||
const char *halo_vr_input_value(int input)
|
||||
{
|
||||
return input >= 0 && input < HALO_VR_INPUT_COUNT ? vr_input_values[input] : "";
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
#include "platform.h"
|
||||
@@ -36,6 +58,14 @@ yaw plus the head's.
|
||||
/* the facing the game changes itself by more than this (a respawn, a
|
||||
teleport, a vehicle's seat) turns the body with it */
|
||||
#define FACING_JUMP (10.0f * PI / 180.0f)
|
||||
/* vr.foot_vignette: none on foot, while the stick turns smoothly, and
|
||||
while a stick turns smoothly or moves */
|
||||
enum
|
||||
{
|
||||
FOOT_VIGNETTE_OFF = 0,
|
||||
FOOT_VIGNETTE_TURNING,
|
||||
FOOT_VIGNETTE_MOVING,
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
@@ -133,9 +163,13 @@ static struct
|
||||
float vehicle_steer_smoothing; /* seconds */
|
||||
float steer[2]; /* the steering as smoothed: aside, up (-1..1) */
|
||||
unsigned long long steer_last_us;
|
||||
/* the comfort vignette: how strong at most, now, and the heading it
|
||||
last saw */
|
||||
/* the comfort vignette: how strong at most in a vehicle, on foot
|
||||
(vr.foot_vignette: FOOT_VIGNETTE_*) and how strong there, now, and the
|
||||
heading it last saw */
|
||||
float comfort_vignette;
|
||||
int foot_vignette;
|
||||
float foot_vignette_strength;
|
||||
int seated; /* in a vehicle's seat, whatever the view */
|
||||
float vignette;
|
||||
float vignette_last_yaw;
|
||||
int vignette_yaw_valid;
|
||||
@@ -220,9 +254,9 @@ static struct
|
||||
/* until when a melee attack aims where the head looks: its damage is
|
||||
dealt some ticks into the attack, along the unit's aim */
|
||||
unsigned long long melee_aim_until_us;
|
||||
int melee_grip_down;
|
||||
int melee_button_down;
|
||||
float melee_strength; /* 0..1, the last attack's swing */
|
||||
unsigned long long melee_jab_start_us; /* the weapon hand's grip's attack: the weapon jabs */
|
||||
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;
|
||||
unsigned long laser_frame;
|
||||
@@ -250,6 +284,9 @@ static struct
|
||||
vr.mirror_controls the sticks and face buttons mirrored too */
|
||||
int left_handed;
|
||||
int mirror_controls;
|
||||
/* the button each action is on (vr.button_*: HALO_VR_INPUT_* by
|
||||
HALO_VR_ACTION_*) */
|
||||
int action_inputs[HALO_VR_ACTION_COUNT];
|
||||
/* vr.refresh_rate as last asked for (the VR options change it), and
|
||||
vr.resolution_scale as the app started with it */
|
||||
float refresh_rate_wanted;
|
||||
@@ -307,6 +344,7 @@ static int name_pose(const float point[3], struct halo_xr_pose *pose, float *wid
|
||||
static int weapon_hand_tracked(short local_player_index);
|
||||
static void controller_axes(float forward[3], float up[3], float right[3]);
|
||||
static void hand_offset(float out[3]);
|
||||
static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT]);
|
||||
|
||||
/* the hand that holds the weapon, the free one (the grenade, the
|
||||
foregrip), and the one whose stick turns the body and steers */
|
||||
@@ -494,6 +532,13 @@ static void vr_read_live_settings(void)
|
||||
vr.laser = config_boolean("vr.laser");
|
||||
vr.vehicle_wheel = config_boolean("vr.vehicle_wheel");
|
||||
vr.comfort_vignette = (float)config_real("vr.comfort_vignette");
|
||||
{
|
||||
const char *foot = config_string("vr.foot_vignette");
|
||||
|
||||
vr.foot_vignette = !strcmp(foot, "moving") ? FOOT_VIGNETTE_MOVING :
|
||||
!strcmp(foot, "turning") ? FOOT_VIGNETTE_TURNING : FOOT_VIGNETTE_OFF;
|
||||
}
|
||||
vr.foot_vignette_strength = (float)config_real("vr.foot_vignette_strength");
|
||||
vr.fingers = config_boolean("vr.fingers");
|
||||
vr.arm_ik = config_boolean("vr.arm_ik");
|
||||
{
|
||||
@@ -517,6 +562,12 @@ static void vr_read_live_settings(void)
|
||||
vr.left_handed = left_handed;
|
||||
vr.mirror_controls = config_boolean("vr.mirror_controls");
|
||||
}
|
||||
{
|
||||
int action;
|
||||
|
||||
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
|
||||
vr.action_inputs[action] = halo_vr_action_input(action);
|
||||
}
|
||||
{
|
||||
/* (the refresh rate changes at once where the runtime lets it) */
|
||||
float refresh_rate = (float)config_real("vr.refresh_rate");
|
||||
@@ -619,6 +670,22 @@ int halo_vr_weapon_hand(void)
|
||||
return weapon_hand();
|
||||
}
|
||||
|
||||
int halo_vr_action_input(int action)
|
||||
{
|
||||
const char *value;
|
||||
int input;
|
||||
|
||||
if (action < 0 || action >= HALO_VR_ACTION_COUNT)
|
||||
return HALO_VR_INPUT_A;
|
||||
value = config_string(vr_action_settings[action]);
|
||||
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
|
||||
{
|
||||
if (!strcmp(value, vr_input_values[input]))
|
||||
return input;
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
int halo_vr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
*current = 0.0f;
|
||||
@@ -980,18 +1047,21 @@ static void melee_update(void)
|
||||
|
||||
vr.hand_last_us = now;
|
||||
{
|
||||
/* the weapon hand's grip's melee attack (xinput_sdl.c) aims the
|
||||
same way */
|
||||
int grip_down = vr.frame.hands[weapon_hand()].active &&
|
||||
(vr.frame.hands[weapon_hand()].buttons & HALO_XR_BUTTON_SQUEEZE);
|
||||
/* the melee button's attack (xinput_sdl.c; the weapon hand's grip
|
||||
unless vr.button_melee moves it) aims the same way */
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int button_down;
|
||||
|
||||
if (grip_down && !vr.melee_grip_down)
|
||||
memcpy(hands, vr.frame.hands, sizeof(hands));
|
||||
controls_in_play(hands);
|
||||
button_down = (hands[HALO_XR_HAND_RIGHT].buttons & HALO_XR_BUTTON_SQUEEZE) != 0;
|
||||
if (button_down && !vr.melee_button_down)
|
||||
{
|
||||
vr.melee_aim_until_us = now + 700000;
|
||||
vr.melee_strength = 0.75f;
|
||||
vr.melee_jab_start_us = now;
|
||||
}
|
||||
vr.melee_grip_down = grip_down;
|
||||
vr.melee_button_down = button_down;
|
||||
}
|
||||
quaternion_rotate(vr.frame.head.orientation, look, facing);
|
||||
facing[1] = 0.0f;
|
||||
@@ -1378,13 +1448,45 @@ int halo_vr_wheel_held(void)
|
||||
}
|
||||
|
||||
/* The comfort vignette: while a vehicle turns the player (from 15 degrees a
|
||||
second, fully at 60), the eye images darken from their edges; it comes in
|
||||
quickly and goes slowly. */
|
||||
second, fully at 60), the eye images darken from their edges; on foot
|
||||
(vr.foot_vignette), while the turning stick turns smoothly and, with
|
||||
"moving", the move stick moves, as far as they are pushed (fully at three
|
||||
quarters). It comes in quickly and goes slowly. */
|
||||
static float foot_vignette_target(void)
|
||||
{
|
||||
const struct halo_xr_hand *turn = &vr.frame.hands[turn_hand()];
|
||||
const struct halo_xr_hand *move = &vr.frame.hands[turn_hand() == HALO_XR_HAND_RIGHT ? HALO_XR_HAND_LEFT :
|
||||
HALO_XR_HAND_RIGHT];
|
||||
float push = 0.0f;
|
||||
|
||||
if (vr.foot_vignette == FOOT_VIGNETTE_OFF || vr.foot_vignette_strength <= 0.0f || vr.seated ||
|
||||
!vr.frame.focused || vr.last_overlay_mode == HALO_XR_OVERLAY_SCREEN)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
if (vr.smooth_turn && turn->active)
|
||||
push = fabsf(turn->thumbstick[0]);
|
||||
if (vr.foot_vignette == FOOT_VIGNETTE_MOVING && move->active)
|
||||
{
|
||||
float moving = sqrtf(move->thumbstick[0] * move->thumbstick[0] + move->thumbstick[1] * move->thumbstick[1]);
|
||||
|
||||
if (moving > push)
|
||||
push = moving;
|
||||
}
|
||||
/* (beyond the sticks' dead zone, 0.15) */
|
||||
push = (push - 0.15f) / 0.6f;
|
||||
if (push < 0.0f)
|
||||
push = 0.0f;
|
||||
if (push > 1.0f)
|
||||
push = 1.0f;
|
||||
return push * (vr.foot_vignette_strength > 1.0f ? 1.0f : vr.foot_vignette_strength);
|
||||
}
|
||||
|
||||
static void vignette_update(void)
|
||||
{
|
||||
unsigned long long now = now_us();
|
||||
float seconds = vr.vignette_last_us ? (float)(now - vr.vignette_last_us) * 1e-6f : 0.0f;
|
||||
float target = 0.0f, rate;
|
||||
float target = foot_vignette_target(), rate;
|
||||
|
||||
vr.vignette_last_us = now;
|
||||
if (seconds > 0.1f)
|
||||
@@ -1396,13 +1498,15 @@ static void vignette_update(void)
|
||||
if (vr.vignette_yaw_valid && seconds > 0.0f)
|
||||
{
|
||||
float turning = fabsf(wrap_angle(yaw - vr.vignette_last_yaw)) / seconds;
|
||||
float vehicle = (turning - 15.0f * PI / 180.0f) / (45.0f * PI / 180.0f);
|
||||
|
||||
target = (turning - 15.0f * PI / 180.0f) / (45.0f * PI / 180.0f);
|
||||
if (target < 0.0f)
|
||||
target = 0.0f;
|
||||
if (target > 1.0f)
|
||||
target = 1.0f;
|
||||
target *= vr.comfort_vignette;
|
||||
if (vehicle < 0.0f)
|
||||
vehicle = 0.0f;
|
||||
if (vehicle > 1.0f)
|
||||
vehicle = 1.0f;
|
||||
vehicle *= vr.comfort_vignette;
|
||||
if (vehicle > target)
|
||||
target = vehicle;
|
||||
}
|
||||
vr.vignette_last_yaw = yaw;
|
||||
vr.vignette_yaw_valid = 1;
|
||||
@@ -1423,6 +1527,7 @@ void halo_vr_set_seat(short local_player_index, int seat, int armed, float vehic
|
||||
{
|
||||
if (!vr.active || local_player_index != 0)
|
||||
return;
|
||||
vr.seated = seat != HALO_VR_SEAT_NONE;
|
||||
if (!vr.vehicle_first_person)
|
||||
seat = HALO_VR_SEAT_NONE;
|
||||
vr.seat_yaw = vehicle_yaw;
|
||||
@@ -3046,6 +3151,51 @@ static void controllers_mirrored(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
|
||||
}
|
||||
}
|
||||
|
||||
/* The buttons the actions are on (vr.button_*), as the gamepad's sides
|
||||
have them once mirrored: A and B the right hand's, X and Y the left's
|
||||
A and B, the move stick's click the left's, the turning stick's the
|
||||
right's, the grip the right's (the weapon hand's). Each action's button
|
||||
is read and handed on as the button the action has by default, which
|
||||
xinput_sdl.c turns into it; with the defaults nothing changes. The grips
|
||||
holding a Warthog's wheel press nothing. */
|
||||
static const int input_hands[HALO_VR_INPUT_COUNT] =
|
||||
{
|
||||
HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT, HALO_XR_HAND_LEFT,
|
||||
HALO_XR_HAND_RIGHT, HALO_XR_HAND_RIGHT,
|
||||
};
|
||||
static const int input_buttons[HALO_VR_INPUT_COUNT] =
|
||||
{
|
||||
HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_A, HALO_XR_BUTTON_B, HALO_XR_BUTTON_THUMBSTICK,
|
||||
HALO_XR_BUTTON_THUMBSTICK, HALO_XR_BUTTON_SQUEEZE,
|
||||
};
|
||||
|
||||
static void controls_remapped(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
|
||||
{
|
||||
int down[HALO_VR_INPUT_COUNT];
|
||||
int input, action;
|
||||
|
||||
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
|
||||
down[input] = (hands[input_hands[input]].buttons & input_buttons[input]) != 0;
|
||||
if (vr.wheel_hands)
|
||||
down[HALO_VR_INPUT_GRIP] = 0;
|
||||
for (input = 0; input < HALO_VR_INPUT_COUNT; input++)
|
||||
hands[input_hands[input]].buttons &= ~input_buttons[input];
|
||||
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
|
||||
{
|
||||
if (down[vr.action_inputs[action]])
|
||||
hands[input_hands[action]].buttons |= input_buttons[action];
|
||||
}
|
||||
}
|
||||
|
||||
/* the controllers as the game is played with them: mirrored (left-handed)
|
||||
and the actions on their buttons */
|
||||
static void controls_in_play(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
|
||||
{
|
||||
if (vr.left_handed)
|
||||
controllers_mirrored(hands);
|
||||
controls_remapped(hands);
|
||||
}
|
||||
|
||||
/* the controllers, for the gamepad the game reads (xinput_sdl.c) */
|
||||
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus)
|
||||
{
|
||||
@@ -3053,8 +3203,8 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
|
||||
return 0;
|
||||
memcpy(hands, vr.frame.hands, sizeof(vr.frame.hands));
|
||||
*menus = vr.last_overlay_mode == HALO_XR_OVERLAY_SCREEN;
|
||||
if (vr.left_handed && !*menus)
|
||||
controllers_mirrored(hands);
|
||||
if (!*menus)
|
||||
controls_in_play(hands);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3062,6 +3212,7 @@ int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menu
|
||||
|
||||
int halo_vr_controller_layout(void) { return 0; }
|
||||
int halo_vr_weapon_hand(void) { return 1; }
|
||||
int halo_vr_action_input(int action) { return action; }
|
||||
int halo_vr_graphics(void) { return HALO_VR_GRAPHICS_HIGH; }
|
||||
int halo_vr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
|
||||
@@ -859,6 +859,11 @@ face buttons too (the right stick moves, X jumps, A reloads; on the Steam
|
||||
Frame the left D-pad takes A, B, X and Y). Here "right" and "left" are the
|
||||
sides so handed over.
|
||||
|
||||
The VR options' BUTTONS page (vr.button_*) moves jump, the flashlight,
|
||||
action, switch weapon, crouch, zoom and melee between A, B, X, Y, the
|
||||
stick clicks and the weapon hand's grip: vr.c hands each action's button
|
||||
over as the one above, so "A" here is the jump wherever it is.
|
||||
|
||||
In the menus the buttons are the Xbox's by position: A accepts, B goes back. */
|
||||
|
||||
#include "guest_host.h"
|
||||
@@ -937,9 +942,8 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
}
|
||||
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
|
||||
/* (the grips holding a Warthog's wheel are not a blow) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_B,
|
||||
((right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 && !halo_vr_wheel_held()) || halo_vr_melee());
|
||||
/* (the grips holding a Warthog's wheel are not a blow: vr.c) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 || halo_vr_melee());
|
||||
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
|
||||
/* (the Steam Frame's bumpers: the left switches grenades, the right is
|
||||
the flashlight) */
|
||||
|
||||
@@ -6648,6 +6648,7 @@ double config_real(char const *name);
|
||||
char const *config_string(char const *name);
|
||||
int config_write(char const *name, char const *value);
|
||||
int config_write_boolean(char const *name, int value);
|
||||
int config_default(char const *name, char *text, size_t size);
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -6655,6 +6656,8 @@ enum
|
||||
_vr_option_snap_angle,
|
||||
_vr_option_smooth_speed,
|
||||
_vr_option_vignette,
|
||||
_vr_option_foot_vignette,
|
||||
_vr_option_foot_vignette_strength,
|
||||
_vr_option_wheel,
|
||||
_vr_option_handedness,
|
||||
_vr_option_mirror_controls,
|
||||
@@ -6663,6 +6666,10 @@ enum
|
||||
_vr_option_motion_tracker,
|
||||
_vr_option_body,
|
||||
_vr_option_fingers,
|
||||
/* (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_buttons_reset,
|
||||
_vr_option_graphics,
|
||||
_vr_option_refresh_rate,
|
||||
_vr_option_resolution,
|
||||
@@ -6675,7 +6682,17 @@ enum
|
||||
VR_OPTIONS_ROW_HEIGHT = 22,
|
||||
VR_OPTIONS_FOOTER = 362,
|
||||
VR_OPTIONS_PER_PAGE = 12,
|
||||
NUMBER_OF_VR_OPTION_PAGES = 2,
|
||||
|
||||
_vr_options_page_play = 0,
|
||||
_vr_options_page_comfort,
|
||||
_vr_options_page_buttons,
|
||||
_vr_options_page_more,
|
||||
NUMBER_OF_VR_OPTION_PAGES,
|
||||
|
||||
/* (the pages' tabs, beside the title, as wide as there is room) */
|
||||
VR_OPTIONS_TABS_LEFT = 216,
|
||||
VR_OPTIONS_TABS_RIGHT = 528,
|
||||
VR_OPTIONS_TAB_WIDTH = (VR_OPTIONS_TABS_RIGHT - VR_OPTIONS_TABS_LEFT) / NUMBER_OF_VR_OPTION_PAGES,
|
||||
|
||||
_vr_controls_on_foot = 0,
|
||||
_vr_controls_vehicles,
|
||||
@@ -6694,14 +6711,22 @@ static struct
|
||||
} ui_vr_options_globals;
|
||||
|
||||
/* the panel's pages (its tabs), their rows in order (NONE past the last) */
|
||||
static char const *const vr_option_page_names[NUMBER_OF_VR_OPTION_PAGES] = { "PLAY", "MORE" };
|
||||
static char const *const vr_option_page_names[NUMBER_OF_VR_OPTION_PAGES] = { "PLAY", "COMFORT", "BUTTONS", "MORE" };
|
||||
static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAGE] =
|
||||
{
|
||||
{ _vr_option_turning, _vr_option_snap_angle, _vr_option_smooth_speed, _vr_option_vignette, _vr_option_wheel,
|
||||
_vr_option_handedness, _vr_option_mirror_controls, _vr_option_laser, _vr_option_crosshair,
|
||||
_vr_option_motion_tracker, _vr_option_body, _vr_option_fingers },
|
||||
{ _vr_option_graphics, _vr_option_refresh_rate, _vr_option_resolution, _vr_option_controls, _vr_option_quit,
|
||||
NONE, NONE, NONE, NONE, NONE, NONE, NONE },
|
||||
{ _vr_option_handedness, _vr_option_mirror_controls, _vr_option_laser, _vr_option_crosshair,
|
||||
_vr_option_motion_tracker, _vr_option_body, _vr_option_fingers, _vr_option_wheel,
|
||||
NONE, NONE, NONE, NONE },
|
||||
{ _vr_option_turning, _vr_option_snap_angle, _vr_option_smooth_speed, _vr_option_vignette,
|
||||
_vr_option_foot_vignette, _vr_option_foot_vignette_strength,
|
||||
NONE, NONE, NONE, NONE, NONE, NONE },
|
||||
{ _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_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 },
|
||||
};
|
||||
|
||||
/* the page a row is on, and its place there */
|
||||
@@ -6754,12 +6779,151 @@ static boolean ui_vr_controls_mirrored(
|
||||
return ui_vr_left_handed() && config_boolean("vr.mirror_controls");
|
||||
}
|
||||
|
||||
/* Where a button the game's actions go on (HALO_VR_INPUT_*) is, as the
|
||||
hands are set: its controller (0 the left, 1 the right) and its name,
|
||||
short under its controller on the controls page, or full in the
|
||||
options' rows. On the Quest A and B are on the right, X and Y on the
|
||||
left, and mirrored the other way round; on the Steam Frame all four are
|
||||
on the right, and mirrored the left D-pad takes them (down, left, right,
|
||||
up). The sticks' clicks change sides with them; the grip is the weapon
|
||||
hand's. */
|
||||
static char const *ui_vr_input_name(
|
||||
short input,
|
||||
boolean full,
|
||||
short *column)
|
||||
{
|
||||
static char const *const buttons[] = { "A", "B", "X", "Y" };
|
||||
static char const *const quest_mirrored[] = { "X", "Y", "A", "B" };
|
||||
static char const *const frame_mirrored[] = { "D-pad down", "D-pad left", "D-pad right", "D-pad up" };
|
||||
boolean frame = halo_vr_controller_layout() == 1;
|
||||
boolean mirrored = ui_vr_controls_mirrored();
|
||||
short side;
|
||||
char const *name;
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case HALO_VR_INPUT_MOVE_STICK:
|
||||
case HALO_VR_INPUT_TURN_STICK:
|
||||
side = (short)((input == HALO_VR_INPUT_TURN_STICK) != (mirrored != 0));
|
||||
name = full ? (side ? "Right stick click" : "Left stick click") : "Stick click";
|
||||
break;
|
||||
case HALO_VR_INPUT_GRIP:
|
||||
side = (short)(ui_vr_left_handed() ? 0 : 1);
|
||||
name = full ? (side ? "Right grip" : "Left grip") : "Grip";
|
||||
break;
|
||||
default:
|
||||
input = (short)PIN(input, HALO_VR_INPUT_A, HALO_VR_INPUT_Y);
|
||||
if (frame)
|
||||
{
|
||||
side = (short)(mirrored ? 0 : 1);
|
||||
name = mirrored ? frame_mirrored[input] : buttons[input];
|
||||
}
|
||||
else
|
||||
{
|
||||
side = (short)((input <= HALO_VR_INPUT_B) != (mirrored != 0));
|
||||
name = mirrored ? quest_mirrored[input] : buttons[input];
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (column)
|
||||
*column = side;
|
||||
return name;
|
||||
}
|
||||
|
||||
/* the game's actions on the buttons (HALO_VR_ACTION_*'s order): as the
|
||||
options' rows name them, and in short (the Steam Frame's stick lines) */
|
||||
static char const *const vr_action_names[HALO_VR_ACTION_COUNT] =
|
||||
{
|
||||
"Jump", "Light; held: grenades", "Action, reload", "Switch weapon", "Crouch", "Zoom", "Melee",
|
||||
};
|
||||
static char const *const vr_action_short_names[HALO_VR_ACTION_COUNT] =
|
||||
{
|
||||
"jump", "light", "action", "weapon", "crouch", "zoom", "melee",
|
||||
};
|
||||
|
||||
/* an action moved to the next button or the one before: the action that
|
||||
was there takes its old one (so that every action keeps a button) */
|
||||
static void ui_vr_options_button_change(
|
||||
short action,
|
||||
short change)
|
||||
{
|
||||
short input = (short)halo_vr_action_input(action);
|
||||
short wanted = (short)((input + change + HALO_VR_INPUT_COUNT) % HALO_VR_INPUT_COUNT);
|
||||
short other;
|
||||
|
||||
for (other = 0; other < HALO_VR_ACTION_COUNT; other++)
|
||||
{
|
||||
if (other != action && halo_vr_action_input(other) == wanted)
|
||||
config_write(halo_vr_action_setting(other), halo_vr_input_value(input));
|
||||
}
|
||||
config_write(halo_vr_action_setting(action), halo_vr_input_value(wanted));
|
||||
}
|
||||
|
||||
static boolean ui_vr_options_buttons_default(
|
||||
void)
|
||||
{
|
||||
short action;
|
||||
|
||||
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
|
||||
{
|
||||
if (halo_vr_action_input(action) != action)
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void ui_vr_options_buttons_reset(
|
||||
void)
|
||||
{
|
||||
short action;
|
||||
|
||||
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
|
||||
{
|
||||
char text[32];
|
||||
|
||||
if (config_default(halo_vr_action_setting(action), text, sizeof(text)))
|
||||
config_write(halo_vr_action_setting(action), text);
|
||||
}
|
||||
}
|
||||
|
||||
void platform_request_quit(void);
|
||||
|
||||
static real const vr_snap_angles[] = { 30.0f, 45.0f, 60.0f, 90.0f };
|
||||
static real const vr_smooth_speeds[] = { 60.0f, 90.0f, 120.0f, 150.0f, 180.0f };
|
||||
static real const vr_vignettes[] = { 0.0f, 0.35f, 0.7f, 1.0f };
|
||||
static char const *const vr_vignette_names[] = { "Off", "Light", "Medium", "Strong" };
|
||||
/* (in finer steps than they had: the old values among them) */
|
||||
static real const vr_snap_angles[] = { 10.0f, 15.0f, 20.0f, 25.0f, 30.0f, 35.0f, 40.0f, 45.0f, 60.0f, 75.0f, 90.0f };
|
||||
static real const vr_smooth_speeds[] = { 30.0f, 45.0f, 60.0f, 75.0f, 90.0f, 105.0f, 120.0f, 135.0f, 150.0f, 180.0f,
|
||||
210.0f, 240.0f, 270.0f, 300.0f };
|
||||
static real const vr_vignettes[] = { 0.0f, 0.15f, 0.25f, 0.35f, 0.5f, 0.6f, 0.7f, 0.85f, 1.0f };
|
||||
/* the on-foot vignette's strengths (vr.foot_vignette_strength: the
|
||||
vignettes' without Off), and vr.foot_vignette's values */
|
||||
static real const vr_foot_vignette_strengths[] = { 0.15f, 0.25f, 0.35f, 0.5f, 0.6f, 0.7f, 0.85f, 1.0f };
|
||||
static char const *const vr_foot_vignettes[] = { "off", "turning", "moving" };
|
||||
static char const *const vr_foot_vignette_names[] = { "Off", "Smooth turns", "Moving, turning" };
|
||||
|
||||
static short ui_vr_options_foot_vignette(
|
||||
void)
|
||||
{
|
||||
char const *mode = config_string("vr.foot_vignette");
|
||||
short index;
|
||||
|
||||
for (index = 0; index < (short)NUMBEROF(vr_foot_vignettes); index++)
|
||||
{
|
||||
if (mode && strcmp(mode, vr_foot_vignettes[index]) == 0)
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* a vignette's strength as the rows show it: Off, or how far it darkens */
|
||||
static void ui_vr_options_vignette_text(
|
||||
char *text,
|
||||
size_t size,
|
||||
real strength)
|
||||
{
|
||||
if (strength <= 0.0f)
|
||||
snprintf(text, size, "Off");
|
||||
else
|
||||
snprintf(text, size, "%.0f%%", strength * 100.0f);
|
||||
}
|
||||
/* vr.body's values, and as the panel shows them */
|
||||
static char const *const vr_bodies[] = { "arms", "hands", "off" };
|
||||
static char const *const vr_body_names[] = { "Arms and hands", "Hands only", "Weapon only" };
|
||||
@@ -6838,8 +7002,9 @@ static boolean ui_vr_options_screen(
|
||||
{
|
||||
struct widget_instance *screen = widget_globals.active_widgets[0];
|
||||
|
||||
/* (the Xbox's main menu, or the PC version's: vr.menus) */
|
||||
return halo_vr_active() && screen && screen->name &&
|
||||
/* (the Xbox's main menu, or the PC version's: vr.menus; off the
|
||||
headset with debug.vr_options, to check the pages) */
|
||||
return (halo_vr_active() || config_boolean("debug.vr_options")) && screen && screen->name &&
|
||||
(main_menu_screen_is_active() || strstr(screen->name, "pause_game") != NULL ||
|
||||
strcmp(screen->name, "main_menu") == 0);
|
||||
}
|
||||
@@ -6891,6 +7056,14 @@ static void ui_vr_options_change(
|
||||
case _vr_option_vignette:
|
||||
ui_vr_options_step("vr.comfort_vignette", vr_vignettes, NUMBEROF(vr_vignettes), change);
|
||||
break;
|
||||
case _vr_option_foot_vignette:
|
||||
config_write("vr.foot_vignette", vr_foot_vignettes[(ui_vr_options_foot_vignette() + change +
|
||||
(short)NUMBEROF(vr_foot_vignettes)) % (short)NUMBEROF(vr_foot_vignettes)]);
|
||||
break;
|
||||
case _vr_option_foot_vignette_strength:
|
||||
ui_vr_options_step("vr.foot_vignette_strength", vr_foot_vignette_strengths,
|
||||
NUMBEROF(vr_foot_vignette_strengths), change);
|
||||
break;
|
||||
case _vr_option_wheel: config_write_boolean("vr.vehicle_wheel", !config_boolean("vr.vehicle_wheel")); break;
|
||||
case _vr_option_handedness: config_write("vr.handedness", ui_vr_left_handed() ? "right" : "left"); break;
|
||||
case _vr_option_mirror_controls:
|
||||
@@ -6928,6 +7101,9 @@ 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_buttons_reset:
|
||||
ui_vr_options_buttons_reset();
|
||||
break;
|
||||
case _vr_option_controls:
|
||||
ui_vr_options_globals.controls = TRUE;
|
||||
ui_vr_options_globals.page = _vr_controls_on_foot;
|
||||
@@ -6940,6 +7116,11 @@ static void ui_vr_options_change(
|
||||
ui_vr_options_globals.quit_armed = TRUE;
|
||||
break;
|
||||
}
|
||||
if (ui_vr_options_globals.selection >= _vr_option_button_first &&
|
||||
ui_vr_options_globals.selection <= _vr_option_button_last)
|
||||
{
|
||||
ui_vr_options_button_change((short)(ui_vr_options_globals.selection - _vr_option_button_first), change);
|
||||
}
|
||||
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
|
||||
}
|
||||
|
||||
@@ -7056,7 +7237,8 @@ static boolean ui_vr_options_take_event(
|
||||
{
|
||||
ui_vr_options_change(1);
|
||||
}
|
||||
else if (change && ui_vr_options_globals.selection != _vr_option_controls)
|
||||
else if (change && ui_vr_options_globals.selection != _vr_option_controls &&
|
||||
ui_vr_options_globals.selection != _vr_option_buttons_reset)
|
||||
{
|
||||
ui_vr_options_change(change);
|
||||
}
|
||||
@@ -7113,10 +7295,11 @@ static boolean ui_vr_options_take_pointer(
|
||||
{
|
||||
short page = ui_vr_options_page_of(ui_vr_options_globals.selection, NULL);
|
||||
|
||||
if (pointer->left_clicks && y >= 68 && y < 88 && x >= 330 && x < 514)
|
||||
if (pointer->left_clicks && y >= 68 && y < 88 && x >= VR_OPTIONS_TABS_LEFT && x < VR_OPTIONS_TABS_RIGHT)
|
||||
{
|
||||
/* (a tab: its page) */
|
||||
short tab = (short)PIN((x - 330) / 92, 0, NUMBER_OF_VR_OPTION_PAGES - 1);
|
||||
short tab = (short)PIN((x - VR_OPTIONS_TABS_LEFT) / VR_OPTIONS_TAB_WIDTH, 0,
|
||||
NUMBER_OF_VR_OPTION_PAGES - 1);
|
||||
|
||||
if (tab != page)
|
||||
{
|
||||
@@ -7196,6 +7379,77 @@ static void ui_vr_controls_add(
|
||||
}
|
||||
}
|
||||
|
||||
/* the action on a button (vr.button_*; the swaps keep one on each) */
|
||||
static short ui_vr_input_action(
|
||||
short input)
|
||||
{
|
||||
short action;
|
||||
|
||||
for (action = 0; action < HALO_VR_ACTION_COUNT; action++)
|
||||
{
|
||||
if (halo_vr_action_input(action) == input)
|
||||
return action;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/* in a vehicle: the action button gets in and out, the light's is the
|
||||
flashlight */
|
||||
static void ui_vr_controls_add_vehicle_actions(
|
||||
struct ui_vr_controls_column columns[2])
|
||||
{
|
||||
short column;
|
||||
char const *name = ui_vr_input_name((short)halo_vr_action_input(HALO_VR_ACTION_ACTION), FALSE, &column);
|
||||
|
||||
ui_vr_controls_add(&columns[column], name, "Get in, get out");
|
||||
name = ui_vr_input_name((short)halo_vr_action_input(HALO_VR_ACTION_LIGHT), FALSE, &column);
|
||||
ui_vr_controls_add(&columns[column], name, "Flashlight");
|
||||
}
|
||||
|
||||
/* an action's line under the controller its button is on (vr.button_*), as
|
||||
the Quest's page words it or, shorter, the Steam Frame's (whose page has
|
||||
more lines) */
|
||||
static void ui_vr_controls_add_action(
|
||||
struct ui_vr_controls_column columns[2],
|
||||
short action,
|
||||
boolean frame)
|
||||
{
|
||||
/* (one action is held: its name lives on with the column) */
|
||||
static char held[24];
|
||||
short column;
|
||||
char const *name = ui_vr_input_name((short)halo_vr_action_input(action), FALSE, &column);
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case HALO_VR_ACTION_LIGHT:
|
||||
if (frame)
|
||||
{
|
||||
ui_vr_controls_add(&columns[column], name, "Light; held: grenades");
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(held, sizeof(held), "%s held", strcmp(name, "Stick click") == 0 ? "Click" : name);
|
||||
ui_vr_controls_add(&columns[column], name, "Flashlight");
|
||||
ui_vr_controls_add(&columns[column], held, "Switch grenades");
|
||||
}
|
||||
break;
|
||||
case HALO_VR_ACTION_MELEE:
|
||||
if (frame)
|
||||
{
|
||||
ui_vr_controls_add(&columns[column], name, "Melee (or punch)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ui_vr_controls_add(&columns[column], name, "Melee (or punch");
|
||||
ui_vr_controls_add(&columns[column], "", "forward)");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ui_vr_controls_add(&columns[column], name, vr_action_names[action]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_vr_controls_column_render(
|
||||
short x,
|
||||
short y,
|
||||
@@ -7264,26 +7518,27 @@ static void ui_vr_controls_render(
|
||||
{
|
||||
/* the Steam Frame's controllers: A, B, X, Y and a menu button on the
|
||||
right one, a D-pad and a View button on the left one; mirrored, the
|
||||
D-pad does what A, B, X and Y do (down, left, right, up) */
|
||||
char const *jump = mirrored ? "D-pad down" : "A";
|
||||
char const *light = mirrored ? "D-pad left" : "B";
|
||||
char const *act = mirrored ? "D-pad right" : "X";
|
||||
char const *weapon = mirrored ? "D-pad up" : "Y";
|
||||
short buttons = mirrored ? 0 : 1;
|
||||
D-pad does what A, B, X and Y do (down, left, right, up). The sticks'
|
||||
clicks on the sticks' lines, to keep to the page's lines */
|
||||
static char move_line[32], turn_line[32];
|
||||
|
||||
switch (ui_vr_options_globals.page)
|
||||
{
|
||||
case _vr_controls_on_foot:
|
||||
ui_vr_controls_add(&columns[move], "Stick", "Move; click: crouch");
|
||||
ui_vr_controls_add(&columns[turn], "Stick", "Turn; click: zoom");
|
||||
snprintf(move_line, sizeof(move_line), "Move; click: %s",
|
||||
vr_action_short_names[ui_vr_input_action(HALO_VR_INPUT_MOVE_STICK)]);
|
||||
snprintf(turn_line, sizeof(turn_line), "Turn; click: %s",
|
||||
vr_action_short_names[ui_vr_input_action(HALO_VR_INPUT_TURN_STICK)]);
|
||||
ui_vr_controls_add(&columns[move], "Stick", move_line);
|
||||
ui_vr_controls_add(&columns[turn], "Stick", turn_line);
|
||||
ui_vr_controls_add(&columns[gun], "Trigger", "Fire");
|
||||
ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold, throw");
|
||||
ui_vr_controls_add(&columns[gun], "Grip", "Melee (or punch)");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), TRUE);
|
||||
ui_vr_controls_add(&columns[free_column], "Grip", "Weapon in both hands");
|
||||
ui_vr_controls_add(&columns[buttons], jump, "Jump");
|
||||
ui_vr_controls_add(&columns[buttons], light, "Light; held: grenades");
|
||||
ui_vr_controls_add(&columns[buttons], act, "Action, reload");
|
||||
ui_vr_controls_add(&columns[buttons], weapon, "Switch weapon");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), TRUE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), TRUE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), TRUE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_Y), TRUE);
|
||||
ui_vr_controls_add(&columns[mirrored ? 0 : 1], "Bumper", "Flashlight");
|
||||
ui_vr_controls_add(&columns[mirrored ? 1 : 0], "Bumper", "Switch grenades");
|
||||
ui_vr_controls_add(&columns[0], "View", "Scores");
|
||||
@@ -7302,8 +7557,7 @@ static void ui_vr_controls_render(
|
||||
ui_vr_controls_add(&columns[gun], "Trigger", "Fire");
|
||||
ui_vr_controls_add(&columns[free_column], "Trigger", "Second weapon");
|
||||
ui_vr_controls_add(&columns[free_column], "", "(Banshee, tank)");
|
||||
ui_vr_controls_add(&columns[buttons], act, "Get in, get out");
|
||||
ui_vr_controls_add(&columns[buttons], light, "Flashlight");
|
||||
ui_vr_controls_add_vehicle_actions(columns);
|
||||
break;
|
||||
default:
|
||||
ui_vr_controls_add(&columns[0], "Stick", "Choose");
|
||||
@@ -7322,27 +7576,22 @@ static void ui_vr_controls_render(
|
||||
/* the Quest's: A and B on the right controller, X, Y and the menu
|
||||
button on the left; mirrored, X and Y do what A and B do, and the
|
||||
other way round */
|
||||
short jumps = mirrored ? 0 : 1;
|
||||
short acts = (short)(1 - jumps);
|
||||
|
||||
switch (ui_vr_options_globals.page)
|
||||
{
|
||||
case _vr_controls_on_foot:
|
||||
ui_vr_controls_add(&columns[move], "Stick", "Move");
|
||||
ui_vr_controls_add(&columns[move], "Stick click", "Crouch");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_MOVE_STICK), FALSE);
|
||||
ui_vr_controls_add(&columns[turn], "Stick", "Turn");
|
||||
ui_vr_controls_add(&columns[turn], "Stick click", "Zoom");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_TURN_STICK), FALSE);
|
||||
ui_vr_controls_add(&columns[gun], "Trigger", "Fire");
|
||||
ui_vr_controls_add(&columns[gun], "Grip", "Melee (or punch");
|
||||
ui_vr_controls_add(&columns[gun], "", "forward)");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_GRIP), FALSE);
|
||||
ui_vr_controls_add(&columns[free_column], "Trigger", "Grenade: hold it,");
|
||||
ui_vr_controls_add(&columns[free_column], "", "let go as you throw");
|
||||
ui_vr_controls_add(&columns[free_column], "Grip", "Weapon in both hands");
|
||||
ui_vr_controls_add(&columns[jumps], jumps ? "A" : "X", "Jump");
|
||||
ui_vr_controls_add(&columns[jumps], jumps ? "B" : "Y", "Flashlight");
|
||||
ui_vr_controls_add(&columns[jumps], jumps ? "B held" : "Y held", "Switch grenades");
|
||||
ui_vr_controls_add(&columns[acts], acts ? "A" : "X", "Action, reload");
|
||||
ui_vr_controls_add(&columns[acts], acts ? "B" : "Y", "Switch weapon");
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_A), FALSE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_B), FALSE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_X), FALSE);
|
||||
ui_vr_controls_add_action(columns, ui_vr_input_action(HALO_VR_INPUT_Y), FALSE);
|
||||
ui_vr_controls_add(&columns[0], "Menu", "Pause");
|
||||
ui_vr_controls_note(320, wrist_note);
|
||||
ui_vr_controls_note(340, "Scope: zoom, and look through the disc above the weapon.");
|
||||
@@ -7355,8 +7604,7 @@ static void ui_vr_controls_render(
|
||||
ui_vr_controls_add(&columns[gun], "Trigger", "Fire");
|
||||
ui_vr_controls_add(&columns[free_column], "Trigger", "Second weapon");
|
||||
ui_vr_controls_add(&columns[free_column], "", "(Banshee, tank)");
|
||||
ui_vr_controls_add(&columns[acts], acts ? "A" : "X", "Get in, get out");
|
||||
ui_vr_controls_add(&columns[jumps], jumps ? "B" : "Y", "Flashlight");
|
||||
ui_vr_controls_add_vehicle_actions(columns);
|
||||
break;
|
||||
default:
|
||||
ui_vr_controls_add(&columns[0], "Stick", "Choose");
|
||||
@@ -7373,6 +7621,9 @@ static void ui_vr_controls_render(
|
||||
ui_vr_controls_column_render(right, y, &columns[1]);
|
||||
switch (ui_vr_options_globals.page)
|
||||
{
|
||||
case _vr_controls_on_foot:
|
||||
/* (its notes are with its lines, above) */
|
||||
break;
|
||||
case _vr_controls_vehicles:
|
||||
ui_vr_controls_note(300, "Warthog: grip the wheel with one hand or both and turn it.");
|
||||
ui_vr_controls_note(320, left_handed ? "Holding it, the left trigger accelerates, the right one brakes" :
|
||||
@@ -7429,15 +7680,15 @@ static void ui_vr_options_render(
|
||||
ui_join_code_draw_quad(110, 62, 530, 396, 0xFF050A18);
|
||||
ui_join_code_draw_quad(110, 62, 530, 64, 0xFF3A6EA5);
|
||||
ui_join_code_draw_quad(110, 394, 530, 396, 0xFF3A6EA5);
|
||||
ui_join_code_draw_text(126, 330, 72, "VR OPTIONS", FALSE, 0, &title_color);
|
||||
ui_join_code_draw_text(126, VR_OPTIONS_TABS_LEFT, 72, "VR OPTIONS", FALSE, 0, &title_color);
|
||||
/* (the pages' tabs, beside the title) */
|
||||
for (tab = 0; tab < NUMBER_OF_VR_OPTION_PAGES; tab++)
|
||||
{
|
||||
short x0 = (short)(330 + tab * 92);
|
||||
short x0 = (short)(VR_OPTIONS_TABS_LEFT + tab * VR_OPTIONS_TAB_WIDTH);
|
||||
|
||||
if (tab == page)
|
||||
ui_join_code_draw_quad(x0, 68, (short)(x0 + 88), 88, 0x803A6EA5);
|
||||
ui_join_code_draw_text(x0, (short)(x0 + 88), 72, vr_option_page_names[tab], FALSE, 2,
|
||||
ui_join_code_draw_quad(x0, 68, (short)(x0 + VR_OPTIONS_TAB_WIDTH - 4), 88, 0x803A6EA5);
|
||||
ui_join_code_draw_text(x0, (short)(x0 + VR_OPTIONS_TAB_WIDTH - 4), 72, vr_option_page_names[tab], FALSE, 2,
|
||||
tab == page ? &text_color : &dim_color);
|
||||
}
|
||||
for (row = 0; row < ui_vr_options_page_rows(page); row++)
|
||||
@@ -7467,10 +7718,23 @@ static void ui_vr_options_render(
|
||||
unused = !smooth;
|
||||
break;
|
||||
case _vr_option_vignette:
|
||||
label = "Vehicle comfort vignette";
|
||||
snprintf(value, sizeof(value), "%s", vr_vignette_names[ui_vr_options_nearest(
|
||||
label = "Vehicle vignette";
|
||||
ui_vr_options_vignette_text(value, sizeof(value), vr_vignettes[ui_vr_options_nearest(
|
||||
(real)config_real("vr.comfort_vignette"), vr_vignettes, NUMBEROF(vr_vignettes))]);
|
||||
break;
|
||||
case _vr_option_foot_vignette:
|
||||
label = "On-foot vignette";
|
||||
snprintf(value, sizeof(value), "%s", vr_foot_vignette_names[ui_vr_options_foot_vignette()]);
|
||||
/* (smooth turns alone, with snap turning: nothing to darken) */
|
||||
unused = ui_vr_options_foot_vignette() == 1 && !smooth;
|
||||
break;
|
||||
case _vr_option_foot_vignette_strength:
|
||||
label = "On-foot vignette strength";
|
||||
ui_vr_options_vignette_text(value, sizeof(value), vr_foot_vignette_strengths[ui_vr_options_nearest(
|
||||
(real)config_real("vr.foot_vignette_strength"), vr_foot_vignette_strengths,
|
||||
NUMBEROF(vr_foot_vignette_strengths))]);
|
||||
unused = ui_vr_options_foot_vignette() == 0;
|
||||
break;
|
||||
case _vr_option_wheel:
|
||||
label = "Warthog steering wheel";
|
||||
snprintf(value, sizeof(value), "%s", config_boolean("vr.vehicle_wheel") ? "On" : "Off (stick)");
|
||||
@@ -7537,12 +7801,26 @@ static void ui_vr_options_render(
|
||||
snprintf(value, sizeof(value), "%.0f%%", scale * 100.0f);
|
||||
break;
|
||||
}
|
||||
case _vr_option_buttons_reset:
|
||||
label = "Reset buttons to defaults";
|
||||
unused = ui_vr_options_buttons_default();
|
||||
break;
|
||||
case _vr_option_controls:
|
||||
label = "Controller buttons...";
|
||||
break;
|
||||
default:
|
||||
if (option >= _vr_option_button_first && option <= _vr_option_button_last)
|
||||
{
|
||||
short action = (short)(option - _vr_option_button_first);
|
||||
|
||||
label = vr_action_names[action];
|
||||
snprintf(value, sizeof(value), "%s", ui_vr_input_name((short)halo_vr_action_input(action), TRUE,
|
||||
NULL));
|
||||
}
|
||||
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)
|
||||
@@ -7556,7 +7834,26 @@ static void ui_vr_options_render(
|
||||
ui_join_code_draw_text(330, 512, y, shown, FALSE, 1, unused ? &dim_color : &text_color);
|
||||
}
|
||||
}
|
||||
if (page == 1)
|
||||
if (page == _vr_options_page_comfort)
|
||||
{
|
||||
/* (what the vignettes do) */
|
||||
ui_join_code_draw_text(128, 512, 248, "The vignette darkens the view from its edges while the", FALSE, 0,
|
||||
&dim_color);
|
||||
ui_join_code_draw_text(128, 512, 266, "game moves you: in a vehicle as it turns you; on foot, if", FALSE, 0,
|
||||
&dim_color);
|
||||
ui_join_code_draw_text(128, 512, 284, "on, as the sticks turn you smoothly (and move you).", FALSE, 0,
|
||||
&dim_color);
|
||||
}
|
||||
if (page == _vr_options_page_buttons)
|
||||
{
|
||||
/* (what the buttons' rows do) */
|
||||
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, 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)
|
||||
{
|
||||
/* (what the graphics rows do) */
|
||||
ui_join_code_draw_text(128, 512, 230, "Graphics: Medium and Low leave effects out (shadows,", FALSE, 0,
|
||||
|
||||
@@ -31,7 +31,14 @@ enum
|
||||
RASTERIZER_MAXIMUM_TRIANGLES_PER_TRIANGLE_BUFFER = 24576,
|
||||
RASTERIZER_MAXIMUM_DEBUG_PRIMITIVES = 131072,
|
||||
RASTERIZER_MAXIMUM_DEBUG_VERTICES = 393216,
|
||||
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 384,
|
||||
/* port: the Xbox's 384 groups a window (each a model's transparent part,
|
||||
a particle system, a contrail, a light volume, a window's or water's
|
||||
surface) are spent in games of 30 and more players, and whatever is
|
||||
submitted after the last is dropped, the structure's glass and water
|
||||
and the particles first (render_window submits the objects before
|
||||
them). Their indices are shorts, the pending flags a bit vector of
|
||||
this size: 2048 groups take 320 KB. */
|
||||
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS = 2048,
|
||||
RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS2 = 32,
|
||||
RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLES = 32768,
|
||||
RASTERIZER_MAXIMUM_DYNAMIC_TRIANGLE_BUFFERS = 1024,
|
||||
|
||||
@@ -150,6 +150,7 @@ typedef char transparent_geometry_group_cortana_hack_offset_assert[
|
||||
/* ---------- prototypes */
|
||||
static void rasterizer_sort_external(
|
||||
void);
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
@@ -162,6 +163,9 @@ static long transparent_geometry_group_count = 0;
|
||||
static long transparent_geometry_group_count2 = 0;
|
||||
static short *transparent_geometry_group_sorted_indices = NULL;
|
||||
static short transparent_geometry_next_group_sorted_index = 0;
|
||||
/* port: the most groups a window has had, logged as it passes the Xbox's
|
||||
384 and every 128 beyond (rasterizer.h) */
|
||||
static long transparent_geometry_group_count_logged = 384;
|
||||
|
||||
|
||||
/* ---------- public code */
|
||||
@@ -200,6 +204,12 @@ boolean rasterizer_transparent_geometry_initialize(
|
||||
void rasterizer_transparent_geometry_begin(
|
||||
void)
|
||||
{
|
||||
if (transparent_geometry_group_count>transparent_geometry_group_count_logged)
|
||||
{
|
||||
platform_log("rasterizer: %ld transparent geometry groups in a window (the Xbox had room for 384, the port %d)",
|
||||
transparent_geometry_group_count, RASTERIZER_MAXIMUM_TRANSPARENT_GEOMETRY_GROUPS);
|
||||
transparent_geometry_group_count_logged = (transparent_geometry_group_count/128 + 1)*128;
|
||||
}
|
||||
transparent_geometry_group_count = 0;
|
||||
transparent_geometry_next_group_sorted_index = 0;
|
||||
memset(transparent_geometry_group_pending_flags, 0,
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||