Compare commits
No files matched your search
@@ -0,0 +1,38 @@
|
||||
name: Problem report
|
||||
description: Something does not work as it should
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
|
||||
- type: dropdown
|
||||
id: headset
|
||||
attributes:
|
||||
label: Headset
|
||||
options:
|
||||
- Meta Quest 3
|
||||
- Meta Quest 3S
|
||||
- Steam Frame
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build
|
||||
attributes:
|
||||
label: Build number
|
||||
description: The release you installed, e.g. "build 180".
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What you did, what you expected and what happened instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: |
|
||||
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: How to install
|
||||
url: https://github.com/Cesarus85/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
|
||||
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
|
||||
@@ -237,6 +237,13 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
port/android
|
||||
port/third_party/extract-xiso
|
||||
tools
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: halo-*
|
||||
@@ -245,10 +252,18 @@ jobs:
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir release
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
# (Ringfall: Mjolnir VR publishes its Android builds only, under
|
||||
# its own name: Updater.java looks for them; the desktop ports are
|
||||
# built to keep them building)
|
||||
for config in debug release; do
|
||||
(cd "artifacts/halo-android-$config" &&
|
||||
zip -qr "$GITHUB_WORKSPACE/release/ringfall-mjolnir-vr-android-$config.zip" .)
|
||||
done
|
||||
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
|
||||
# bundle with the release APK, and the desktop file that fetches it
|
||||
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
|
||||
# the APK itself, for sideloading on the Quest (SideQuest, adb)
|
||||
cp artifacts/halo-android-release/app-release.apk release/RingfallMjolnirVR.apk
|
||||
ls -l release
|
||||
|
||||
- name: Publish
|
||||
@@ -260,10 +275,18 @@ jobs:
|
||||
run: |
|
||||
tag="build-$GITHUB_RUN_NUMBER"
|
||||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
|
||||
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
|
||||
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
|
||||
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
|
||||
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
||||
{
|
||||
printf '%s\n\n' "$subject"
|
||||
printf '**Ringfall: Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
|
||||
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
|
||||
printf '| Meta Quest 3 / 3S | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
|
||||
printf '| Steam Frame | `Install-Ringfall-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
|
||||
printf 'The other files are for the built-in updater and the Steam Frame installer.\n\n'
|
||||
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
|
||||
} > notes.md
|
||||
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
|
||||
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Keep the last five
|
||||
@@ -318,7 +341,7 @@ jobs:
|
||||
title="Build failed"; color=15548997
|
||||
fi
|
||||
latest="$SERVER/$REPOSITORY/releases/latest"
|
||||
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
|
||||
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||||
else
|
||||
|
||||
@@ -30,3 +30,8 @@ original/
|
||||
|
||||
# CI build outputs (tools/ci_build.py)
|
||||
dist/
|
||||
|
||||
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
|
||||
port/android/signing.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
@@ -1,140 +1,181 @@
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
<p align="center">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
|
||||
</p>
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
# Ringfall: Mjolnir VR
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
|
||||
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
|
||||
multiplayer in full 6DoF virtual reality, with your hands in the game.
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
Ringfall: Mjolnir VR runs on your headset by itself, without a PC. It
|
||||
brings no game files: it uses a disc image of **your own, purchased copy**
|
||||
of the game.
|
||||
|
||||
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
|
||||
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
> **Unofficial fan project.** Ringfall: Mjolnir VR is a free,
|
||||
> non-commercial fan project. It is not affiliated with, endorsed,
|
||||
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
|
||||
> Bungie. See [Legal](#legal).
|
||||
|
||||
## Download
|
||||
## Contents
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
- [What you get](#what-you-get)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
|
||||
- [Install on Steam Frame](#install-on-steam-frame)
|
||||
- [Playing](#playing)
|
||||
- [Problems and questions](#problems-and-questions)
|
||||
- [Credits](#credits)
|
||||
- [Legal](#legal)
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
|
||||
## What you get
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
problems.
|
||||
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
|
||||
the HUD, menus, scope and crosshair float as layers of their own.
|
||||
- **Your hands in the game**: the weapon sits in your hand and fires
|
||||
where you point it; arms and fingers follow your controllers;
|
||||
two-handed weapons, melee by swinging, grenades thrown with your hand,
|
||||
crouching and leaning with your body.
|
||||
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
|
||||
turrets with handles.
|
||||
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
|
||||
left-handed mode, buttons remapped freely, graphics presets, refresh
|
||||
rate and resolution, all in the in-game VR options (**Y**).
|
||||
- **Multiplayer**: the PC version's menus and server browser, join codes,
|
||||
password lobbies, co-op campaign and bots, up to 128 players;
|
||||
cross-play with the PC and phone builds of
|
||||
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
|
||||
|
||||
The game updates itself. At start-up it looks for a newer release, and asks
|
||||
if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](port/linux/README.md#updates).
|
||||
## What you need
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
|
||||
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
|
||||
original Xbox**, made from **a disc you own**. Any region and version
|
||||
works.
|
||||
|
||||
## Game data
|
||||
This project does not include, link to or help find game files. Please
|
||||
do not ask for or share disc images in issues or discussions; such posts
|
||||
are deleted.
|
||||
|
||||
The port does not include the game data. Download an Xbox disc image
|
||||
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
## Install on Meta Quest 3 / 3S
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
You install the game once with a computer (Windows, macOS or Linux); after
|
||||
that it updates itself.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](port/android/README.md).
|
||||
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
|
||||
app on your phone, open your headset's settings, then *Developer
|
||||
Mode* (Meta asks you to create a free developer account first).
|
||||
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
|
||||
"Advanced Installer") on your computer and connect the Quest with a
|
||||
USB cable. In the headset, allow USB debugging for this computer.
|
||||
3. **Download `RingfallMjolnirVR.apk`** from the
|
||||
[latest release](../../releases/latest) and drag it onto the SideQuest
|
||||
window. It installs the game.
|
||||
4. **Copy your disc image** to the Quest's `Download` folder: in
|
||||
SideQuest, open the file manager (the folder icon), go to `Download`
|
||||
and drag the `.iso` file in.
|
||||
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
|
||||
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
|
||||
your file in `Download`. The game copies what it needs (about 1.8 GB,
|
||||
a minute or two) and starts.
|
||||
|
||||
## Platforms
|
||||
You can delete the disc image from `Download` afterwards. When a new
|
||||
version comes out, the game asks you at start whether to update.
|
||||
|
||||
Each platform has its own instructions:
|
||||
## Install on Steam Frame
|
||||
|
||||
| Platform | Instructions |
|
||||
| --- | --- |
|
||||
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
|
||||
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
|
||||
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
|
||||
No computer and no keyboard needed.
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
|
||||
*Switch to Desktop*.
|
||||
2. **Put your disc image** in the `Downloads` folder (for example from a
|
||||
USB stick or memory card with the file manager, Dolphin).
|
||||
3. **Download `Install-Ringfall-Mjolnir-VR.desktop`** from the
|
||||
[latest release](../../releases/latest) with the web browser.
|
||||
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
|
||||
If you are asked whether to run it, choose to continue.
|
||||
5. **Answer the dialogs.** The installer finds your disc image, copies
|
||||
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
|
||||
library and offers to switch back to Gaming Mode.
|
||||
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
|
||||
|
||||
## Multiplayer
|
||||
To update, open the same file in Desktop Mode again; your settings,
|
||||
saves and game data stay. More about the Steam Frame:
|
||||
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
## Playing
|
||||
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](port/linux/NETCODE.md).
|
||||
- **Y** opens the VR options in the main and pause menus: turning,
|
||||
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
|
||||
*Quit the game* is on its last page.
|
||||
- In the menus, point with a controller and pull the trigger to click.
|
||||
- The controller layout is on the VR options' *Controller buttons* page.
|
||||
|
||||
## Build the game
|
||||
## Problems and questions
|
||||
|
||||
You do not need the Xbox SDK. The port supplies the SDK declarations that
|
||||
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
|
||||
Please open an [issue](../../issues) and include:
|
||||
|
||||
To build the game:
|
||||
- your headset and the build number of the release you installed;
|
||||
- what you did and what happened;
|
||||
- the game's log, `debug.txt`:
|
||||
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
|
||||
(SideQuest's file manager);
|
||||
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
|
||||
`logcat.txt` and `install.log` beside it).
|
||||
|
||||
1. Install Python and [ninja](https://ninja-build.org/).
|
||||
2. Install the tools for your platform. Refer to the README for the
|
||||
platform.
|
||||
3. In the root folder of the repository, enter `python configure.py`.
|
||||
4. Enter `ninja` with the target for the platform:
|
||||
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
|
||||
|
||||
| Target | Result |
|
||||
| --- | --- |
|
||||
| `ninja linux` | `build/linux/halo` |
|
||||
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
**Network use.** The game connects to the internet only for what you
|
||||
use: the update check (GitHub), online games (to other players
|
||||
directly), and join codes and lobby lists through public MQTT brokers
|
||||
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
|
||||
telemetry.
|
||||
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
## Credits
|
||||
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
Ringfall: Mjolnir VR stands on the work of others:
|
||||
|
||||
### Build options
|
||||
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
|
||||
Community Edition): the port of the decompiled game to Linux, Windows
|
||||
and Android, with its renderer, netcode, PC menus and multiplayer, that
|
||||
this project builds on and follows.
|
||||
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
|
||||
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
|
||||
by moistman42069 (CC0): the arm IK, the forearm's twist and the
|
||||
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
|
||||
left-handed mode and the graphics presets follow its ideas.
|
||||
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
|
||||
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
|
||||
trace markers) are used here.
|
||||
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
|
||||
image readers follow its format handling.
|
||||
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
Give these options to `configure.py`:
|
||||
Thank you all.
|
||||
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
## Building it
|
||||
|
||||
Without `--portable`, the Linux and Windows builds use all the instructions
|
||||
of the processor that builds them (`-march=native`). Such a build does not
|
||||
always start on a different computer.
|
||||
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
|
||||
and NDK needed). See [port/android/README.md](port/android/README.md) for
|
||||
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
|
||||
port it is built on.
|
||||
|
||||
### Optimization profiles
|
||||
## Legal
|
||||
|
||||
The builds use profiles of the game to optimize the code:
|
||||
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
|
||||
It is not affiliated with, endorsed, sponsored or approved by Microsoft
|
||||
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
|
||||
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
|
||||
here only to say which game this mod works with. Meta Quest is a trademark
|
||||
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
|
||||
Corporation.
|
||||
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
No game data, art, music or other content of the game is included or
|
||||
distributed. You need your own, legally purchased copy of the game. If
|
||||
you enjoy this mod, please support the creators of the original by buying
|
||||
their games.
|
||||
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
This project's own code is released under [CC0 1.0](LICENSE.md), as the
|
||||
decompilation it builds on. Third-party code and fonts keep their own
|
||||
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
@@ -0,0 +1,46 @@
|
||||
# Third-party notices
|
||||
|
||||
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
|
||||
It includes or builds on the following, under their own licenses; each
|
||||
license's full text is in the file named.
|
||||
|
||||
## Code
|
||||
|
||||
| Component | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) |
|
||||
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
|
||||
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
|
||||
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
|
||||
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
|
||||
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
|
||||
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
|
||||
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
|
||||
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
|
||||
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
|
||||
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
|
||||
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
|
||||
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
|
||||
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
|
||||
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
|
||||
|
||||
The disc image readers include this acknowledgement, as extract-xiso's
|
||||
license asks: *This product includes software developed by in
|
||||
<in@fishtank.com>.*
|
||||
|
||||
## Fonts
|
||||
|
||||
| Font | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
|
||||
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
|
||||
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
|
||||
|
||||
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
|
||||
each font is used.
|
||||
|
||||
## Not included
|
||||
|
||||
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
|
||||
executables) is part of this repository or its releases. The game reads
|
||||
them from the player's own disc image.
|
||||
@@ -0,0 +1,145 @@
|
||||
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
|
||||
> the port Ringfall: Mjolnir VR is built on, kept for its build and
|
||||
> development notes. Its download links are OpenCE's. For the VR mod, see
|
||||
> the [main README](../README.md).
|
||||
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
|
||||
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
|
||||
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
|
||||
## Download
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
problems.
|
||||
|
||||
The game updates itself. At start-up it looks for a newer release, and asks
|
||||
if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](../port/linux/README.md#updates).
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
|
||||
## Game data
|
||||
|
||||
The port does not include the game data. It needs an Xbox disc image
|
||||
(`.xiso` or `.iso`) made from your own copy of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](../port/android/README.md).
|
||||
|
||||
## Platforms
|
||||
|
||||
Each platform has its own instructions:
|
||||
|
||||
| Platform | Instructions |
|
||||
| --- | --- |
|
||||
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](../port/linux/README.md) |
|
||||
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](../port/windows/README.md) |
|
||||
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](../port/android/README.md) |
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](../port/linux/NETCODE.md).
|
||||
|
||||
## Build the game
|
||||
|
||||
You do not need the Xbox SDK. The port supplies the SDK declarations that
|
||||
the game uses. Refer to [port/include/xdk](../port/include/xdk/README.md).
|
||||
|
||||
To build the game:
|
||||
|
||||
1. Install Python and [ninja](https://ninja-build.org/).
|
||||
2. Install the tools for your platform. Refer to the README for the
|
||||
platform.
|
||||
3. In the root folder of the repository, enter `python configure.py`.
|
||||
4. Enter `ninja` with the target for the platform:
|
||||
|
||||
| Target | Result |
|
||||
| --- | --- |
|
||||
| `ninja linux` | `build/linux/halo` |
|
||||
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
|
||||
### Build options
|
||||
|
||||
Give these options to `configure.py`:
|
||||
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
|
||||
Without `--portable`, the Linux and Windows builds use all the instructions
|
||||
of the processor that builds them (`-march=native`). Such a build does not
|
||||
always start on a different computer.
|
||||
|
||||
### Optimization profiles
|
||||
|
||||
The builds use profiles of the game to optimize the code:
|
||||
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Android
|
||||
|
||||
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
|
||||
its Java classes keep the decompilation's package, `com.halo.decomp`).
|
||||
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
|
||||
`ninja android_apk` makes an app from it:
|
||||
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
@@ -41,8 +43,8 @@ ninja) and these items:
|
||||
## Game data
|
||||
|
||||
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
|
||||
`.iso`) of any version of the game. The app extracts `maps/` from the disc
|
||||
image. The app keeps the data in `/sdcard/Android/data/com.halo.decomp/files`.
|
||||
`.iso`) made from your own copy of the game, any version. The app extracts `maps/` from the disc
|
||||
image. The app keeps the data in `/sdcard/Android/data/io.github.cesarus85.ringfall/files`.
|
||||
|
||||
To install the data with the app:
|
||||
|
||||
@@ -57,16 +59,16 @@ To install the data with the app:
|
||||
To install the data from a computer:
|
||||
|
||||
1. Start the app one time. The app makes its folders.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
|
||||
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
|
||||
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
|
||||
| --- | --- |
|
||||
| Saved games (`z:\` and `u:\`) | `save` |
|
||||
| Log | `debug.txt` |
|
||||
| Settings | `config.toml` |
|
||||
|
||||
To make a copy of the saved games, enter
|
||||
`adb pull /sdcard/Android/data/com.halo.decomp/files/save`.
|
||||
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
|
||||
|
||||
## Controls
|
||||
|
||||
@@ -100,9 +102,9 @@ not accept touch input.
|
||||
The settings are in `config.toml` in the data folder of the app. To change
|
||||
them:
|
||||
|
||||
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
|
||||
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
|
||||
2. Change the file.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
|
||||
At the first start, the game writes the file with the default values. To
|
||||
get the default values again, delete the file.
|
||||
@@ -148,14 +150,22 @@ 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.
|
||||
|
||||
To install over the previous version, each build must have the same
|
||||
signature. GitHub Actions signs each build with the key in the
|
||||
`ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the
|
||||
repository. If you installed a build that has a different signature, remove
|
||||
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/ringfall-release.jks
|
||||
password=<the keystore's password>
|
||||
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
|
||||
that build before you install a new build. Removing the app deletes its data
|
||||
folder: first make a copy of `maps/` and `save/`.
|
||||
|
||||
@@ -182,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 |
|
||||
@@ -230,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
|
||||
|
||||
@@ -4,6 +4,43 @@ The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
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 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-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 "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 `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/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, 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.
|
||||
|
||||
## The device
|
||||
|
||||
- Steam Settings > System > Enable Developer Mode; then start "Lepton
|
||||
@@ -15,7 +52,7 @@ What differs is found at run time, not by a build of its own.
|
||||
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.
|
||||
@@ -33,7 +70,7 @@ What differs is found at run time, not by a build of its own.
|
||||
| # | 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 |
|
||||
@@ -43,10 +80,10 @@ What differs is found at run time, not by a build of its own.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -61,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.
|
||||
|
||||
@@ -75,13 +112,15 @@ 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/HaloCE` holds `maps/ui.map` and is writable, the
|
||||
launcher and the game take it as their data folder (config.toml, saves and
|
||||
debug.txt go there too). Once:
|
||||
`/home/steamos/Documents` can be written, the launcher makes
|
||||
`/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 |
|
||||
@@ -25,6 +25,8 @@ named where code is taken over.
|
||||
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
|
||||
| Multiplayer | Up to date with OpenCE (network version 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
|
||||
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
|
||||
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
|
||||
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
|
||||
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
|
||||
|
||||
## Next, in order
|
||||
@@ -33,11 +35,182 @@ 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 | |
|
||||
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **done** (in `main` 2026-10-06; Quest 3 good): buttons remapped, finer turning and vignette choices; the flashlight by tapping the head taken out (did not work) |
|
||||
| 3b | The whole body: torso from head and hands, legs and feet following in the room; full / arms only / hands only | |
|
||||
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
|
||||
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
|
||||
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | |
|
||||
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Sprint by arm swing still open |
|
||||
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
|
||||
|
||||
## Step 2, first part (done)
|
||||
|
||||
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
|
||||
mirrored first-person model, its graphics switches.
|
||||
|
||||
Left-handed (`vr.handedness = "left"`):
|
||||
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
|
||||
scope disc, turret aim. The first-person model is posed as the right
|
||||
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
|
||||
mirrored back; a model whose node matrices mirror has its triangles'
|
||||
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
|
||||
arms reach to the other shoulders, the foregrip is gripped with the
|
||||
right hand, the right hand's fingers follow its controller.
|
||||
- Grenades held and thrown with the right trigger and hand; melee by the
|
||||
left grip (a swing of either hand, as before); the motion tracker on the
|
||||
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
|
||||
the weapon hand.
|
||||
- The left trigger fires, the right one throws; at the Warthog's wheel the
|
||||
left trigger accelerates, the right one brakes.
|
||||
- With `vr.mirror_controls` (default): the right stick moves, the left
|
||||
turns and steers; their clicks, the bumpers and the face buttons change
|
||||
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
|
||||
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
|
||||
mirror image, down jump, left light, right action, up switch weapon,
|
||||
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
|
||||
laser pointer works from either hand as before.
|
||||
- Other machines see the shots leave the left hand (the network message
|
||||
is unchanged: aim and muzzle from the camera).
|
||||
- The assault rifle's ammo counter and compass read the right way round
|
||||
on the mirrored model (2026-10-06): their geometry stays mirrored, each
|
||||
digit's bitmap is drawn turned over in u and the digits' order across
|
||||
the display swapped (numeric shaders drawn mirrored,
|
||||
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
|
||||
with `debug.mirror_first_person` (the first-person weapon mirrored off
|
||||
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
|
||||
(2026-10-06).
|
||||
- Kept as they are (accepted 2026-10-06): the small static icons under the
|
||||
counter (round and "x") stay mirror images (a texture atlas, not
|
||||
numeric). The game's prompts still name the Xbox's buttons.
|
||||
- Settings kept: on Android `config.toml` is written beside itself and
|
||||
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
|
||||
the app could not open for writing) no longer loses the VR options'
|
||||
changes at a restart.
|
||||
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
|
||||
controls mirrored and not, counter, presets, refresh rates, resolution,
|
||||
settings kept over a restart).
|
||||
|
||||
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
|
||||
on the game's own switches:
|
||||
|
||||
| Preset | Leaves out |
|
||||
| --- | --- |
|
||||
| `"high"` (default) | nothing: as before |
|
||||
| `"medium"` | object shadows, environment reflections, lens flares |
|
||||
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
|
||||
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
|
||||
|
||||
A client in another's game keeps grass, lens flares and the fog layer as
|
||||
the host's rules have them (`cheats.c`). Shadows, specular passes,
|
||||
reflections and detail objects are draws (CPU, which the Frame's GL
|
||||
driver is short of); bump maps, fog, camouflage and per-pixel lighting
|
||||
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); 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.
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.handedness` | `"right"` (default), `"left"` |
|
||||
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
|
||||
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
|
||||
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
|
||||
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
|
||||
launcher; weapon held right in the left hand, arms from the right
|
||||
shoulders, nothing inside out (faces culled the wrong way would show
|
||||
the inside of the gun or the glove).
|
||||
2. Two-handed grip with the right hand; grenades in the right hand (held
|
||||
and thrown); melee by grip and by swing; motion tracker on the right
|
||||
wrist.
|
||||
3. Buttons and sticks mirrored on the Touch and the Frame controllers
|
||||
(every function reachable), then `Mirrored controls: Off`.
|
||||
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
|
||||
menus and the laser pointer; a network game (shots from the left hand
|
||||
on the other machine).
|
||||
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
|
||||
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 (done)
|
||||
|
||||
In `main` 2026-10-06; tried on the Quest 3 (with the rename): good. The flashlight by tapping the head (after HaloCE-Quest-VR)
|
||||
did not work in the headset and was taken out again; the button stays.
|
||||
The VR
|
||||
options (Y) have four pages: PLAY ("Weapon hand", "Mirrored controls",
|
||||
laser, crosshair, motion tracker, body, fingers, the Warthog's wheel),
|
||||
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons,
|
||||
"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)
|
||||
|
||||
@@ -97,10 +270,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`)
|
||||
|
||||
|
||||
@@ -13,16 +13,30 @@ def buildNumberText = System.getenv("HALO_BUILD_NUMBER") ?: "0"
|
||||
def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
|
||||
|
||||
// the workflow's signing key (.github/workflows/build.yml): every build of
|
||||
// main signed with it can install over the one before; without it, the
|
||||
// main signed with it can install over the one before; on a computer that
|
||||
// builds for its own devices, the same key from port/android/signing.properties
|
||||
// (not in the repository: storeFile, password, keyAlias), so that its
|
||||
// builds and the releases install over each other; without either, the
|
||||
// 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") ?: "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", "ringfall")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.halo.decomp"
|
||||
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)
|
||||
@@ -60,9 +74,9 @@ android {
|
||||
if (signingKey) {
|
||||
halo {
|
||||
storeFile file(signingKey)
|
||||
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
|
||||
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
storePassword signingPassword
|
||||
keyAlias signingAlias
|
||||
keyPassword signingPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -49,10 +49,15 @@ public class LauncherActivity extends Activity {
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
|
||||
// the same choice as host_main.c's)
|
||||
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
|
||||
dataRoot = new File(SHARED_DATA_ROOT);
|
||||
// Documents instead, whenever they can be written, so that a disc
|
||||
// image imported here stays too (port/android/STEAM_FRAME.md; the
|
||||
// same choice as host_main.c's)
|
||||
File shared = new File(SHARED_DATA_ROOT);
|
||||
if (shared.getParentFile().canWrite() || shared.canWrite()) {
|
||||
shared.mkdirs();
|
||||
if (shared.canWrite())
|
||||
dataRoot = shared;
|
||||
}
|
||||
// created by the app, so that files pushed into it with adb stay
|
||||
// readable (a directory adb creates there belongs to the shell user)
|
||||
if (dataRoot != null)
|
||||
@@ -142,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 |
@@ -135,6 +135,11 @@ into a layer's image, opaque */
|
||||
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
void host_xr_set_vignette(float strength);
|
||||
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
|
||||
it lets none be chosen), and the one it runs at (0: unknown); and asking
|
||||
for the highest offered at or below wanted (0: the highest) */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current);
|
||||
void host_xr_set_refresh_rate(float wanted);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -273,9 +273,9 @@ static void *game_main(void *unused)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
|
||||
as LauncherActivity's) */
|
||||
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
instead, whenever they can be written (port/android/STEAM_FRAME.md; the
|
||||
same choice as LauncherActivity's, which makes the folder) */
|
||||
if (access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
@@ -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");
|
||||
|
||||
@@ -120,10 +120,10 @@ static struct
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
/* where the head and the left hand are this frame (the wrist layer) */
|
||||
/* where the head and the hands are this frame (the wrist layer) */
|
||||
XrPosef head;
|
||||
XrPosef left_grip;
|
||||
int left_grip_valid;
|
||||
XrPosef grips[HALO_XR_HAND_COUNT];
|
||||
int grips_valid[HALO_XR_HAND_COUNT];
|
||||
|
||||
int has_performance_settings;
|
||||
/* (XR_META_performance_metrics: the compositor's counts, in the log,
|
||||
@@ -946,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
{
|
||||
out->flags = flags_from_location(location.locationFlags);
|
||||
pose_from_xr(&out->grip, &location.pose);
|
||||
if (hand == HALO_XR_HAND_LEFT)
|
||||
{
|
||||
xr.left_grip = location.pose;
|
||||
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
xr.grips[hand] = location.pose;
|
||||
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
location.locationFlags = 0;
|
||||
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
|
||||
@@ -1048,7 +1045,9 @@ On the back of the left wrist, like a watch: behind the palm, where the
|
||||
grip pose is, and out of the back of the hand. In the left grip pose -Z
|
||||
runs to the fingers, +Y up the handle and -X out of the back of the hand;
|
||||
the layer faces -X with its top towards the elbow, as the wrist turned to
|
||||
the eyes reads it. Shown only while it is. */
|
||||
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
|
||||
right wrist, as the mirror image: there +X runs out of the back of the
|
||||
hand. */
|
||||
|
||||
#define WRIST_IMAGE_SIZE 512
|
||||
#define CROSSHAIR_IMAGE_SIZE 256
|
||||
@@ -1088,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
|
||||
|
||||
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
|
||||
{
|
||||
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z */
|
||||
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
|
||||
WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z; in the
|
||||
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
|
||||
way round: its x turned back) */
|
||||
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
|
||||
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
|
||||
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
|
||||
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
|
||||
/* clockwise as read: about the layer's normal, negatively */
|
||||
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
/* clockwise as read: about the layer's normal, negatively (the mirror
|
||||
image's the other way) */
|
||||
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
|
||||
XrQuaternionf unturned;
|
||||
XrVector3f moved, normal, to_eyes;
|
||||
float distance;
|
||||
|
||||
if (!xr.left_grip_valid)
|
||||
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
|
||||
return 0;
|
||||
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
|
||||
pose->position.x = xr.left_grip.position.x + moved.x;
|
||||
pose->position.y = xr.left_grip.position.y + moved.y;
|
||||
pose->position.z = xr.left_grip.position.z + moved.z;
|
||||
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
|
||||
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
|
||||
pose->position.x = grip->position.x + moved.x;
|
||||
pose->position.y = grip->position.y + moved.y;
|
||||
pose->position.z = grip->position.z + moved.z;
|
||||
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
|
||||
quaternion_multiply(&unturned, &turn, &pose->orientation);
|
||||
|
||||
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
|
||||
@@ -1227,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;
|
||||
@@ -1555,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
|
||||
pose_from_xr(&frame->head, &head.pose);
|
||||
xr.head = head.pose;
|
||||
}
|
||||
xr.left_grip_valid = 0;
|
||||
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
|
||||
if (xr.focused)
|
||||
hands_read(frame);
|
||||
return 1;
|
||||
@@ -2047,6 +2052,34 @@ void host_xr_set_vignette(float strength)
|
||||
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
|
||||
}
|
||||
|
||||
/* the VR options' refresh rates: those the runtime offers (none where it
|
||||
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
|
||||
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
float offered[16];
|
||||
uint32_t count = 0, index;
|
||||
|
||||
*current = 0.0f;
|
||||
if (!xr.initialized)
|
||||
return 0;
|
||||
if (xr.has_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, current);
|
||||
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
|
||||
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
|
||||
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
|
||||
return 0;
|
||||
for (index = 0; index < count && (int)index < maximum; index++)
|
||||
rates[index] = offered[index];
|
||||
return (int)index;
|
||||
}
|
||||
|
||||
void host_xr_set_refresh_rate(float wanted)
|
||||
{
|
||||
if (xr.initialized)
|
||||
refresh_rate_choose(wanted);
|
||||
}
|
||||
|
||||
void host_xr_haptic(int hand, float amplitude, float seconds)
|
||||
{
|
||||
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
|
||||
@@ -2134,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());
|
||||
|
||||
@@ -61,6 +61,8 @@ host_xr_end_frame
|
||||
host_xr_haptic
|
||||
host_xr_set_vignette
|
||||
host_xr_copy_layer_array
|
||||
host_xr_refresh_rates
|
||||
host_xr_set_refresh_rate
|
||||
host_android_path
|
||||
|
||||
host_errno
|
||||
@@ -24,7 +24,7 @@ enum
|
||||
HALO_XR_LAYER_EYE_LEFT = 0,
|
||||
HALO_XR_LAYER_EYE_RIGHT = 1,
|
||||
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
|
||||
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
|
||||
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
|
||||
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
|
||||
@@ -155,15 +155,18 @@ struct halo_xr_submit
|
||||
float overlay_width; /* metres; the height follows from 4:3 */
|
||||
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
|
||||
struct halo_xr_pose screen;
|
||||
/* the wrist layer was filled: shown on the back of the left wrist,
|
||||
this wide (metres), while the wrist is turned to the eyes */
|
||||
/* the wrist layer was filled: shown on the back of the left wrist (of
|
||||
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
|
||||
(metres), while the wrist is turned to the eyes */
|
||||
int wrist;
|
||||
float wrist_size;
|
||||
/* its placement as the wrist turned to the eyes reads it: turned this
|
||||
many degrees clockwise, and moved this far right and down (metres) */
|
||||
/* its placement as the left wrist turned to the eyes reads it: turned
|
||||
this many degrees clockwise, and moved this far right and down (metres;
|
||||
on the right wrist, the mirror image) */
|
||||
float wrist_turn;
|
||||
float wrist_right;
|
||||
float wrist_down;
|
||||
int wrist_hand;
|
||||
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
|
||||
space, facing back along the weapon */
|
||||
int scope;
|
||||
|
||||
|
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 |
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
# 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)"
|
||||
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
|
||||
STEAM_DIR="$HOME/.local/share/Steam"
|
||||
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
|
||||
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"
|
||||
|
||||
# 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()
|
||||
{
|
||||
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# yes/no; unattended: the default given ($2, "yes" or "no")
|
||||
ask()
|
||||
{
|
||||
if unattended; then [ "$2" = yes ]; return; fi
|
||||
zenity --question --title="$TITLE" --width=460 --text="$1"
|
||||
}
|
||||
|
||||
# the disc image the player chooses
|
||||
choose_image()
|
||||
{
|
||||
if unattended; then return 1; fi
|
||||
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 (the game's image is a few GB;
|
||||
# anything under 500 MB is not one)
|
||||
find_images()
|
||||
{
|
||||
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
|
||||
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
|
||||
sort -rn | cut -f2-
|
||||
}
|
||||
|
||||
extract_maps()
|
||||
{
|
||||
local image="$1" log
|
||||
log="$(mktemp)"
|
||||
if unattended; then
|
||||
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" "$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]}
|
||||
fi
|
||||
if [ "$status" != 0 ]; then
|
||||
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
|
||||
rm -f "$log"
|
||||
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
|
||||
fi
|
||||
rm -f "$log"
|
||||
}
|
||||
|
||||
# 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 - "$1" "$2" <<'PYTHON'
|
||||
import struct, sys
|
||||
|
||||
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):
|
||||
items = []
|
||||
while True:
|
||||
kind = data[at]; at += 1
|
||||
if kind == 0x08:
|
||||
return items, at
|
||||
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:
|
||||
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
|
||||
elif kind == 0x02:
|
||||
value = struct.unpack_from("<I", data, at)[0]; at += 4
|
||||
else:
|
||||
raise ValueError("unknown VDF type %d" % kind)
|
||||
items.append((key, value))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# 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 "$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
|
||||
|
||||
# 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
|
||||
|
||||
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 "$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/$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 "$GAME_DIR/maps/ui.map" ]; then
|
||||
image="${RINGFALL_IMAGE:-}"
|
||||
if [ -z "$image" ]; then
|
||||
mapfile -t images < <(find_images)
|
||||
for candidate in "${images[@]}"; do
|
||||
if ask "Use this disc image?\n\n$candidate" yes; then
|
||||
image="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -z "$image" ]; then
|
||||
if [ ${#images[@]} = 0 ]; then
|
||||
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
|
||||
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 "$GAME_DIR/maps"
|
||||
image_note="\n\nThe disc image itself is no longer needed:\n$image"
|
||||
fi
|
||||
|
||||
# --- Steam
|
||||
|
||||
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 $GAME_DIR"
|
||||
exit 0
|
||||
fi
|
||||
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)."
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
# 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 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/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
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
|
||||
disc's ".iso") on the Steam Frame's SteamOS side, for its installer
|
||||
(steam_frame_install.sh). It reads the image as the app's own import does
|
||||
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
|
||||
the desktop games' is port/linux/src/xiso.c).
|
||||
|
||||
steam_frame_xiso.py <image> <destination>
|
||||
|
||||
writes <destination>/maps (through maps.partial, so an interrupted copy
|
||||
never leaves maps/ui.map behind) and prints its progress for zenity
|
||||
--progress: a percentage on a line of its own, and "# <text>" lines. On
|
||||
failure it prints "error: <reason the player can act on>" to stderr and
|
||||
exits 1.
|
||||
|
||||
The image's file system is XDVDFS, read as extract-xiso does
|
||||
(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format
|
||||
handling this follows; its license is below and in
|
||||
port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume
|
||||
descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and
|
||||
gives the root directory's sector and size; and directories whose entries
|
||||
form a binary tree (each entry: left and right subtree offsets in 4-byte
|
||||
units, the start sector, the size, attributes, the name's length and the
|
||||
name). Images made from a whole disc put the game partition further in, at
|
||||
one of the offsets below.
|
||||
|
||||
Some parts of this code are copyright in@fishtank.com. This product
|
||||
includes software developed by in <in@fishtank.com>.
|
||||
|
||||
Copyright (c) 2003 in <in@fishtank.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
|
||||
This product includes software developed by in <in@fishtank.com>.
|
||||
|
||||
4. Neither the name of "in" nor the email address "in@fishtank.com"
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
SECTOR_SIZE = 2048
|
||||
VOLUME_DESCRIPTOR_OFFSET = 0x10000
|
||||
ENTRY_HEADER_SIZE = 14
|
||||
ATTRIBUTE_DIRECTORY = 0x10
|
||||
# directory tables are a few sectors; anything much larger is not one
|
||||
MAXIMUM_DIRECTORY_SIZE = 4 << 20
|
||||
MAXIMUM_FILES = 256
|
||||
COPY_BUFFER_SIZE = 1 << 20
|
||||
VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA"
|
||||
# where the game partition starts: a plain image, and whole-disc images
|
||||
# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets)
|
||||
PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000)
|
||||
|
||||
|
||||
class ExtractError(Exception):
|
||||
"""a reason the player can act on"""
|
||||
|
||||
|
||||
def read_at(image, offset, size):
|
||||
image.seek(offset)
|
||||
data = image.read(size)
|
||||
if len(data) != size:
|
||||
raise ExtractError("Could not read the disc image (is it complete?).")
|
||||
return data
|
||||
|
||||
|
||||
def find_volume(image):
|
||||
"""the game partition's offset, and the root directory's sector and size"""
|
||||
for offset in PARTITION_OFFSETS:
|
||||
try:
|
||||
descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE)
|
||||
except ExtractError:
|
||||
continue
|
||||
if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC:
|
||||
continue
|
||||
sector, size = struct.unpack_from("<II", descriptor, 20)
|
||||
return offset, sector, size
|
||||
raise ExtractError("This is not an Xbox disc image.")
|
||||
|
||||
|
||||
def read_directory(image, partition, sector, size, what):
|
||||
if size <= 0 or size > MAXIMUM_DIRECTORY_SIZE:
|
||||
raise ExtractError(what)
|
||||
return read_at(image, partition + sector * SECTOR_SIZE, size)
|
||||
|
||||
|
||||
def walk(table, offset, depth, directories, out, visited):
|
||||
"""collects the entries (name, sector, size) of the subtree at offset
|
||||
(4-byte units), files or directories as asked"""
|
||||
at = offset * 4
|
||||
# a malformed table must not loop or run off its end
|
||||
visited[0] += 1
|
||||
if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table):
|
||||
return
|
||||
left, right, sector, size, attributes, name_length = struct.unpack_from("<HHIIBB", table, at)
|
||||
# 0xFFFF: padding, an empty directory
|
||||
if left == 0xFFFF:
|
||||
return
|
||||
if left:
|
||||
walk(table, left, depth + 1, directories, out, visited)
|
||||
is_directory = bool(attributes & ATTRIBUTE_DIRECTORY)
|
||||
if (at + ENTRY_HEADER_SIZE + name_length <= len(table) and name_length > 0
|
||||
and is_directory == directories and len(out) < MAXIMUM_FILES):
|
||||
name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1")
|
||||
# (as extract-xiso refuses them: no name may leave the folder)
|
||||
if name not in (".", "..") and "/" not in name and "\\" not in name:
|
||||
out.append((name, sector, size))
|
||||
if right:
|
||||
walk(table, right, depth + 1, directories, out, visited)
|
||||
|
||||
|
||||
def extract(image_path, destination):
|
||||
with open(image_path, "rb") as image:
|
||||
partition, root_sector, root_size = find_volume(image)
|
||||
|
||||
# the root's maps folder
|
||||
table = read_directory(image, partition, root_sector, root_size,
|
||||
"The disc image's file system is damaged.")
|
||||
directories = []
|
||||
walk(table, 0, 0, True, directories, [0])
|
||||
maps = [entry for entry in directories if entry[0].lower() == "maps"]
|
||||
if not maps:
|
||||
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
|
||||
|
||||
# its files
|
||||
table = read_directory(image, partition, maps[-1][1], maps[-1][2],
|
||||
"The disc image's maps folder is damaged.")
|
||||
files = []
|
||||
walk(table, 0, 0, False, files, [0])
|
||||
if not any(name.lower() == "ui.map" for name, _, _ in files):
|
||||
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
|
||||
total = sum(size for _, _, size in files) or 1
|
||||
|
||||
partial = os.path.join(destination, "maps.partial")
|
||||
finished = os.path.join(destination, "maps")
|
||||
try:
|
||||
os.makedirs(partial, exist_ok=True)
|
||||
except OSError:
|
||||
raise ExtractError("Could not create %s." % partial)
|
||||
done = 0
|
||||
shown = -1
|
||||
for name, sector, size in files:
|
||||
path = os.path.join(partial, name)
|
||||
offset = partition + sector * SECTOR_SIZE
|
||||
remaining = size
|
||||
print("# Copying maps/%s" % name, flush=True)
|
||||
try:
|
||||
with open(path, "wb") as out:
|
||||
while remaining > 0:
|
||||
count = min(remaining, COPY_BUFFER_SIZE)
|
||||
out.write(read_at(image, offset, count))
|
||||
offset += count
|
||||
remaining -= count
|
||||
done += count
|
||||
# (zenity closes at 100: that is left to the caller)
|
||||
percent = min(99, done * 100 // total)
|
||||
if percent != shown:
|
||||
print(percent, flush=True)
|
||||
shown = percent
|
||||
except ExtractError:
|
||||
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
|
||||
except OSError:
|
||||
raise ExtractError("Could not write %s (is the storage full?)." % path)
|
||||
|
||||
# (the maps folder may be there, empty)
|
||||
try:
|
||||
os.makedirs(finished, exist_ok=True)
|
||||
for name, _, _ in files:
|
||||
os.replace(os.path.join(partial, name), os.path.join(finished, name))
|
||||
os.rmdir(partial)
|
||||
except OSError as error:
|
||||
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
extract(sys.argv[1], sys.argv[2])
|
||||
except ExtractError as error:
|
||||
print("error: %s" % error, file=sys.stderr)
|
||||
return 1
|
||||
except OSError as error:
|
||||
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -3,19 +3,22 @@ NETWORK_VR.C
|
||||
|
||||
VR players' hands for the other machines: an extension of the distributed
|
||||
netcode (port/linux/NETCODE.md, network_distributed.c). A VR player's
|
||||
weapon aims where the right controller points and fires from the hand, and
|
||||
the left hand holds a grenade and throws it its own way (port/linux/src/
|
||||
vr.c); the input the netcode relays has only where the head looks, so the
|
||||
other machines would show that player aiming and throwing where they look.
|
||||
weapon aims where the weapon hand's controller points and fires from the
|
||||
hand, and the free hand holds a grenade and throws it its own way
|
||||
(port/linux/src/vr.c; the right hand and the left, or left-handed the other
|
||||
way round); the input the netcode relays has only where the head looks, so
|
||||
the other machines would show that player aiming and throwing where they
|
||||
look.
|
||||
|
||||
Every tick, a machine with a VR player sends the host that player's hands
|
||||
(_distributed_message_vr_poses, unreliable): where the weapon aims, where
|
||||
its muzzle is from the unit's camera, whether the left hand holds a
|
||||
grenade, and the last throw (from where, which way, how hard) for a second
|
||||
after it. The host keeps them, its own VR players' too, and sends each
|
||||
client those of the players of other machines. A machine drives a VR
|
||||
player of another (players.c, weapons.c, units.c) as long as their hands
|
||||
keep coming, else as before.
|
||||
its muzzle is from the unit's camera (whichever hand holds it: the others
|
||||
see shots leave the left hand of a left-handed player), whether the free
|
||||
hand holds a grenade, and the last throw (from where, which way, how hard)
|
||||
for a second after it. The host keeps them, its own VR players' too, and
|
||||
sends each client those of the players of other machines. A machine drives
|
||||
a VR player of another (players.c, weapons.c, units.c) as long as their
|
||||
hands keep coming, else as before.
|
||||
|
||||
What the hands decide is only how the game looks: damage is the shooter's
|
||||
report, which the host checks as any other (network_damage.c).
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
VR_GRAPHICS.C
|
||||
|
||||
The VR graphics presets (vr.graphics, the VR options' "Graphics" row): which
|
||||
of the game's costlier effects are drawn, for headsets with little time to
|
||||
spare (the Steam Frame spends most of its frame in its GL driver: fewer
|
||||
draws help it most; the Quest 3 holds 90 Hz with all of them).
|
||||
|
||||
- "high", the default: everything, as before;
|
||||
- "medium": no object shadows, environment reflections or lens flares;
|
||||
- "low": nor specular light, bump maps, grass and other detail objects,
|
||||
the screen's fog layer, contrails, weather or camouflage's second pass;
|
||||
- "max": "high", the models lit for each pixel and the shadows drawn at
|
||||
512x512 (d3d8_gl.c asks halo_vr_graphics).
|
||||
|
||||
Each effect is a switch the game has already: the rasterizer's console
|
||||
variables (rasterizer_debug_options) and render.c's effect switches, after
|
||||
HaloCE-Quest-VR's vr_graphics.c (CC0). Scripts and the console set some of
|
||||
them (render_effects turns particles off in some cutscenes): a switch is
|
||||
drawn only while both the game and the preset want it. A change the game
|
||||
made since the last frame is taken as its wish, kept and combined with the
|
||||
preset's. Playing in another's game, a client draws grass, lens flares and
|
||||
the screen's fog as the host's rules have it (cheats.c), whatever the
|
||||
preset; the reductions it does keep there, cheats.c leaves alone
|
||||
(vr_graphics_reduces).
|
||||
|
||||
What each costs: shadows (each object's map, blur and projection: draws),
|
||||
specular light and reflections (passes of the level's geometry: draws and
|
||||
fill), detail objects (many small draws), lens flares (their visibility
|
||||
tests), contrails and weather (particles), bump maps, the fog layer and
|
||||
camouflage's pass (the GPU's fill).
|
||||
*/
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
#include "cseries.h"
|
||||
#include "rasterizer/rasterizer_console_vars.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
/* source/render/render.c, render_objects.c; source/effects/
|
||||
weather_particle_systems.c */
|
||||
extern boolean render_shadows;
|
||||
extern boolean render_contrails_enabled;
|
||||
extern boolean render_weather_particle_systems_enabled;
|
||||
extern boolean weather;
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* an effect: whether each preset draws it (low, medium; high and max draw
|
||||
everything), whether a client in another's game draws it anyway, and the
|
||||
game's switches it governs */
|
||||
struct vr_graphics_effect
|
||||
{
|
||||
char const *name;
|
||||
boolean preset[2];
|
||||
boolean host_rules;
|
||||
boolean *switches[2];
|
||||
};
|
||||
|
||||
static struct vr_graphics_effect vr_graphics_effects[] =
|
||||
{
|
||||
{ "shadows", { FALSE, FALSE }, FALSE,
|
||||
{ &render_shadows, &rasterizer_debug_options.draw_environment_shadows } },
|
||||
{ "specular", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.draw_environment_specular_lights,
|
||||
&rasterizer_debug_options.draw_environment_specular_lightmaps } },
|
||||
{ "reflections", { FALSE, FALSE }, FALSE,
|
||||
{ &rasterizer_debug_options.draw_environment_reflections, NULL } },
|
||||
{ "bump maps", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.bump_mapping_enabled, NULL } },
|
||||
{ "detail objects", { FALSE, TRUE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_detail_objects, NULL } },
|
||||
{ "lens flares", { FALSE, FALSE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_lens_flares, NULL } },
|
||||
{ "screen fog", { FALSE, TRUE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_environment_fog_screen, NULL } },
|
||||
{ "contrails", { FALSE, TRUE }, FALSE,
|
||||
{ &render_contrails_enabled, NULL } },
|
||||
{ "weather", { FALSE, TRUE }, FALSE,
|
||||
{ &weather, &render_weather_particle_systems_enabled } },
|
||||
{ "camouflage pass", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.active_camouflage_multipass_enabled, NULL } },
|
||||
};
|
||||
|
||||
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
|
||||
#define VR_GRAPHICS_SWITCHES (sizeof(vr_graphics_effects[0].switches) / sizeof(vr_graphics_effects[0].switches[0]))
|
||||
|
||||
static struct
|
||||
{
|
||||
boolean started;
|
||||
int preset;
|
||||
boolean client;
|
||||
/* whether the preset allows each effect; what the game wants of each
|
||||
switch, and what was written last frame */
|
||||
boolean allowed[NUMBER_OF_VR_GRAPHICS_EFFECTS];
|
||||
boolean wanted[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
|
||||
boolean written[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
|
||||
} vr_graphics;
|
||||
|
||||
static void vr_graphics_choose(
|
||||
int preset,
|
||||
boolean client)
|
||||
{
|
||||
static char const *const names[] = { "low", "medium", "high", "max" };
|
||||
unsigned int index;
|
||||
|
||||
vr_graphics.preset = preset;
|
||||
vr_graphics.client = client;
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
|
||||
|
||||
vr_graphics.allowed[index] = preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset] ||
|
||||
(client && effect->host_rules);
|
||||
}
|
||||
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
|
||||
}
|
||||
|
||||
/* each frame, before it is drawn (render.c) */
|
||||
void vr_graphics_update(
|
||||
void)
|
||||
{
|
||||
int preset = halo_vr_graphics();
|
||||
boolean client = network_game_distributed_client();
|
||||
unsigned int index, which;
|
||||
|
||||
if (!vr_graphics.started)
|
||||
{
|
||||
/* the game's switches as it starts are its wishes */
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
vr_graphics.wanted[index][which] = *vr_graphics_effects[index].switches[which];
|
||||
vr_graphics.written[index][which] = *vr_graphics_effects[index].switches[which];
|
||||
}
|
||||
}
|
||||
vr_graphics.started = TRUE;
|
||||
vr_graphics_choose(preset, client);
|
||||
}
|
||||
else if (preset != vr_graphics.preset || client != vr_graphics.client)
|
||||
{
|
||||
vr_graphics_choose(preset, client);
|
||||
}
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
boolean *game_switch = vr_graphics_effects[index].switches[which];
|
||||
boolean value;
|
||||
|
||||
/* (the game, a script, the console changed it since: its wish) */
|
||||
if (*game_switch != vr_graphics.written[index][which])
|
||||
vr_graphics.wanted[index][which] = *game_switch;
|
||||
value = vr_graphics.wanted[index][which] && vr_graphics.allowed[index];
|
||||
*game_switch = value;
|
||||
vr_graphics.written[index][which] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* whether the preset keeps this switch of the game's off now (cheats.c, a
|
||||
client's rules: those it may keep off) */
|
||||
boolean vr_graphics_reduces(
|
||||
boolean const *game_switch)
|
||||
{
|
||||
unsigned int index, which;
|
||||
|
||||
if (!vr_graphics.started)
|
||||
return FALSE;
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
if (vr_graphics_effects[index].switches[which] == game_switch)
|
||||
return !vr_graphics.allowed[index];
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -88,13 +88,14 @@ degrees on an axis (the other axis 0); nonzero if it has. The game counts
|
||||
it as looking around (player_control.c's action tests). */
|
||||
int halo_vr_look_delta(float *yaw, float *pitch);
|
||||
|
||||
/* The weapon in the right hand (vr.aim_with = "controller"; each returns 0,
|
||||
and the game does as before, when the hand is not tracked):
|
||||
/* The weapon in the right hand, or the left one (vr.handedness; vr.aim_with
|
||||
= "controller"; each returns 0, and the game does as before, when the hand
|
||||
is not tracked):
|
||||
- the direction the player's weapon aims, where the controller points (the
|
||||
body and the view keep facing where the head looks);
|
||||
- where its shots start, as an offset from the unit's camera (world units);
|
||||
- the camera the first-person weapon model is posed from, so that it lies
|
||||
in the hand;
|
||||
in the hand (in model space, below);
|
||||
- the aiming laser's start and direction; */
|
||||
int halo_vr_aim_vector(short local_player_index, float vector[3]);
|
||||
int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
|
||||
@@ -104,9 +105,18 @@ int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
|
||||
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
|
||||
int halo_vr_weapon_camera(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
int halo_vr_laser(short local_player_index, float start[3], float direction[3]);
|
||||
/* Model space: the first-person model (first_person_weapons.c) is always
|
||||
posed as the right hand's. Left-handed, it is posed in a mirror image of
|
||||
the room and drawn mirrored back (its triangles' winding turned over); the
|
||||
mirror: a point on it (world units) and its normal, or 0 when it is not
|
||||
mirrored. What first_person_weapons.c and vr.c exchange about the model
|
||||
is in model space: the weapon camera, the free hand, the foregrip, the
|
||||
fingers (0 the model's left hand, the free one), the barrel and the laser
|
||||
along it. */
|
||||
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3]);
|
||||
/* the laser for this frame, along the barrel (first_person_weapons.c: while
|
||||
an animation turns the weapon, as in a reload), and the laser at the ready,
|
||||
where the controller aims */
|
||||
an animation turns the weapon, as in a reload; model space), and the laser
|
||||
at the ready, where the controller aims (in the room) */
|
||||
void halo_vr_set_laser(short local_player_index, const float start[3], const float direction[3]);
|
||||
int halo_vr_ready_laser(short local_player_index, float start[3], float direction[3]);
|
||||
/* The arms (interface/first_person_weapons.c):
|
||||
@@ -114,16 +124,16 @@ int halo_vr_ready_laser(short local_player_index, float start[3], float directio
|
||||
and the torso's flat heading (which follows the head beyond a few
|
||||
degrees, and the hands; the stick's turns turn it at once), from which
|
||||
the shoulders hang;
|
||||
- the left hand's pose;
|
||||
- the left hand on the weapon: given where the model's left hand holds it
|
||||
- the free hand's pose (the left one, left-handed the right; model space);
|
||||
- the free hand on the weapon: given where the model's left hand holds it
|
||||
(world units) and the line from its right hand to its left in the
|
||||
weapon's forward, right and up, whether the left hand grips it; then the
|
||||
weapon's forward, right and up, whether the free hand grips it; then the
|
||||
weapon aims along both hands; */
|
||||
int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3]);
|
||||
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
|
||||
/* - the fingers of a hand (0 the left, 1 the right), from the controller's trigger,
|
||||
grip and touch: the thumb, index, middle finger and the last two, each
|
||||
/* - the fingers of a hand of the model (0 the left, the free hand; 1 the
|
||||
right, the weapon's), from the controller's trigger, grip and touch: the thumb, index, middle finger and the last two, each
|
||||
curled 0 (straight) to 1; 0 when the hand is not tracked or vr.fingers
|
||||
is off, else HALO_VR_FINGERS_CURLED, and HALO_VR_FINGERS_TOUCH too when
|
||||
the controller tells where the fingers rest (else a finger off the
|
||||
@@ -156,13 +166,14 @@ int halo_vr_melee(void);
|
||||
0 with none): the game scales melee damage by the player's run instead,
|
||||
and in VR a player punches standing (units.c) */
|
||||
float halo_vr_melee_strength(void);
|
||||
/* The grenade (units.c): whether the left trigger holds it in the hand; and
|
||||
/* The grenade (units.c): whether the free hand's trigger holds it in the
|
||||
hand; and
|
||||
as it let go, whether the hand threw it: from where (world units), which
|
||||
way (the game's axes) and how hard (0..1 of the game's throw) */
|
||||
int halo_vr_grenade_held(short local_player_index);
|
||||
int halo_vr_grenade_throw(short local_player_index, float origin[3], float direction[3], float *strength);
|
||||
/* where the held grenade is drawn (render_objects.c): in the left palm, and
|
||||
at what size */
|
||||
/* where the held grenade is drawn (first_person_weapons.c): in the free
|
||||
hand's palm, and at what size */
|
||||
int halo_vr_grenade_in_hand(short local_player_index, float position[3], float forward[3], float up[3],
|
||||
float *scale);
|
||||
/* The scope (render.c): zoomed in by magnification, the camera for the
|
||||
@@ -193,9 +204,9 @@ void halo_vr_capture_names(void);
|
||||
void halo_vr_set_scope_raise(float metres);
|
||||
void halo_vr_capture_scope(float left, float top, float right, float bottom);
|
||||
/* where the weapon's gun node is as drawn, at the ready (world units;
|
||||
first_person_weapons.c, each frame): shots, the crosshair and the laser
|
||||
start there; and the direction its barrel points in the weapon camera's
|
||||
frame (forward, left, up), which the camera is turned to aim */
|
||||
first_person_weapons.c, each frame; model space): shots, the crosshair and
|
||||
the laser start there; and the direction its barrel points in the weapon
|
||||
camera's frame (forward, left, up), which the camera is turned to aim */
|
||||
void halo_vr_set_barrel(short local_player_index, const float world[3], const float direction[3]);
|
||||
/* how far, in world units, the weapon's aim runs before it hits something
|
||||
(render.c, each frame); the crosshair floats there */
|
||||
@@ -211,7 +222,8 @@ void halo_vr_capture_crosshair(void);
|
||||
/* Vehicles (player_control.c, each tick): the seat the player sits in and
|
||||
its vehicle's heading (yaw, radians). Seated, the body turns with the
|
||||
vehicle; a driver who steers by facing (HALO_VR_SEAT_STEER) steers with the
|
||||
right stick, else the right controller aims (HALO_VR_SEAT_AIM). And each
|
||||
turning stick, else the weapon hand's controller aims (HALO_VR_SEAT_AIM).
|
||||
And each
|
||||
frame (render.c) the heading as drawn, between ticks. */
|
||||
enum
|
||||
{
|
||||
@@ -237,6 +249,59 @@ int halo_vr_wheel_held(void);
|
||||
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
|
||||
Frame's), for what the game tells the player */
|
||||
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) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_GRAPHICS_LOW = 0,
|
||||
HALO_VR_GRAPHICS_MEDIUM,
|
||||
HALO_VR_GRAPHICS_HIGH,
|
||||
HALO_VR_GRAPHICS_MAX,
|
||||
};
|
||||
int halo_vr_graphics(void);
|
||||
/* the VR options: the refresh rates the headset offers (Hz, at most maximum;
|
||||
0 when it lets none be chosen, as SteamVR), and the one it runs at (0:
|
||||
unknown; vr.refresh_rate asks for one); and vr.resolution_scale as the app
|
||||
started (it takes a restart) */
|
||||
int halo_vr_refresh_rates(float *rates, int maximum, float *current);
|
||||
float halo_vr_started_resolution_scale(void);
|
||||
/* whether a seated player's view stays in first person (director.c) */
|
||||
int halo_vr_vehicle_first_person(void);
|
||||
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
|
||||
@@ -266,7 +331,7 @@ coming (each returns 0 otherwise, and for this machine's own players, and
|
||||
the game does as before):
|
||||
- where the weapon aims, on foot;
|
||||
- where its shots start (world units), from the unit's camera;
|
||||
- whether the left hand holds the grenade;
|
||||
- whether the free hand holds the grenade;
|
||||
- the hand's throw, once, as the grenade goes (as halo_vr_grenade_throw);
|
||||
and this machine's own VR player's throw, to send (units.c). */
|
||||
int halo_vr_network_aim(long player_index, float vector[3]);
|
||||
|
||||
@@ -281,6 +281,10 @@ static long shadow_scale_choose(void)
|
||||
long resolution = config_integer("display.shadow_resolution");
|
||||
long scale = 1;
|
||||
|
||||
/* (VR's "max" graphics: 512 at least) */
|
||||
if (halo_vr_graphics() == HALO_VR_GRAPHICS_MAX && resolution < 512)
|
||||
resolution = 512;
|
||||
|
||||
while (scale < SHADOW_MAP_MAXIMUM_SCALE && SHADOW_MAP_SIZE * scale * 2 <= resolution)
|
||||
scale *= 2;
|
||||
return scale;
|
||||
@@ -3170,6 +3174,22 @@ static GLenum blend_equation(DWORD operation)
|
||||
}
|
||||
}
|
||||
|
||||
/* the model being drawn mirrors: its node matrices are left-handed (the
|
||||
left hand's first-person weapon in VR, first_person_weapons.c), and its
|
||||
triangles' winding is turned over (set by rasterizer_xbox.c as it sets them,
|
||||
after HaloCE-Quest-VR, CC0) */
|
||||
static int mirrored_skinning;
|
||||
|
||||
void d3d8_set_mirrored_skinning(int mirrored)
|
||||
{
|
||||
mirrored_skinning = mirrored != 0;
|
||||
}
|
||||
|
||||
int d3d8_mirrored_skinning(void)
|
||||
{
|
||||
return mirrored_skinning;
|
||||
}
|
||||
|
||||
static void apply_raster_state(BOOL has_depth)
|
||||
{
|
||||
DWORD *rs = D3D__RenderState;
|
||||
@@ -3305,6 +3325,8 @@ static void apply_raster_state(BOOL has_depth)
|
||||
#endif
|
||||
GLenum cull_mode = rs[D3DRS_CULLMODE] == rs[D3DRS_FRONTFACE] ? GL_FRONT : GL_BACK;
|
||||
|
||||
if (mirrored_skinning)
|
||||
front_face = front_face == GL_CW ? GL_CCW : GL_CW;
|
||||
if (gl_state.front_face != front_face)
|
||||
{
|
||||
gl_state.front_face = front_face;
|
||||
@@ -3416,9 +3438,10 @@ static void draw_flush(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* display.per_pixel_lighting: the model lighting programs' draws are lit for
|
||||
each pixel (nv2a_psh.c model_lighting), from shaders of their own; read
|
||||
again when a setting changes */
|
||||
/* display.per_pixel_lighting (or VR's "max" graphics, vr.graphics): the
|
||||
model lighting programs' draws are lit for each pixel (nv2a_psh.c
|
||||
model_lighting), from shaders of their own; read again when a setting
|
||||
changes */
|
||||
static BOOL per_pixel_lighting(void)
|
||||
{
|
||||
static unsigned long read_at = (unsigned long)-1;
|
||||
@@ -3427,7 +3450,7 @@ static BOOL per_pixel_lighting(void)
|
||||
if (read_at != config_changes())
|
||||
{
|
||||
read_at = config_changes();
|
||||
enabled = config_boolean("display.per_pixel_lighting") != 0;
|
||||
enabled = config_boolean("display.per_pixel_lighting") != 0 || halo_vr_graphics() == HALO_VR_GRAPHICS_MAX;
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ it exists, so that the player's edits and comments stay.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -164,6 +165,13 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_android,
|
||||
"The display refresh rate to ask the headset for, in Hz (72, 90, 120);\n"
|
||||
"it picks the nearest it has at or below. 0 for the highest." },
|
||||
{ "vr.graphics", _config_string, "\"high\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
|
||||
"Which of the costlier effects are drawn: \"high\" all of them; \"medium\"\n"
|
||||
"no object shadows, environment reflections or lens flares; \"low\" nor\n"
|
||||
"specular light, bump maps, grass, the screen's fog, contrails, weather\n"
|
||||
"or camouflage's second pass (a client in another's game keeps grass,\n"
|
||||
"lens flares and fog); \"max\" all of them, the models lit for each\n"
|
||||
"pixel and shadows at 512x512." },
|
||||
{ "vr.positional_tracking", _config_boolean, "true", "HALO_VR_POSITIONAL", _environment_value, _platform_android,
|
||||
"Moving your head moves the view (leaning, ducking); false only turns it.\n"
|
||||
"The view recentres when play starts or resumes after a cinematic, and\n"
|
||||
@@ -180,8 +188,41 @@ static const struct config_setting config_settings[] =
|
||||
"Log the frame rate and where the frame time goes every five seconds\n"
|
||||
"(adb logcat -s halo)." },
|
||||
{ "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android,
|
||||
"\"controller\": the weapon is in your right hand and aims where it points,\n"
|
||||
"while you move and look with your head; \"head\": you aim where you look." },
|
||||
"\"controller\": the weapon is in your hand (vr.handedness) and aims where\n"
|
||||
"it points, while you move and look with your head; \"head\": you aim\n"
|
||||
"where you look." },
|
||||
{ "vr.handedness", _config_string, "\"right\"", "HALO_VR_HANDEDNESS", _environment_value, _platform_android,
|
||||
"The hand that holds the weapon: \"right\", or \"left\", which mirrors the\n"
|
||||
"weapon and the arms, throws grenades and grips the foregrip with the\n"
|
||||
"right hand, and fires with the left trigger." },
|
||||
{ "vr.mirror_controls", _config_boolean, "true", "HALO_VR_MIRROR_CONTROLS", _environment_value,
|
||||
_platform_android,
|
||||
"Left-handed, the sticks and buttons swap sides too: the right stick moves\n"
|
||||
"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,
|
||||
@@ -196,7 +237,7 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.weapon_down", _config_real, "0.14", "HALO_VR_WEAPON_DOWN", _environment_value, _platform_android,
|
||||
"See weapon_forward." },
|
||||
{ "vr.two_hand_reach", _config_real, "0.2", "HALO_VR_TWO_HAND_REACH", _environment_value, _platform_android,
|
||||
"How near, in metres, the left hand must be to the weapon's foregrip for\n"
|
||||
"How near, in metres, the free hand must be to the weapon's foregrip for\n"
|
||||
"its grip button to take hold of it with both hands." },
|
||||
{ "vr.body", _config_string, "\"arms\"", "HALO_VR_BODY", _environment_value, _platform_android,
|
||||
"What of your body is drawn with the weapon: \"arms\", the arms and hands\n"
|
||||
@@ -212,17 +253,17 @@ static const struct config_setting config_settings[] =
|
||||
"false keeps the arm's shape as on the weapon, pointed at the shoulder." },
|
||||
{ "vr.melee_swing_speed", _config_real, "2.0", "HALO_VR_MELEE_SWING", _environment_value, _platform_android,
|
||||
"Thrusting a hand forward faster than this, in metres per second, is a\n"
|
||||
"melee attack (the right grip is too); 0 turns it off." },
|
||||
"melee attack (the weapon hand's grip is too); 0 turns it off." },
|
||||
{ "vr.grenade_throw_speed", _config_real, "1.5", "HALO_VR_GRENADE_THROW", _environment_value, _platform_android,
|
||||
"A grenade is held while the left trigger is, and thrown as it lets go:\n"
|
||||
"along the hand's throw, when the hand moved faster than this (metres per\n"
|
||||
"second; full strength at 4), else as the game throws it. 0 turns the\n"
|
||||
"hand's throw off." },
|
||||
"A grenade is held while the free hand's trigger is, and thrown as it\n"
|
||||
"lets go: along the hand's throw, when the hand moved faster than this\n"
|
||||
"(metres per second; full strength at 4), else as the game throws it. 0\n"
|
||||
"turns the hand's throw off." },
|
||||
{ "vr.grenade_scale", _config_real, "0.55", "HALO_VR_GRENADE_SCALE", _environment_value, _platform_android,
|
||||
"The size the grenade held in the left hand is drawn at (1 as in the game)." },
|
||||
"The size the grenade held in the free hand is drawn at (1 as in the game)." },
|
||||
{ "vr.grenade_right", _config_real, "0.055", "HALO_VR_GRENADE_RIGHT", _environment_value, _platform_android,
|
||||
"Where the held grenade is drawn, in metres from the left controller's\n"
|
||||
"grip: towards the palm (right),", },
|
||||
"Where the held grenade is drawn, in metres from the free controller's\n"
|
||||
"grip: towards the palm (the left hand's right),", },
|
||||
{ "vr.grenade_forward", _config_real, "0.01", "HALO_VR_GRENADE_FORWARD", _environment_value, _platform_android,
|
||||
"the grip's forward (through the fist, towards the index finger),", },
|
||||
{ "vr.grenade_down", _config_real, "0.01", "HALO_VR_GRENADE_DOWN", _environment_value, _platform_android,
|
||||
@@ -285,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"
|
||||
@@ -305,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)." },
|
||||
@@ -554,6 +604,13 @@ 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"
|
||||
"left hand's is in VR: a test aid for what reads mirrored there. Read once." },
|
||||
{ "debug.test_input", _config_string, "\"\"", "HALO_TEST_INPUT", _environment_value, _platform_all,
|
||||
"\"bot:<seed>\" plays controller 1 with a scripted pattern (automated\n"
|
||||
"network tests); \"look:<seed>\" stands still, only turning and looking\n"
|
||||
@@ -695,13 +752,24 @@ static char *config_read_file(const char *path, size_t *size)
|
||||
static int config_write_file(const char *path, const char *text)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
FILE *file = fopen(path, "wb");
|
||||
/* written beside it and renamed over it: a file the app may not write
|
||||
(one pushed by adb belongs to the shell) is replaced all the same, as the
|
||||
folder is the app's, and a write cut short leaves the old file whole */
|
||||
char temporary[1100];
|
||||
FILE *file;
|
||||
int written;
|
||||
|
||||
snprintf(temporary, sizeof(temporary), "%s.tmp", path);
|
||||
file = fopen(temporary, "wb");
|
||||
if (!file)
|
||||
return 0;
|
||||
written = fwrite(text, 1, strlen(text), file) == strlen(text);
|
||||
return fclose(file) == 0 && written;
|
||||
written = fclose(file) == 0 && written;
|
||||
if (written && rename(temporary, path) == 0)
|
||||
return 1;
|
||||
platform_log("settings: cannot replace %s (%s)", path, strerror(errno));
|
||||
remove(temporary);
|
||||
return 0;
|
||||
#else
|
||||
return SDL_SaveFile(path, text, strlen(text));
|
||||
#endif
|
||||
|
||||
@@ -10,12 +10,35 @@ Frames of reference. The headset reports poses in its LOCAL space: metres,
|
||||
+x right, +y up, -z forward, the origin and heading where the user last
|
||||
recentred. The game's player stands in that space's place: its origin is
|
||||
the game's camera position, and its forward is the "body" yaw kept here.
|
||||
The right stick turns the body; the head turns on top of it, so the facing
|
||||
the game uses (halo_vr_aim) is the body's yaw plus the head's.
|
||||
The right stick (left-handed, mirrored: the left) turns the body; the head
|
||||
turns on top of it, so the facing the game uses (halo_vr_aim) is the body's
|
||||
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"
|
||||
@@ -35,6 +58,14 @@ the game uses (halo_vr_aim) is the body's 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
|
||||
{
|
||||
@@ -132,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;
|
||||
@@ -187,13 +222,13 @@ static struct
|
||||
int laser;
|
||||
float weapon_offset[3]; /* forward, right, down, metres: the weapon model's camera from the hand */
|
||||
float muzzle_distance;
|
||||
/* the left hand on the weapon (halo_vr_two_hand) */
|
||||
/* the free hand on the weapon (halo_vr_two_hand) */
|
||||
int two_hand_gripping;
|
||||
int two_hand_latched; /* the grip is held (on the weapon or not) */
|
||||
int two_hand_valid;
|
||||
float two_hand_reach; /* metres from the foregrip a closing grip takes the weapon */
|
||||
/* the grenade in the left hand (grenade_update, vr.grenade_throw_speed):
|
||||
the left trigger held, the hand's last positions (LOCAL), and the throw
|
||||
/* the grenade in the free hand (grenade_update, vr.grenade_throw_speed):
|
||||
its trigger held, the hand's last positions (LOCAL), and the throw
|
||||
as it let go */
|
||||
float grenade_throw_speed;
|
||||
float grenade_scale;
|
||||
@@ -219,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 right 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;
|
||||
@@ -245,6 +280,17 @@ static struct
|
||||
int torso_valid;
|
||||
unsigned long long torso_last_us;
|
||||
float head_level_yaw;
|
||||
/* the hands (vr.handedness): the weapon in the left one, and with
|
||||
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;
|
||||
float resolution_scale;
|
||||
} vr;
|
||||
|
||||
/* d3d8_gl.c */
|
||||
@@ -298,6 +344,24 @@ 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 */
|
||||
static int weapon_hand(void)
|
||||
{
|
||||
return vr.left_handed ? HALO_XR_HAND_LEFT : HALO_XR_HAND_RIGHT;
|
||||
}
|
||||
|
||||
static int free_hand(void)
|
||||
{
|
||||
return vr.left_handed ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT;
|
||||
}
|
||||
|
||||
static int turn_hand(void)
|
||||
{
|
||||
return vr.left_handed && vr.mirror_controls ? HALO_XR_HAND_LEFT : HALO_XR_HAND_RIGHT;
|
||||
}
|
||||
|
||||
static void quaternion_rotate(const float q[4], const float v[3], float out[3])
|
||||
{
|
||||
@@ -468,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");
|
||||
{
|
||||
@@ -476,6 +547,38 @@ static void vr_read_live_settings(void)
|
||||
vr.body_mode = !strcmp(body, "hands") ? HALO_VR_BODY_HANDS :
|
||||
!strcmp(body, "off") ? HALO_VR_BODY_OFF : HALO_VR_BODY_ARMS;
|
||||
}
|
||||
{
|
||||
int left_handed = !strcmp(config_string("vr.handedness"), "left");
|
||||
|
||||
if (left_handed != vr.left_handed)
|
||||
{
|
||||
/* (the hands change places: what the free hand held lets go) */
|
||||
vr.two_hand_latched = vr.two_hand_gripping = 0;
|
||||
vr.grenade_held = vr.grenade_thrown = 0;
|
||||
vr.grenade_sample_count = 0;
|
||||
vr.barrel_frame = 0;
|
||||
platform_log("VR: the weapon in the %s hand", left_handed ? "left" : "right");
|
||||
}
|
||||
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");
|
||||
|
||||
if (vr.active && refresh_rate != vr.refresh_rate_wanted)
|
||||
{
|
||||
platform_log("VR: refresh rate %.0f Hz asked for", refresh_rate);
|
||||
host_xr_set_refresh_rate(refresh_rate);
|
||||
}
|
||||
vr.refresh_rate_wanted = refresh_rate;
|
||||
}
|
||||
}
|
||||
|
||||
void halo_vr_initialize(void)
|
||||
@@ -498,6 +601,7 @@ void halo_vr_initialize(void)
|
||||
vr.crouch_limit = (float)config_real("vr.crouch_limit");
|
||||
vr.rise_limit = 0.25f;
|
||||
vr.log_performance = config_boolean("vr.log_performance");
|
||||
vr.resolution_scale = (float)config_real("vr.resolution_scale");
|
||||
vr.aim_with_controller = strcmp(config_string("vr.aim_with"), "head") != 0;
|
||||
vr.weapon_offset[0] = (float)config_real("vr.weapon_forward");
|
||||
vr.weapon_offset[1] = (float)config_real("vr.weapon_right");
|
||||
@@ -561,6 +665,59 @@ int halo_vr_controller_layout(void)
|
||||
return vr.active ? vr.frame.controller_layout : HALO_XR_LAYOUT_TOUCH;
|
||||
}
|
||||
|
||||
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;
|
||||
return vr.active ? host_xr_refresh_rates(rates, maximum, current) : 0;
|
||||
}
|
||||
|
||||
float halo_vr_started_resolution_scale(void)
|
||||
{
|
||||
return vr.resolution_scale;
|
||||
}
|
||||
|
||||
int halo_vr_graphics(void)
|
||||
{
|
||||
/* (read as it changes: d3d8_gl.c may ask before vr.c reads the live
|
||||
settings again) */
|
||||
static unsigned long read_at = (unsigned long)-1;
|
||||
static int graphics = HALO_VR_GRAPHICS_HIGH;
|
||||
|
||||
if (!vr.active)
|
||||
return HALO_VR_GRAPHICS_HIGH;
|
||||
if (read_at != config_changes())
|
||||
{
|
||||
const char *name = config_string("vr.graphics");
|
||||
|
||||
read_at = config_changes();
|
||||
graphics = !strcmp(name, "low") ? HALO_VR_GRAPHICS_LOW :
|
||||
!strcmp(name, "medium") ? HALO_VR_GRAPHICS_MEDIUM :
|
||||
!strcmp(name, "max") ? HALO_VR_GRAPHICS_MAX : HALO_VR_GRAPHICS_HIGH;
|
||||
}
|
||||
return graphics;
|
||||
}
|
||||
|
||||
int halo_vr_eye_size(int *width, int *height)
|
||||
{
|
||||
const char *eye_width = getenv("HALO_VR_EYE_WIDTH");
|
||||
@@ -650,6 +807,14 @@ static void performance_report(int stereo)
|
||||
vr.report_start_us = now;
|
||||
if (now - vr.report_start_us < 5000000)
|
||||
return;
|
||||
{
|
||||
/* (the rate the headset runs at now: the VR options change it) */
|
||||
float rates[1], current = 0.0f;
|
||||
|
||||
host_xr_refresh_rates(rates, 0, ¤t);
|
||||
if (current > 1.0f)
|
||||
vr.info.refresh_rate = current;
|
||||
}
|
||||
{
|
||||
float frames = (float)vr.report_frames;
|
||||
float seconds = (float)(now - vr.report_start_us) * 1e-6f;
|
||||
@@ -714,15 +879,16 @@ static void look_update(void)
|
||||
vr.look_valid = 1;
|
||||
}
|
||||
|
||||
/* The grenade in the left hand: while the left trigger is held the game
|
||||
keeps it in the hand (halo_vr_grenade_held); as the trigger lets go, the
|
||||
fastest the hand moved in its last moments is the throw (halo_vr_grenade_throw). */
|
||||
/* The grenade in the free hand (the left one, or left-handed the right):
|
||||
while its trigger is held the game keeps it in the hand
|
||||
(halo_vr_grenade_held); as the trigger lets go, the fastest the hand moved
|
||||
in its last moments is the throw (halo_vr_grenade_throw). */
|
||||
static void grenade_update(void)
|
||||
{
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
|
||||
unsigned long long now = now_us();
|
||||
int valid = hand->active && (hand->flags & HALO_XR_POSE_VALID);
|
||||
/* (at a wheel the left trigger brakes: xinput_sdl.c) */
|
||||
/* (at a wheel the free hand's trigger brakes: xinput_sdl.c) */
|
||||
int held = valid && vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN && !vr.wheel_hands &&
|
||||
hand->trigger > (vr.grenade_held ? 0.2f : 0.3f);
|
||||
int count = vr.grenade_sample_count;
|
||||
@@ -796,7 +962,7 @@ static void grenade_update(void)
|
||||
vr.grenade_strength = 0.25f;
|
||||
if (vr.grenade_strength > 1.0f)
|
||||
vr.grenade_strength = 1.0f;
|
||||
host_xr_haptic(HALO_XR_HAND_LEFT, 0.6f, 0.06f);
|
||||
host_xr_haptic(free_hand(), 0.6f, 0.06f);
|
||||
platform_log("VR: grenade thrown at %.1f m/s", best_speed);
|
||||
}
|
||||
vr.grenade_sample_count = 0;
|
||||
@@ -814,8 +980,8 @@ int halo_vr_grenade_in_hand(short local_player_index, float position[3], float f
|
||||
{
|
||||
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
|
||||
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
|
||||
float held[3], offset[3], direction[3];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
|
||||
float held[3], offset[3], direction[3], grip_offset[3];
|
||||
int axis;
|
||||
|
||||
if (!vr.aim_with_controller || local_player_index != 0 || !halo_vr_stereo_frame() || !hand->active ||
|
||||
@@ -823,13 +989,16 @@ int halo_vr_grenade_in_hand(short local_player_index, float position[3], float f
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
/* the grip's +x faces out of the left palm; the drawn hand's knuckles
|
||||
run along the aim's y, and its fingers along the aim's -z
|
||||
(first_person_weapons.c, vr_left_hand_free) */
|
||||
/* the grip's +x faces out of the left palm, into the right one; the
|
||||
drawn hand's knuckles run along the aim's y, and its fingers along the
|
||||
aim's -z (first_person_weapons.c, vr_left_hand_free) */
|
||||
memcpy(grip_offset, vr.grenade_offset, sizeof(grip_offset));
|
||||
if (free_hand() == HALO_XR_HAND_RIGHT)
|
||||
grip_offset[0] = -grip_offset[0];
|
||||
{
|
||||
float knuckles[3], fingers[3];
|
||||
|
||||
quaternion_rotate(hand->grip.orientation, vr.grenade_offset, held);
|
||||
quaternion_rotate(hand->grip.orientation, grip_offset, held);
|
||||
quaternion_rotate(hand->aim.orientation, local_up, knuckles);
|
||||
quaternion_rotate(hand->aim.orientation, local_forward, fingers);
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
@@ -878,17 +1047,21 @@ static void melee_update(void)
|
||||
|
||||
vr.hand_last_us = now;
|
||||
{
|
||||
/* the right grip's melee attack (xinput_sdl.c) aims the same way */
|
||||
int grip_down = vr.frame.hands[HALO_XR_HAND_RIGHT].active &&
|
||||
(vr.frame.hands[HALO_XR_HAND_RIGHT].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;
|
||||
@@ -907,7 +1080,7 @@ static void melee_update(void)
|
||||
if (vr.hand_last_valid[hand] && seconds > 0.0f && seconds < 0.1f && vr.melee_swing_speed > 0.0f &&
|
||||
!vr.wheel_held[hand] &&
|
||||
vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN && now >= vr.melee_ready_us &&
|
||||
!(hand == HALO_XR_HAND_LEFT && (vr.grenade_held || now - vr.grenade_released_us < 500000)))
|
||||
!(hand == free_hand() && (vr.grenade_held || now - vr.grenade_released_us < 500000)))
|
||||
{
|
||||
float velocity[3], forward_speed;
|
||||
int axis;
|
||||
@@ -963,7 +1136,7 @@ static void fingers_update(void)
|
||||
target[0] = (state->buttons & HALO_XR_BUTTON_THUMB_TOUCH) ? 0.75f : 0.0f;
|
||||
target[1] = fmaxf(state->trigger, (state->buttons & HALO_XR_BUTTON_INDEX_TOUCH) ? 0.22f : 0.0f);
|
||||
target[2] = target[3] = state->squeeze;
|
||||
if (hand == HALO_XR_HAND_LEFT && vr.grenade_held)
|
||||
if (hand == free_hand() && vr.grenade_held)
|
||||
{
|
||||
for (finger = 0; finger < 4; finger++)
|
||||
target[finger] = 0.8f;
|
||||
@@ -1076,6 +1249,9 @@ int halo_vr_finger_curls(short local_player_index, int hand, float curls[4])
|
||||
|
||||
if (!vr.active || !vr.fingers || local_player_index != 0 || hand < 0 || hand >= HALO_XR_HAND_COUNT)
|
||||
return 0;
|
||||
/* (the model's hands: left-handed, its left is the right controller's) */
|
||||
if (vr.left_handed)
|
||||
hand = HALO_XR_HAND_COUNT - 1 - hand;
|
||||
state = &vr.frame.hands[hand];
|
||||
if (!state->active || !(state->flags & HALO_XR_POSE_VALID))
|
||||
return 0;
|
||||
@@ -1095,7 +1271,7 @@ float halo_vr_melee_strength(void)
|
||||
|
||||
static void turn_update(void)
|
||||
{
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[turn_hand()];
|
||||
unsigned long long now = SDL_GetTicks();
|
||||
float seconds = vr.last_frame_ms ? (float)(now - vr.last_frame_ms) * 0.001f : 0.0f;
|
||||
float x = hand->active ? hand->thumbstick[0] : 0.0f;
|
||||
@@ -1144,7 +1320,7 @@ grips near it takes hold of it, and the hands turn it about its column: both
|
||||
by the line between them, one about the wheel's middle. Where it is, the
|
||||
driver's hands as drawn hold it (halo_vr_set_wheel); its column runs from
|
||||
there back towards the driver, tipped up WHEEL_COLUMN_TILT. Held, its turn
|
||||
steers (halo_vr_aim); let go, the right stick does. */
|
||||
steers (halo_vr_aim); let go, the turning stick does. */
|
||||
#define WHEEL_REACH 0.35f /* metres from its middle */
|
||||
#define WHEEL_COLUMN_TILT (45.0f * PI / 180.0f) /* from the horizontal */
|
||||
|
||||
@@ -1272,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)
|
||||
@@ -1290,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;
|
||||
@@ -1317,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;
|
||||
@@ -1404,8 +1615,9 @@ float halo_vr_gunner_camera_back(void)
|
||||
return vr.gunner_camera_back / METRES_PER_WORLD_UNIT;
|
||||
}
|
||||
|
||||
/* seated at a gun or a window, where the right controller aims it: at a
|
||||
gun's handles (vr.turret_handles), the barrel rises as the hand tips down */
|
||||
/* seated at a gun or a window, where the weapon hand's controller aims it:
|
||||
at a gun's handles (vr.turret_handles), the barrel rises as the hand tips
|
||||
down */
|
||||
static void seat_aim_direction(float direction[3])
|
||||
{
|
||||
float up[3], right[3];
|
||||
@@ -1416,7 +1628,8 @@ static void seat_aim_direction(float direction[3])
|
||||
}
|
||||
|
||||
/* seated, where the aim runs from (world units) and along: from the eyes,
|
||||
where the right controller points, or driving, the way the stick steers */
|
||||
where the weapon hand's controller points, or driving, the way the stick
|
||||
steers */
|
||||
static int seat_aim(float start[3], float direction[3])
|
||||
{
|
||||
int axis;
|
||||
@@ -1541,6 +1754,7 @@ void halo_vr_present(unsigned int back_buffer_texture)
|
||||
submit.wrist_turn = vr.motion_tracker_turn;
|
||||
submit.wrist_right = vr.motion_tracker_right;
|
||||
submit.wrist_down = vr.motion_tracker_down;
|
||||
submit.wrist_hand = free_hand();
|
||||
d3d8_clear_back_buffer_region(WRIST_REGION_LEFT, WRIST_REGION_TOP, WRIST_REGION_RIGHT, WRIST_REGION_BOTTOM);
|
||||
}
|
||||
/* the crosshair, drawn apart (halo_vr_capture_crosshair), to where the
|
||||
@@ -1831,9 +2045,10 @@ int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch
|
||||
head_angles(&head_yaw, &head_pitch);
|
||||
if (vr.seat_mode == HALO_VR_SEAT_STEER)
|
||||
{
|
||||
/* driving: the right stick steers off the vehicle's heading, and
|
||||
aims above or below the horizon (the body turns with the vehicle) */
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
|
||||
/* driving: the right stick (mirrored, the left) steers off the
|
||||
vehicle's heading, and aims above or below the horizon (the body
|
||||
turns with the vehicle) */
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[turn_hand()];
|
||||
float stick[2];
|
||||
unsigned long long now = now_us();
|
||||
float seconds = vr.steer_last_us ? (float)(now - vr.steer_last_us) * 1e-6f : 0.0f;
|
||||
@@ -1872,7 +2087,7 @@ int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch
|
||||
}
|
||||
else if (vr.seat_mode == HALO_VR_SEAT_AIM && weapon_hand_tracked(local_player_index))
|
||||
{
|
||||
/* a gunner or passenger: where the right controller points */
|
||||
/* a gunner or passenger: where the weapon hand's controller points */
|
||||
float forward[3];
|
||||
|
||||
seat_aim_direction(forward);
|
||||
@@ -1949,42 +2164,125 @@ static void rotate_between(const float from[3], const float to[3], float v[3])
|
||||
v[index] = v[index] * cosine + turned[index] * sine + axis[index] * along * (1.0f - cosine);
|
||||
}
|
||||
|
||||
/* ---------- the weapon in the right hand (vr.aim_with = "controller") */
|
||||
/* ---------- the weapon in the hand (vr.aim_with = "controller"): the right
|
||||
one, or left-handed (vr.handedness) the left */
|
||||
|
||||
static int weapon_hand_tracked(short local_player_index)
|
||||
{
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[weapon_hand()];
|
||||
|
||||
return vr.aim_with_controller && local_player_index == 0 && halo_vr_stereo_frame() &&
|
||||
hand->active && (hand->flags & HALO_XR_POSE_VALID);
|
||||
}
|
||||
|
||||
/* the weapon's axes in the game's: where the right controller points */
|
||||
/* Left-handed, the first-person model (first_person_weapons.c) is posed as
|
||||
the right hand's is, in a mirror image of the room, "model space", and
|
||||
drawn mirrored back across the same mirror (as HaloCE-Quest-VR mirrors the
|
||||
whole model across the weapon, CC0): its right hand takes the left
|
||||
controller, its left hand the right one, its arms the other shoulders. The
|
||||
mirror is the upright plane through the weapon hand's grip, square to
|
||||
LOCAL's x: any upright one would do (the model, posed about the vertical
|
||||
the same, mirrors back the same), and this one leaves the weapon hand
|
||||
where it is. Right-handed, model space is the room. */
|
||||
static void model_local_point(float point[3])
|
||||
{
|
||||
if (vr.left_handed)
|
||||
point[0] = 2.0f * vr.frame.hands[weapon_hand()].grip.position[0] - point[0];
|
||||
}
|
||||
|
||||
static void model_local_direction(float direction[3])
|
||||
{
|
||||
if (vr.left_handed)
|
||||
direction[0] = -direction[0];
|
||||
}
|
||||
|
||||
/* the mirror in the game's world: a point on it (world units) and its
|
||||
normal, LOCAL's x */
|
||||
static void mirror_plane(float point[3], float normal[3])
|
||||
{
|
||||
static const float local_x[3] = { 1.0f, 0.0f, 0.0f };
|
||||
float offset[3];
|
||||
int axis;
|
||||
|
||||
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, offset);
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
point[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT;
|
||||
local_to_game(local_x, vr.body_yaw, normal);
|
||||
}
|
||||
|
||||
/* a point or a direction in the game's world, between the room and model
|
||||
space (either way: the mirror is its own inverse) */
|
||||
static void mirror_game_point(float point[3])
|
||||
{
|
||||
float on[3], normal[3], along;
|
||||
int axis;
|
||||
|
||||
if (!vr.left_handed)
|
||||
return;
|
||||
mirror_plane(on, normal);
|
||||
along = (point[0] - on[0]) * normal[0] + (point[1] - on[1]) * normal[1] + (point[2] - on[2]) * normal[2];
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
point[axis] -= 2.0f * along * normal[axis];
|
||||
}
|
||||
|
||||
static void mirror_game_direction(float direction[3])
|
||||
{
|
||||
float on[3], normal[3], along;
|
||||
int axis;
|
||||
|
||||
if (!vr.left_handed)
|
||||
return;
|
||||
mirror_plane(on, normal);
|
||||
along = direction[0] * normal[0] + direction[1] * normal[1] + direction[2] * normal[2];
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
direction[axis] -= 2.0f * along * normal[axis];
|
||||
}
|
||||
|
||||
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3])
|
||||
{
|
||||
if (!vr.left_handed || !weapon_hand_tracked(local_player_index))
|
||||
return 0;
|
||||
mirror_plane(point, normal);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The weapon's frames, all in the game's axes:
|
||||
- the controller's: where it points;
|
||||
- the model's: the controller's turned so that the first-person model's
|
||||
barrel, pointed in towards the screen's centre, points where the
|
||||
controller does (barrel_direction);
|
||||
- with both hands on it, both turned alike so that the line from the
|
||||
model's right hand to its left, in the model's frame, runs from the right
|
||||
controller to the left one.
|
||||
model's right hand to its left, in the model's frame, runs from the
|
||||
weapon hand's controller to the free one's.
|
||||
The weapon aims along the controller's frame (so turned); the model hangs
|
||||
from the model's frame. */
|
||||
static void controller_axes(float forward[3], float up[3], float right[3])
|
||||
from the model's frame. Turned in model space, both; the aim is then
|
||||
mirrored back. */
|
||||
static void hand_axes(int hand, int model, float forward[3], float up[3], float right[3])
|
||||
{
|
||||
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
|
||||
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
|
||||
const float *orientation = vr.frame.hands[HALO_XR_HAND_RIGHT].aim.orientation;
|
||||
const float *orientation = vr.frame.hands[hand].aim.orientation;
|
||||
float direction[3];
|
||||
|
||||
quaternion_rotate(orientation, local_forward, direction);
|
||||
if (model)
|
||||
model_local_direction(direction);
|
||||
local_to_game(direction, vr.body_yaw, forward);
|
||||
quaternion_rotate(orientation, local_up, direction);
|
||||
if (model)
|
||||
model_local_direction(direction);
|
||||
local_to_game(direction, vr.body_yaw, up);
|
||||
/* the game's axes are right-handed with +z up: forward x up is right */
|
||||
/* the game's axes are right-handed with +z up: forward x up is right
|
||||
(in model space too: the mirrored controller's frame is a right hand's) */
|
||||
cross3(forward, up, right);
|
||||
}
|
||||
|
||||
/* where the weapon hand's controller points */
|
||||
static void controller_axes(float forward[3], float up[3], float right[3])
|
||||
{
|
||||
hand_axes(weapon_hand(), 0, forward, up, right);
|
||||
}
|
||||
|
||||
static void model_axes(const float forward[3], const float up[3], const float right[3],
|
||||
float model_forward[3], float model_up[3], float model_right[3])
|
||||
{
|
||||
@@ -2010,11 +2308,11 @@ static void model_axes(const float forward[3], const float up[3], const float ri
|
||||
}
|
||||
|
||||
/* with both hands on the weapon, the turn that brings the model's grips in
|
||||
line with the hands (from, to); 0 without */
|
||||
line with the hands (from, to; model space); 0 without */
|
||||
static int two_hand_turn(const float model_forward[3], const float model_up[3], const float model_right[3],
|
||||
float from[3], float to[3])
|
||||
{
|
||||
float left[3], right_hand[3];
|
||||
float free_grip[3], free_offset[3], weapon_offset[3];
|
||||
int axis;
|
||||
|
||||
if (!vr.two_hand_gripping || !vr.two_hand_valid)
|
||||
@@ -2024,10 +2322,12 @@ static int two_hand_turn(const float model_forward[3], const float model_up[3],
|
||||
from[axis] = model_forward[axis] * vr.two_hand_reference[0] + model_right[axis] * vr.two_hand_reference[1] +
|
||||
model_up[axis] * vr.two_hand_reference[2];
|
||||
}
|
||||
tracked_offset(vr.frame.hands[HALO_XR_HAND_LEFT].grip.position, left);
|
||||
tracked_offset(vr.frame.hands[HALO_XR_HAND_RIGHT].grip.position, right_hand);
|
||||
memcpy(free_grip, vr.frame.hands[free_hand()].grip.position, sizeof(free_grip));
|
||||
model_local_point(free_grip);
|
||||
tracked_offset(free_grip, free_offset);
|
||||
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, weapon_offset);
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
to[axis] = left[axis] - right_hand[axis];
|
||||
to[axis] = free_offset[axis] - weapon_offset[axis];
|
||||
return normalize3(from) > 0.0f && normalize3(to) > 0.0f;
|
||||
}
|
||||
|
||||
@@ -2038,34 +2338,41 @@ static void turn_axes(const float from[3], const float to[3], float forward[3],
|
||||
cross3(forward, up, right);
|
||||
}
|
||||
|
||||
/* where the weapon aims */
|
||||
/* where the weapon aims (in the room) */
|
||||
static void weapon_axes(float forward[3], float up[3], float right[3])
|
||||
{
|
||||
float model_forward[3], model_up[3], model_right[3], from[3], to[3];
|
||||
|
||||
controller_axes(forward, up, right);
|
||||
hand_axes(weapon_hand(), 1, forward, up, right);
|
||||
model_axes(forward, up, right, model_forward, model_up, model_right);
|
||||
if (two_hand_turn(model_forward, model_up, model_right, from, to))
|
||||
turn_axes(from, to, forward, up, right);
|
||||
if (vr.left_handed)
|
||||
{
|
||||
mirror_game_direction(forward);
|
||||
mirror_game_direction(up);
|
||||
cross3(forward, up, right);
|
||||
}
|
||||
}
|
||||
|
||||
/* how the weapon's model hangs */
|
||||
/* how the weapon's model hangs (model space) */
|
||||
static void weapon_model_axes(float forward[3], float up[3], float right[3])
|
||||
{
|
||||
float controller_forward[3], controller_up[3], controller_right[3], from[3], to[3];
|
||||
|
||||
controller_axes(controller_forward, controller_up, controller_right);
|
||||
hand_axes(weapon_hand(), 1, controller_forward, controller_up, controller_right);
|
||||
model_axes(controller_forward, controller_up, controller_right, forward, up, right);
|
||||
if (two_hand_turn(forward, up, right, from, to))
|
||||
turn_axes(from, to, forward, up, right);
|
||||
}
|
||||
|
||||
/* the hand's offset from the game's camera, in world units */
|
||||
/* the weapon hand's offset from the game's camera, in world units (the
|
||||
same in model space) */
|
||||
static void hand_offset(float out[3])
|
||||
{
|
||||
int axis;
|
||||
|
||||
tracked_offset(vr.frame.hands[HALO_XR_HAND_RIGHT].grip.position, out);
|
||||
tracked_offset(vr.frame.hands[weapon_hand()].grip.position, out);
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
out[axis] /= METRES_PER_WORLD_UNIT;
|
||||
}
|
||||
@@ -2087,7 +2394,10 @@ void halo_vr_set_barrel(short local_player_index, const float world[3], const fl
|
||||
{
|
||||
if (local_player_index != 0)
|
||||
return;
|
||||
/* (posed in model space: the barrel as drawn, in the room; its
|
||||
direction stays the model's, which model_axes turns) */
|
||||
memcpy(vr.barrel, world, sizeof(vr.barrel));
|
||||
mirror_game_point(vr.barrel);
|
||||
vr.barrel_frame = vr.frame_number;
|
||||
if (!vr.barrel_direction_valid || memcmp(vr.barrel_direction, direction, sizeof(vr.barrel_direction)))
|
||||
{
|
||||
@@ -2152,7 +2462,7 @@ int halo_vr_weapon_camera(short local_player_index, float position[3], float for
|
||||
weapon_model_axes(forward, up, right);
|
||||
hand_offset(hand);
|
||||
{
|
||||
/* the right grip's melee attack, which the hand does not swing: the
|
||||
/* the weapon hand's grip's melee attack, which it does not swing: the
|
||||
weapon jabs forward and back (the game's own animation is held
|
||||
off, first_person_weapons.c) */
|
||||
float seconds = vr.melee_jab_start_us ? (float)(now_us() - vr.melee_jab_start_us) * 1e-6f : 1.0f;
|
||||
@@ -2264,12 +2574,12 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3])
|
||||
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3])
|
||||
{
|
||||
static const float local_forward[3] = { 0.0f, 0.0f, -1.0f };
|
||||
static const float local_up[3] = { 0.0f, 1.0f, 0.0f };
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
|
||||
float offset[3], direction[3];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
|
||||
float grip[3], offset[3], direction[3];
|
||||
int axis;
|
||||
|
||||
if (!vr.aim_with_controller || local_player_index != 0 || !halo_vr_stereo_frame() || !hand->active ||
|
||||
@@ -2277,25 +2587,30 @@ int halo_vr_left_hand(short local_player_index, float position[3], float forward
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
tracked_offset(hand->grip.position, offset);
|
||||
/* (in model space: left-handed, the right controller as a left one) */
|
||||
memcpy(grip, hand->grip.position, sizeof(grip));
|
||||
model_local_point(grip);
|
||||
tracked_offset(grip, offset);
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
position[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT;
|
||||
quaternion_rotate(hand->aim.orientation, local_forward, direction);
|
||||
model_local_direction(direction);
|
||||
local_to_game(direction, vr.body_yaw, forward);
|
||||
quaternion_rotate(hand->aim.orientation, local_up, direction);
|
||||
model_local_direction(direction);
|
||||
local_to_game(direction, vr.body_yaw, up);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3])
|
||||
{
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_LEFT];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[free_hand()];
|
||||
float position[3], forward[3], up[3];
|
||||
int squeezed = hand->squeeze > (vr.two_hand_latched ? 0.35f : 0.6f);
|
||||
|
||||
memcpy(vr.two_hand_reference, reference, sizeof(vr.two_hand_reference));
|
||||
vr.two_hand_valid = 1;
|
||||
if (!squeezed || !halo_vr_left_hand(local_player_index, position, forward, up))
|
||||
if (!squeezed || !halo_vr_free_hand(local_player_index, position, forward, up))
|
||||
{
|
||||
vr.two_hand_latched = vr.two_hand_gripping = 0;
|
||||
return 0;
|
||||
@@ -2310,7 +2625,7 @@ int halo_vr_two_hand(short local_player_index, const float foregrip[3], const fl
|
||||
vr.two_hand_latched = 1;
|
||||
vr.two_hand_gripping = distance < vr.two_hand_reach;
|
||||
if (vr.two_hand_gripping)
|
||||
host_xr_haptic(HALO_XR_HAND_LEFT, 0.4f, 0.04f);
|
||||
host_xr_haptic(free_hand(), 0.4f, 0.04f);
|
||||
}
|
||||
return vr.two_hand_gripping;
|
||||
}
|
||||
@@ -2338,8 +2653,11 @@ void halo_vr_set_laser(short local_player_index, const float start[3], const flo
|
||||
{
|
||||
if (local_player_index != 0)
|
||||
return;
|
||||
/* (from model space) */
|
||||
memcpy(vr.laser_start, start, sizeof(vr.laser_start));
|
||||
memcpy(vr.laser_direction, direction, sizeof(vr.laser_direction));
|
||||
mirror_game_point(vr.laser_start);
|
||||
mirror_game_direction(vr.laser_direction);
|
||||
vr.laser_frame = vr.frame_number;
|
||||
}
|
||||
|
||||
@@ -2694,7 +3012,7 @@ void halo_vr_set_scope_raise(float metres)
|
||||
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
|
||||
float *vertical_field_of_view)
|
||||
{
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[HALO_XR_HAND_RIGHT];
|
||||
const struct halo_xr_hand *hand = &vr.frame.hands[weapon_hand()];
|
||||
float right[3], game_offset[3], local_forward[3], local_up[3], local_right[3], back[3];
|
||||
float eye_distance = 0.0f;
|
||||
int axis;
|
||||
@@ -2774,6 +3092,110 @@ void halo_vr_capture_crosshair(void)
|
||||
d3d8_clear_back_buffer(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
/* Left-handed, in play, the controllers as the gamepad's sides: the weapon
|
||||
hand's trigger and grip are the right side's (fire, melee), the free hand's
|
||||
the left side's (the grenade, both hands); with vr.mirror_controls the
|
||||
sticks, their clicks, the bumpers and the face buttons change sides too,
|
||||
the Quest's X and Y for A and B (after HaloCE-Quest-VR, CC0). The Steam
|
||||
Frame has A, B, X and Y on the right controller only, a D-pad on the left:
|
||||
the D-pad becomes them, as their mirror image (down A, left B, right X, up
|
||||
Y), and they the D-pad. The menus keep the controllers as they are (A
|
||||
accepts and B goes back, as the screens show). */
|
||||
static void controllers_mirrored(struct halo_xr_hand hands[HALO_XR_HAND_COUNT])
|
||||
{
|
||||
struct halo_xr_hand *left = &hands[HALO_XR_HAND_LEFT];
|
||||
struct halo_xr_hand *right = &hands[HALO_XR_HAND_RIGHT];
|
||||
int left_buttons = left->buttons, right_buttons = right->buttons;
|
||||
int swapped = HALO_XR_BUTTON_TRIGGER | HALO_XR_BUTTON_SQUEEZE;
|
||||
int frame = vr.frame.controller_layout == HALO_XR_LAYOUT_FRAME;
|
||||
float value;
|
||||
int axis;
|
||||
|
||||
value = left->trigger;
|
||||
left->trigger = right->trigger;
|
||||
right->trigger = value;
|
||||
value = left->squeeze;
|
||||
left->squeeze = right->squeeze;
|
||||
right->squeeze = value;
|
||||
if (vr.mirror_controls)
|
||||
{
|
||||
for (axis = 0; axis < 2; axis++)
|
||||
{
|
||||
value = left->thumbstick[axis];
|
||||
left->thumbstick[axis] = right->thumbstick[axis];
|
||||
right->thumbstick[axis] = value;
|
||||
}
|
||||
swapped |= HALO_XR_BUTTON_THUMBSTICK | HALO_XR_BUTTON_BUMPER;
|
||||
if (!frame)
|
||||
swapped |= HALO_XR_BUTTON_A | HALO_XR_BUTTON_B;
|
||||
}
|
||||
left->buttons = (left_buttons & ~swapped) | (right_buttons & swapped);
|
||||
right->buttons = (right_buttons & ~swapped) | (left_buttons & swapped);
|
||||
if (vr.mirror_controls && frame)
|
||||
{
|
||||
const int face = HALO_XR_BUTTON_A | HALO_XR_BUTTON_B;
|
||||
const int dpad = HALO_XR_BUTTON_DPAD_UP | HALO_XR_BUTTON_DPAD_DOWN | HALO_XR_BUTTON_DPAD_LEFT |
|
||||
HALO_XR_BUTTON_DPAD_RIGHT;
|
||||
|
||||
/* (the right one's X and Y come as the left's A and B: host_xr.c) */
|
||||
left->buttons &= ~(face | dpad);
|
||||
right->buttons &= ~face;
|
||||
right->buttons |= ((left_buttons & HALO_XR_BUTTON_DPAD_DOWN) ? HALO_XR_BUTTON_A : 0) |
|
||||
((left_buttons & HALO_XR_BUTTON_DPAD_LEFT) ? HALO_XR_BUTTON_B : 0);
|
||||
left->buttons |= ((left_buttons & HALO_XR_BUTTON_DPAD_RIGHT) ? HALO_XR_BUTTON_A : 0) |
|
||||
((left_buttons & HALO_XR_BUTTON_DPAD_UP) ? HALO_XR_BUTTON_B : 0) |
|
||||
((right_buttons & HALO_XR_BUTTON_A) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
|
||||
((right_buttons & HALO_XR_BUTTON_B) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
|
||||
((left_buttons & HALO_XR_BUTTON_A) ? HALO_XR_BUTTON_DPAD_RIGHT : 0) |
|
||||
((left_buttons & HALO_XR_BUTTON_B) ? HALO_XR_BUTTON_DPAD_UP : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
@@ -2781,12 +3203,23 @@ 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 (!*menus)
|
||||
controls_in_play(hands);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
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)
|
||||
{
|
||||
(void)rates; (void)maximum; *current = 0.0f;
|
||||
return 0;
|
||||
}
|
||||
float halo_vr_started_resolution_scale(void) { return 1.0f; }
|
||||
int halo_vr_active(void) { return 0; }
|
||||
unsigned long long halo_vr_timing_start(void) { return 0; }
|
||||
unsigned long halo_vr_frame_serial(void) { return 0; }
|
||||
@@ -2887,11 +3320,16 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f
|
||||
(void)local_player_index; (void)head; (void)neck; (void)forward;
|
||||
return 0;
|
||||
}
|
||||
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3])
|
||||
int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3])
|
||||
{
|
||||
(void)local_player_index; (void)position; (void)forward; (void)up;
|
||||
return 0;
|
||||
}
|
||||
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3])
|
||||
{
|
||||
(void)local_player_index; (void)point; (void)normal;
|
||||
return 0;
|
||||
}
|
||||
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3])
|
||||
{
|
||||
(void)local_player_index; (void)foregrip; (void)reference;
|
||||
|
||||
@@ -851,6 +851,19 @@ Holding a Warthog's wheel (vr.c), the right trigger is the throttle and the
|
||||
left one the brake (backwards, once stopped): the left stick pushed up or
|
||||
down as far, unless it is pushed further.
|
||||
|
||||
Left-handed (vr.handedness), vr.c hands the controllers over with the sides
|
||||
swapped where the hands' jobs are (halo_vr_controllers): the left trigger
|
||||
fires and the left grip strikes, the right ones hold and throw the grenade
|
||||
and grip the foregrip; and with vr.mirror_controls the sticks, bumpers and
|
||||
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"
|
||||
@@ -929,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) */
|
||||
@@ -1038,12 +1050,15 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
|
||||
if (port < 0)
|
||||
return ERROR_DEVICE_NOT_CONNECTED;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: the heavy (left) motor in the left hand, the light one in the
|
||||
right, as the game refreshes them every frame */
|
||||
/* VR: the heavy (left) motor in the free hand, the light one in the
|
||||
weapon's (left-handed, the other way round), as the game refreshes them
|
||||
every frame */
|
||||
if (port == 0 && halo_vr_active())
|
||||
{
|
||||
host_xr_haptic(HALO_XR_HAND_LEFT, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
|
||||
host_xr_haptic(HALO_XR_HAND_RIGHT, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
|
||||
int weapon = halo_vr_weapon_hand();
|
||||
|
||||
host_xr_haptic(HALO_XR_HAND_COUNT - 1 - weapon, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
|
||||
host_xr_haptic(weapon, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
|
||||
}
|
||||
#endif
|
||||
count = sdl_gamepads(gamepads);
|
||||
|
||||
@@ -118,6 +118,10 @@ void cheats_dispose_from_old_map(
|
||||
|
||||
/* network_game_globals.c's */
|
||||
boolean network_game_distributed_client(void);
|
||||
#ifdef HALO_ANDROID
|
||||
/* port/linux/game/vr_graphics.c */
|
||||
boolean vr_graphics_reduces(boolean const *game_switch);
|
||||
#endif
|
||||
/* bipeds.c's, player_control.c's */
|
||||
extern boolean rider_ejection;
|
||||
extern boolean player_autoaim_flag;
|
||||
@@ -155,6 +159,12 @@ static boolean cheats_network_client_rasterizer_enforce(
|
||||
environment_part <= &rasterizer_debug_options.draw_environment_fog_screen;
|
||||
environment_part++)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
/* (VR: a part the graphics preset keeps off, as it may in another's
|
||||
game: shadows, specular light, reflections; vr_graphics.c) */
|
||||
if (vr_graphics_reduces(environment_part))
|
||||
continue;
|
||||
#endif
|
||||
if (!*environment_part)
|
||||
changed = TRUE;
|
||||
*environment_part = TRUE;
|
||||
|
||||
@@ -488,10 +488,11 @@ static boolean first_person_weapon_vr_pose_kept(
|
||||
first_person_weapon_vr_pose_weapon[local_player_index]==first_person_weapon_get(local_player_index)->weapon_index;
|
||||
}
|
||||
|
||||
/* VR: the grenade the player holds (the left trigger, units.c) hangs from
|
||||
the body, which the first-person view leaves out, and with it the grenade:
|
||||
drawn instead in the left hand, as it is this frame, with the hands (which
|
||||
are drawn over the world, and would hide it wherever they overlap) */
|
||||
/* VR: the grenade the player holds (the free hand's trigger, units.c)
|
||||
hangs from the body, which the first-person view leaves out, and with it
|
||||
the grenade: drawn instead in the free hand, as it is this frame, with the
|
||||
hands (which are drawn over the world, and would hide it wherever they
|
||||
overlap; in the room, not mirrored) */
|
||||
static void first_person_weapon_draw_held_grenade(
|
||||
long unit_index,
|
||||
struct render_lighting *lighting,
|
||||
@@ -1217,17 +1218,132 @@ static void first_person_weapon_set_state(
|
||||
#ifdef HALO_ANDROID
|
||||
/* ---------- VR arms
|
||||
|
||||
The first-person skeleton hangs from the camera at the right controller
|
||||
(halo_vr_weapon_camera), so the weapon and the right hand lie where they
|
||||
should; its arms, though, reach up to shoulders that float with the weapon.
|
||||
Here each arm's upper arm and forearm are turned (two-bone inverse
|
||||
kinematics, port/linux/game/vr_arms.c) to run from the player's shoulders,
|
||||
below the neck and square to the torso, to its wrist, the controller's roll
|
||||
shared along the forearm. The left wrist, unless the hand holds the
|
||||
weapon's foregrip, goes to the left controller, its fingers with it. */
|
||||
The first-person skeleton hangs from the camera at the weapon hand's
|
||||
controller (halo_vr_weapon_camera), so the weapon and the right hand lie
|
||||
where they should; its arms, though, reach up to shoulders that float with
|
||||
the weapon. Here each arm's upper arm and forearm are turned (two-bone
|
||||
inverse kinematics, port/linux/game/vr_arms.c) to run from the player's
|
||||
shoulders, below the neck and square to the torso, to its wrist, the
|
||||
controller's roll shared along the forearm. The left wrist, unless the hand
|
||||
holds the weapon's foregrip, goes to the free controller, its fingers with
|
||||
it.
|
||||
|
||||
Left-handed (vr.handedness) all this is done in model space (halo_vr.h): a
|
||||
mirror image of the room, where the left controller holds the weapon as a
|
||||
right hand would; the body is mirrored into it, and the posed skeleton
|
||||
mirrored back (vr_mirror_pose), so that the weapon lies in the left hand,
|
||||
the model's left arm reaches to the right controller. */
|
||||
|
||||
#define VR_METRES_PER_WORLD_UNIT 3.048f
|
||||
|
||||
/* a point or a vector across the mirror (a point on it, its normal) */
|
||||
static void vr_mirror_point(
|
||||
float const on[3],
|
||||
float const normal[3],
|
||||
float point[3])
|
||||
{
|
||||
float along = (point[0] - on[0]) * normal[0] + (point[1] - on[1]) * normal[1] + (point[2] - on[2]) * normal[2];
|
||||
|
||||
point[0] -= 2.f * along * normal[0];
|
||||
point[1] -= 2.f * along * normal[1];
|
||||
point[2] -= 2.f * along * normal[2];
|
||||
}
|
||||
|
||||
static void vr_mirror_vector(
|
||||
float const normal[3],
|
||||
float vector[3])
|
||||
{
|
||||
float along = vector[0] * normal[0] + vector[1] * normal[1] + vector[2] * normal[2];
|
||||
|
||||
vector[0] -= 2.f * along * normal[0];
|
||||
vector[1] -= 2.f * along * normal[1];
|
||||
vector[2] -= 2.f * along * normal[2];
|
||||
}
|
||||
|
||||
/* the posed skeleton across a mirror: every node mirrored, its axes too
|
||||
(a left-handed frame: rasterizer_xbox.c turns its triangles' winding over).
|
||||
Numeric displays drawn so, the assault rifle's ammo counter and compass,
|
||||
are read the right way round by rasterizer_xbox_transparent_geometry.c. */
|
||||
static void vr_mirror_nodes(
|
||||
float const on[3],
|
||||
float const normal[3],
|
||||
real_matrix4x3 *node_matrices,
|
||||
short node_count)
|
||||
{
|
||||
short node_index;
|
||||
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
real_matrix4x3 *node = &node_matrices[node_index];
|
||||
real_vector3d *axes[3];
|
||||
float vector[3];
|
||||
short axis;
|
||||
|
||||
vector[0] = node->position.x;
|
||||
vector[1] = node->position.y;
|
||||
vector[2] = node->position.z;
|
||||
vr_mirror_point(on, normal, vector);
|
||||
node->position.x = vector[0];
|
||||
node->position.y = vector[1];
|
||||
node->position.z = vector[2];
|
||||
axes[0] = &node->forward;
|
||||
axes[1] = &node->left;
|
||||
axes[2] = &node->up;
|
||||
for (axis = 0; axis < 3; axis++)
|
||||
{
|
||||
vector[0] = axes[axis]->i;
|
||||
vector[1] = axes[axis]->j;
|
||||
vector[2] = axes[axis]->k;
|
||||
vr_mirror_vector(normal, vector);
|
||||
axes[axis]->i = vector[0];
|
||||
axes[axis]->j = vector[1];
|
||||
axes[axis]->k = vector[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* the posed skeleton out of model space (left-handed) */
|
||||
static void vr_mirror_pose(
|
||||
short local_player_index,
|
||||
real_matrix4x3 *node_matrices,
|
||||
short node_count)
|
||||
{
|
||||
float on[3], normal[3];
|
||||
|
||||
if (halo_vr_first_person_mirror(local_player_index, on, normal))
|
||||
vr_mirror_nodes(on, normal, node_matrices, node_count);
|
||||
}
|
||||
|
||||
/* debug.mirror_first_person (a test aid, flat): the first-person weapon
|
||||
mirrored across the camera's own forward and up, as the left hand's is in
|
||||
VR; its numeric displays can so be checked off the headset */
|
||||
int config_boolean(char const *name);
|
||||
|
||||
static void debug_mirror_first_person(
|
||||
struct render_camera const *camera,
|
||||
real_matrix4x3 *node_matrices,
|
||||
short node_count)
|
||||
{
|
||||
static int wanted = -1;
|
||||
real_vector3d left;
|
||||
float on[3], normal[3];
|
||||
|
||||
if (wanted < 0)
|
||||
wanted = config_boolean("debug.mirror_first_person") ? 1 : 0;
|
||||
if (!wanted)
|
||||
return;
|
||||
cross_product3d(&camera->up, &camera->forward, &left);
|
||||
if (normalize3d(&left) == 0.0f)
|
||||
return;
|
||||
on[0] = camera->position.x;
|
||||
on[1] = camera->position.y;
|
||||
on[2] = camera->position.z;
|
||||
normal[0] = left.i;
|
||||
normal[1] = left.j;
|
||||
normal[2] = left.k;
|
||||
vr_mirror_nodes(on, normal, node_matrices, node_count);
|
||||
}
|
||||
|
||||
struct vr_arm_nodes
|
||||
{
|
||||
long graph_index;
|
||||
@@ -1470,6 +1586,16 @@ static void vr_laser_follows_barrel(
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
/* (the laser at the ready is the room's: into model space) */
|
||||
float on[3], normal[3];
|
||||
|
||||
if (halo_vr_first_person_mirror(local_player_index, on, normal))
|
||||
{
|
||||
vr_mirror_point(on, normal, laser_start);
|
||||
vr_mirror_vector(normal, laser_direction);
|
||||
}
|
||||
}
|
||||
matrix4x3_from_point_and_vectors(&camera_matrix, &weapon_camera->position, &weapon_camera->forward,
|
||||
&weapon_camera->up);
|
||||
matrix4x3_multiply(&camera_matrix, &vr_arm_nodes.idle_pose[vr_arm_nodes.gun], &ready_gun);
|
||||
@@ -1656,6 +1782,18 @@ static void first_person_weapon_vr_arms(
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
/* (the body is the room's: into model space, where the shoulders
|
||||
change sides) */
|
||||
float on[3], normal[3];
|
||||
|
||||
if (halo_vr_first_person_mirror(local_player_index, on, normal))
|
||||
{
|
||||
vr_mirror_point(on, normal, head);
|
||||
vr_mirror_point(on, normal, neck);
|
||||
vr_mirror_vector(normal, body_forward);
|
||||
}
|
||||
}
|
||||
/* (the shoulders hang from the neck, which the head turns about) */
|
||||
eye.x = neck[0];
|
||||
eye.y = neck[1];
|
||||
@@ -1705,7 +1843,7 @@ static void first_person_weapon_vr_arms(
|
||||
return;
|
||||
if (!gripping && vr_arm_nodes.wrist[0] != NONE)
|
||||
{
|
||||
if (halo_vr_left_hand(local_player_index, hand_position, hand_forward, hand_up))
|
||||
if (halo_vr_free_hand(local_player_index, hand_position, hand_forward, hand_up))
|
||||
{
|
||||
/* (rigid: the hand and its arm carried to the controller now;
|
||||
bent, the arm is solved to it first, below, and the hand turned
|
||||
@@ -1834,8 +1972,8 @@ static void first_person_weapon_build_node_matrices(
|
||||
struct first_person_weapon *first_person_weapon= first_person_weapon_get(local_player_index);
|
||||
struct render_camera const *camera= &render.camera;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: posed from the right controller, so the weapon lies in the hand
|
||||
(port/linux/src/vr.c) */
|
||||
/* VR: posed from the weapon hand's controller, so the weapon lies in the
|
||||
hand (port/linux/src/vr.c) */
|
||||
struct render_camera weapon_camera;
|
||||
float weapon_position[3], weapon_forward[3], weapon_up[3];
|
||||
|
||||
@@ -2202,8 +2340,17 @@ static void first_person_weapon_build_node_matrices(
|
||||
(short)animation_graph->nodes.count,
|
||||
camera,
|
||||
first_person_weapon->state);
|
||||
/* (left-handed: out of model space; what the frame takes from
|
||||
the pose after, markers, effects, is then the room's) */
|
||||
vr_mirror_pose(local_player_index, first_person_weapon->node_matrices,
|
||||
(short)MIN(animation_graph->nodes.count, MAXIMUM_NODES_PER_ANIMATION));
|
||||
halo_vr_timing_end(HALO_VR_TIMING_ARMS, timing);
|
||||
}
|
||||
else
|
||||
{
|
||||
debug_mirror_first_person(camera, first_person_weapon->node_matrices,
|
||||
(short)MIN(animation_graph->nodes.count, MAXIMUM_NODES_PER_ANIMATION));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1227,6 +1227,11 @@ void rasterizer_set_model_lighting(
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* port/linux/src/d3d8_gl.c */
|
||||
void d3d8_set_mirrored_skinning(int mirrored);
|
||||
#endif
|
||||
|
||||
void rasterizer_set_model_skinning(
|
||||
struct render_skinning const *skinning)
|
||||
{
|
||||
@@ -1263,6 +1268,21 @@ void rasterizer_set_model_skinning(
|
||||
constants[2][2] = scale * matrix->up.k;
|
||||
constants[2][3] = matrix->position.z;
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
/* port: node matrices that mirror (VR, the left hand's first-person
|
||||
weapon: first_person_weapons.c) turn the triangles' winding over while
|
||||
they are drawn (after HaloCE-Quest-VR, CC0); _rasterizer_model_end and
|
||||
rasterizer_transparent_geometry_group_draw end it */
|
||||
{
|
||||
real_matrix4x3 const *m = &skinning->node_matrices[0];
|
||||
real determinant =
|
||||
m->forward.i * (m->left.j * m->up.k - m->left.k * m->up.j) -
|
||||
m->forward.j * (m->left.i * m->up.k - m->left.k * m->up.i) +
|
||||
m->forward.k * (m->left.i * m->up.j - m->left.j * m->up.i);
|
||||
|
||||
d3d8_set_mirrored_skinning(determinant < 0.0f);
|
||||
}
|
||||
#endif
|
||||
D3DDevice_SetVertexShaderConstant(
|
||||
-36,
|
||||
vsh_constants__nodematrices,
|
||||
|
||||
@@ -596,9 +596,18 @@ void _rasterizer_models_end(
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* port/linux/src/d3d8_gl.c: the model's mirroring (rasterizer_xbox.c) ends
|
||||
with it */
|
||||
void d3d8_set_mirrored_skinning(int mirrored);
|
||||
#endif
|
||||
|
||||
void _rasterizer_model_end(
|
||||
void)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
d3d8_set_mirrored_skinning(0);
|
||||
#endif
|
||||
if (rasterizer_debug_options.draw_models)
|
||||
{
|
||||
match_assert(
|
||||
|
||||
@@ -970,9 +970,83 @@ real_vector4d *offset_vector4d(
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* port: a group's draw, its mirroring (rasterizer_set_model_skinning) put
|
||||
back after it to what it was: a group drawn in the middle of a model's
|
||||
(not queued) leaves the model's own */
|
||||
int d3d8_mirrored_skinning(void);
|
||||
void d3d8_set_mirrored_skinning(int mirrored);
|
||||
|
||||
/* port: a numeric display drawn mirrored (rasterizer_set_model_skinning:
|
||||
the left hand's first-person weapon in VR; the assault rifle's ammo counter
|
||||
and compass) reads the right way round. Its geometry stays mirrored with the
|
||||
weapon (its vertices hang from the gun's nodes as well as the display's);
|
||||
instead each digit's quad shows its bitmap turned over in u
|
||||
(rasterizer_numeric_mirror_u), and as the quads' order across the display is
|
||||
turned over too, the quad of the n-th digit from the right shows the n-th
|
||||
from the left. The digits: as many as the counter's largest value needs,
|
||||
that value itself wrapping to 0 (the compass's whole turn). FALSE, the
|
||||
digit untouched, when not mirrored. Only for a first map in the model's own
|
||||
u and v (2d, not in screen space), which rasterizer_numeric_mirror_u turns. */
|
||||
static boolean rasterizer_numeric_mirrored_digit(
|
||||
real value,
|
||||
short counter_limit,
|
||||
short frame_count,
|
||||
short digit_index,
|
||||
short *bitmap_sequence_index)
|
||||
{
|
||||
short counter_value;
|
||||
short digit_count;
|
||||
short place;
|
||||
long power;
|
||||
|
||||
if (!d3d8_mirrored_skinning() || frame_count < 2)
|
||||
return FALSE;
|
||||
|
||||
counter_value = PIN(fast_ftol(floor(counter_limit*value + 0.5f)), 0, counter_limit);
|
||||
digit_count = 1;
|
||||
for (power = frame_count; power < counter_limit; power *= frame_count)
|
||||
digit_count++;
|
||||
place = digit_count - 1 - digit_index;
|
||||
if (place < 0)
|
||||
place = digit_index;
|
||||
while (place-- > 0)
|
||||
counter_value /= frame_count;
|
||||
*bitmap_sequence_index = counter_value%frame_count;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* the first map's u turned over (1-u: each digit's quad spans its bitmap) */
|
||||
static void rasterizer_numeric_mirror_u(
|
||||
real u_transform[4])
|
||||
{
|
||||
u_transform[0] = -u_transform[0];
|
||||
u_transform[1] = -u_transform[1];
|
||||
u_transform[2] = -u_transform[2];
|
||||
u_transform[3] = 1.0f - u_transform[3];
|
||||
}
|
||||
|
||||
static void rasterizer_transparent_geometry_group_draw_unmirrored(
|
||||
struct transparent_geometry_group *group,
|
||||
boolean dirty);
|
||||
|
||||
void rasterizer_transparent_geometry_group_draw(
|
||||
struct transparent_geometry_group *group,
|
||||
boolean dirty)
|
||||
{
|
||||
int mirrored = d3d8_mirrored_skinning();
|
||||
|
||||
rasterizer_transparent_geometry_group_draw_unmirrored(group, dirty);
|
||||
d3d8_set_mirrored_skinning(mirrored);
|
||||
}
|
||||
|
||||
static void rasterizer_transparent_geometry_group_draw_unmirrored(
|
||||
#else
|
||||
void rasterizer_transparent_geometry_group_draw(
|
||||
#endif
|
||||
struct transparent_geometry_group *group,
|
||||
boolean dirty)
|
||||
{
|
||||
boolean success = TRUE;
|
||||
boolean draw_active_camouflage_groups2 = FALSE;
|
||||
@@ -1813,6 +1887,9 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
short map_index;
|
||||
short stage_index;
|
||||
long result;
|
||||
#ifdef HALO_ANDROID
|
||||
boolean numeric_mirrored = FALSE;
|
||||
#endif
|
||||
|
||||
for (layer_index = 0;
|
||||
layer_index < shader_transparent_generic->generic.extra_layers.count;
|
||||
@@ -1872,6 +1949,18 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
counter_value /= frame_count;
|
||||
|
||||
bitmap_sequence_index = counter_value%frame_count;
|
||||
#ifdef HALO_ANDROID
|
||||
numeric_mirrored =
|
||||
shader_transparent_generic->generic.type==_shader_transparent_generic_type_2d_map &&
|
||||
!TEST_FLAG(shader_transparent_generic->generic.flags,
|
||||
_shader_transparent_flag_first_map_is_in_screenspace_bit) &&
|
||||
rasterizer_numeric_mirrored_digit(
|
||||
group->animation->values[function_index],
|
||||
counter_limit,
|
||||
frame_count,
|
||||
group->shader_permutation_index,
|
||||
&bitmap_sequence_index);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (shader_transparent_generic->generic.maps.count > 0)
|
||||
@@ -2034,6 +2123,10 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
if (numeric_mirrored)
|
||||
rasterizer_numeric_mirror_u(vsh_constants__texanim[0]);
|
||||
#endif
|
||||
result = IDirect3DDevice8_SetVertexShaderConstant(
|
||||
global_d3d_device,
|
||||
VSH_CONSTANTS__TEXANIM_OFFSET,
|
||||
@@ -2300,6 +2393,9 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
short layer_index;
|
||||
short map_index;
|
||||
long result;
|
||||
#ifdef HALO_ANDROID
|
||||
boolean numeric_mirrored = FALSE;
|
||||
#endif
|
||||
|
||||
/* BUG (preserved for exact matching): January never advances layer_index, so the
|
||||
* loop redraws extra layer 0 for as long as the block is non-empty (the bytes push
|
||||
@@ -2374,6 +2470,18 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
counter_value /= frame_count;
|
||||
|
||||
bitmap_sequence_index = counter_value%frame_count;
|
||||
#ifdef HALO_ANDROID
|
||||
numeric_mirrored =
|
||||
shader_transparent_chicago->chicago.type==_shader_transparent_chicago_type_2d_map &&
|
||||
!TEST_FLAG(shader_transparent_chicago->chicago.flags,
|
||||
_shader_transparent_flag_first_map_is_in_screenspace_bit) &&
|
||||
rasterizer_numeric_mirrored_digit(
|
||||
group->animation->values[function_index],
|
||||
counter_limit,
|
||||
frame_count,
|
||||
group->shader_permutation_index,
|
||||
&bitmap_sequence_index);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2548,6 +2656,10 @@ void rasterizer_transparent_geometry_group_draw(
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
if (numeric_mirrored)
|
||||
rasterizer_numeric_mirror_u(vsh_constants__texanim[0]);
|
||||
#endif
|
||||
result = IDirect3DDevice8_SetVertexShaderConstant(
|
||||
global_d3d_device,
|
||||
VSH_CONSTANTS__TEXANIM_OFFSET,
|
||||
|
||||
@@ -92,6 +92,9 @@ symbols in this file:
|
||||
#include "physics/collisions.h"
|
||||
#include "units/units.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
/* port/linux/game/vr_graphics.c */
|
||||
void vr_graphics_update(void);
|
||||
#endif
|
||||
#include "main/main.h"
|
||||
#include "structures/structures.h"
|
||||
@@ -948,6 +951,11 @@ void render_frame(
|
||||
|
||||
render.frame_index++;
|
||||
render.time_delta_since_tick_sec = time_delta_since_tick_sec;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: the graphics preset's effects (port/linux/game/vr_graphics.c) */
|
||||
if (halo_vr_active())
|
||||
vr_graphics_update();
|
||||
#endif
|
||||
memset(¶meters, 0, sizeof(parameters));
|
||||
/* continuous between ticks (render_interpolation.c) */
|
||||
parameters.game_time_sec = render_interpolation_game_time_sec(game_time_get());
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
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
|
||||
- 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-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.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ANDROID = ROOT / "port/android"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4 or "/" not in sys.argv[3]:
|
||||
print(__doc__.strip(), file=sys.stderr)
|
||||
return 2
|
||||
apk, output, repository = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3]
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files = [
|
||||
(ANDROID / "steam_frame_install.sh", "install.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, "RingfallMjolnirVR.apk", 0o644),
|
||||
]
|
||||
files += [(picture, "grid/" + picture.name, 0o644)
|
||||
for picture in sorted((ANDROID / "steam_frame_grid").glob("*.png"))]
|
||||
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)
|
||||
info.external_attr = (0o100000 | mode) << 16
|
||||
info.compress_type = zipfile.ZIP_STORED if name.endswith((".apk", ".png")) else zipfile.ZIP_DEFLATED
|
||||
archive.writestr(info, path.read_bytes())
|
||||
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-Ringfall-Mjolnir-VR.desktop"
|
||||
installer.write_text(desktop)
|
||||
installer.chmod(0o755)
|
||||
print(installer)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||