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/mjolnir-vr#install-on-meta-quest-3--3s
|
||||
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
|
||||
@@ -13,6 +13,9 @@ name: Build
|
||||
# - a newer push to the same branch cancels a run still going;
|
||||
# - pushes that only change documentation do not build;
|
||||
# - Windows, whose minutes cost twice Linux's, builds only Windows;
|
||||
# - Mjolnir VR builds only Android (the VR app): the Linux and
|
||||
# Windows jobs are kept, off (if: false), so that upstream's changes to
|
||||
# them still merge;
|
||||
# - artifacts are kept for three days.
|
||||
#
|
||||
# A successful build of main is also published as a GitHub release, the
|
||||
@@ -21,7 +24,10 @@ name: Build
|
||||
# gives the latest build; the last five are kept. Its web build is then the
|
||||
# site on GitHub Pages (https://halocombatevolved.com/, port/web/README.md).
|
||||
on:
|
||||
# (Mjolnir VR: main only; its branches are tried on the headsets
|
||||
# from local builds)
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
@@ -46,6 +52,8 @@ env:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# Arch Linux has what the build needs as packages: clang 22 (for the
|
||||
# profile-guided optimisation in pgo/), and CMake, pkg-config and
|
||||
# wayland-scanner for the SDL 3 that the portable build brings. The build
|
||||
@@ -91,10 +99,7 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py tools/test_server_sim.py tools/test_egg.py tools/test_deploy_gametypes.py
|
||||
# the dedicated server: --version, and --server without maps refused (exit code 2); its cases with
|
||||
# clients need the retail maps, and are skipped
|
||||
python -m pytest -q tools/test_dedicated_server.py
|
||||
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py
|
||||
python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
@@ -198,6 +203,8 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
windows:
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# The runner image has LLVM, Python and Visual Studio's x86 libraries and
|
||||
# Windows SDK.
|
||||
runs-on: windows-2025
|
||||
@@ -272,6 +279,8 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
web:
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# Emscripten (emsdk, the version below, cached) builds the web port's
|
||||
# site (port/web/README.md); the tests check its graph, the page's
|
||||
# importer and service worker, its sockets, that it starts in a
|
||||
@@ -320,8 +329,6 @@ jobs:
|
||||
run: |
|
||||
source ~/emsdk/emsdk_env.sh
|
||||
node --test "port/web/tests/*.test.js"
|
||||
# (the dedicated server's admin page, port/assets/server_ui, against tools/server_sim.py)
|
||||
node --test tools/server_ui_tests/
|
||||
python3 -m pytest -q tools/test_web_build.py
|
||||
|
||||
- name: Test in Chromium and Firefox
|
||||
@@ -354,12 +361,9 @@ jobs:
|
||||
# The eight builds (and the Windows builds' PDBs) as a release, tagged
|
||||
# build-<run number> and marked latest
|
||||
# (every file keeps its name from release to release, for the README's
|
||||
# latest/download links), with their SHA256SUMS and, when the
|
||||
# RELEASE_SIGNING_KEY secret is set, its Ed25519 signature by the release
|
||||
# key (SHA256SUMS.sig; the public key is deploy/release-key.pub); then all
|
||||
# but the newest five build-* releases go, tags and all. Only for main,
|
||||
# and only when every platform built.
|
||||
needs: [linux, android, windows, web]
|
||||
# latest/download links); then all but the newest five build-* releases
|
||||
# go, tags and all. Only for main, and only when every platform built.
|
||||
needs: [android]
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
@@ -371,14 +375,11 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
# (tools/ci_build.py and tools/release_sign.sh, and the public key)
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
tools/ci_build.py
|
||||
tools/release_sign.sh
|
||||
deploy/release-key.pub
|
||||
sparse-checkout-cone-mode: false
|
||||
port/android
|
||||
tools
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -388,28 +389,28 @@ jobs:
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir release
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
# (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/mjolnir-vr-android-$config.zip" .)
|
||||
done
|
||||
python3 tools/ci_build.py sums release
|
||||
# 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/MjolnirVR.apk
|
||||
# the same under the names before "Mjolnir VR" (Ringfall: Mjolnir
|
||||
# VR, builds 302 to 313): their updaters (Updater.java, the Frame's
|
||||
# launcher) and the links already shared ask for them
|
||||
cp release/mjolnir-vr-android-debug.zip release/ringfall-mjolnir-vr-android-debug.zip
|
||||
cp release/mjolnir-vr-android-release.zip release/ringfall-mjolnir-vr-android-release.zip
|
||||
cp release/mjolnir-vr-steam-frame.zip release/ringfall-mjolnir-vr-steam-frame.zip
|
||||
cp release/Install-Mjolnir-VR.desktop release/Install-Ringfall-Mjolnir-VR.desktop
|
||||
cp release/MjolnirVR.apk release/RingfallMjolnirVR.apk
|
||||
ls -l release
|
||||
|
||||
# SHA256SUMS signed with the release key's seed (the RELEASE_SIGNING_KEY
|
||||
# secret, 64 hex digits), and the signature checked against
|
||||
# deploy/release-key.pub before anything is published: a secret that is
|
||||
# not the key of the public key in the repository stops the release
|
||||
- name: Sign the sums
|
||||
env:
|
||||
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
|
||||
run: |
|
||||
if [ -z "$RELEASE_SIGNING_KEY" ]; then
|
||||
echo "::warning::no RELEASE_SIGNING_KEY secret: the release's SHA256SUMS is not signed"
|
||||
exit 0
|
||||
fi
|
||||
sh tools/release_sign.sh sign release/SHA256SUMS
|
||||
sh tools/release_sign.sh verify release/SHA256SUMS
|
||||
|
||||
- name: Publish
|
||||
id: publish
|
||||
env:
|
||||
@@ -419,10 +420,18 @@ jobs:
|
||||
run: |
|
||||
tag="build-$GITHUB_RUN_NUMBER"
|
||||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
|
||||
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
|
||||
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
|
||||
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
||||
{
|
||||
printf '%s\n\n' "$subject"
|
||||
printf '**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 | `MjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
|
||||
printf '| Steam Frame | `Install-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 (those named `ringfall-…` / `Ringfall…`: the same, for the builds before the name Mjolnir VR).\n\n'
|
||||
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
|
||||
} > notes.md
|
||||
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
|
||||
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
--title "Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The Windows builds' executables and PDBs to Sentry, which turns the
|
||||
@@ -464,13 +473,14 @@ jobs:
|
||||
done
|
||||
|
||||
pages:
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# The release's web build to GitHub Pages, which serves it at
|
||||
# https://halocombatevolved.com/ (port/web/README.md, "Hosting"). Only
|
||||
# main, only once every platform built and the release is out (the site
|
||||
# and the release are one build), and a deployment is never cancelled
|
||||
# halfway. The repository's Pages source must be GitHub Actions.
|
||||
needs: [release]
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
@@ -541,8 +551,8 @@ jobs:
|
||||
# A Discord embed for the run, to the DISCORD_WEBHOOK secret (nothing
|
||||
# without it): green when every platform built, red when one did not,
|
||||
# with each platform's result, this run's release (main only), and links
|
||||
# to the latest release's builds and the site.
|
||||
needs: [linux, android, windows, web, release, pages]
|
||||
# to the latest release's builds.
|
||||
needs: [android, release]
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
@@ -578,13 +588,13 @@ jobs:
|
||||
*) echo "➖ Skipped" ;;
|
||||
esac
|
||||
}
|
||||
if [ "$LINUX" = success ] && [ "$ANDROID" = success ] && [ "$WINDOWS" = success ] && [ "$WEB" = success ]; then
|
||||
if [ "$ANDROID" = success ]; then
|
||||
title="Build succeeded"; color=5763719
|
||||
else
|
||||
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) · [Web]($SITE_URL) ([all builds]($latest))"
|
||||
downloads="[Quest]($latest/download/mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||||
if [ "$PAGES" = success ]; then
|
||||
|
||||
@@ -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,327 +1,227 @@
|
||||
# OpenCE - Halo: Combat Evolved for Linux, Windows, Android and the web
|
||||
|
||||
<p align="center">
|
||||
<img src="port/assets/icon/opence-icon.png" alt="OpenCE" width="128">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Mjolnir VR">
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/OpenCommunityEdition/OpenCE/releases/latest"><img src="https://img.shields.io/github/v/release/OpenCommunityEdition/OpenCE?label=Download&style=for-the-badge" alt="Download"></a>
|
||||
<a href="https://halocombatevolved.com/"><img src="https://img.shields.io/badge/Play-in_browser-blue?style=for-the-badge" alt="Play in browser"></a>
|
||||
<a href="https://discord.gg/9gqcHyr5km"><img src="https://img.shields.io/badge/Discord-join-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
**OpenCE** (Open Community Edition) is an open source port of the Halo: Combat Evolved decompilation (Halo CE) based on an early Xbox development build (2342) and forked from
|
||||
[bnunu/halo-1](https://github.com/bnunu/halo-1). The project aims to enhance the existing game and adds many features, such as 128-player multiplayer, online campaign co-op, mod packs, field-of-view settings, a server browser, peer-to-peer hosting, automatic map downloads, voice chat, support for Custom Edition maps and much more!
|
||||
|
||||
The game runs natively on **Windows, Linux, Android** and in the **web browser** with nothing to install. A macOS port is in progress. Worth noting that it is not an emulator, **the game's own code is compiled for each platform** with a new layer in place of the Xbox's hardware. Runs on OpenGL (Android is on OpenGL ES and WebGL 2 in the browser). Sound, input, gamepads are all handled through SDL3. The game currently draws a frame every refresh of your display with Smooth Motion enabled, while the simulation runs at 30 ticks just like the original Xbox.
|
||||
|
||||
<img width="1284" height="1007" alt="The game running on Windows" src="https://github.com/user-attachments/assets/0a4412e1-f6d3-48ed-8e17-0a374c2122f1" />
|
||||
<p align="center"><em>OpenCE running on Windows (release build)</em></p>
|
||||
|
||||
## A copy of Halo: Combat Evolved is required
|
||||
|
||||
OpenCE does not include the game data. You **must provide your own Xbox disc image** (`.xiso` or `.iso`) of Halo: Combat Evolved. Every version of the game works. The maps of the European (PAL) version were made for a slower console - the game changes them to play as the North American (NTSC) maps do, so players of the two versions can play together.
|
||||
|
||||
1. Start the game (`halo.exe` on Windows and `halo` on Linux).
|
||||
2. At the first start you will get a pop up message asking for the disc image. Select it in the
|
||||
file picker. On Linux and Windows, a disc image (`.iso` or `.xiso`) next
|
||||
to the executable is used without the file picker. Where no file picker
|
||||
can be shown (on Linux, without the desktop portal or `zenity`), the game
|
||||
says to put the disc image next to the executable, and stops.
|
||||
3. The game extracts the `maps` folder. Then the game starts.
|
||||
|
||||
The `maps` folder holds the Xbox maps only. If it holds Halo PC's maps
|
||||
instead (a Halo PC or Custom Edition install's `maps` folder), the game says
|
||||
so and stops: Custom Edition maps go in `custom_maps` folder, beside `maps` folder.
|
||||
|
||||
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). On
|
||||
the web, the page copies `maps/` out of the disc image into the browser's
|
||||
storage for the site - nothing is uploaded ([port/web/README.md](port/web/README.md)).
|
||||
|
||||
## Download
|
||||
|
||||
GitHub Actions builds the game for each commit.
|
||||
|
||||
Builds for latest release:
|
||||
|
||||
| Platform | Release | Debug | Guide |
|
||||
| --- | --- | --- | --- |
|
||||
| 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) | [Linux](port/linux/README.md) |
|
||||
| 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) | [Windows](port/windows/README.md) |
|
||||
| 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) | [Android](port/android/README.md) |
|
||||
| Web | [halocombatevolved.com](https://halocombatevolved.com/) (or [halo-web-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-release.zip) for your own server) | [halo-web-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-debug.zip) | [Web](port/web/README.md) |
|
||||
|
||||
|
||||
Release build is recommended for regular play.
|
||||
|
||||
The debug build stops at the first failed check (assertion) and writes it to the log, so use it when you want 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). The web site offers
|
||||
each new build when it is out.
|
||||
|
||||
Each build of the `main` branch that passes on all four platforms is a new
|
||||
release, and the web site then plays it. 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.
|
||||
|
||||
Each release also has `SHA256SUMS`, the SHA-256 of each of its files, and
|
||||
`SHA256SUMS.sig`, that file's Ed25519 signature by the project's release
|
||||
key, whose public key is [deploy/release-key.pub](deploy/release-key.pub).
|
||||
`sha256sum -c --ignore-missing SHA256SUMS` checks the downloads, and
|
||||
`tools/release_sign.sh verify SHA256SUMS` (OpenSSL 3) the signature.
|
||||
|
||||
The Linux README also covers the controls, every setting and the multiplayer features. These are almost the same on all platforms. The settings are in `config.toml` next to the game, and many are in the menus too.
|
||||
|
||||
The Arch-based distribution release is available on the [AUR](https://aur.archlinux.org/packages/opence-bin).
|
||||
|
||||
## Multiplayer
|
||||
|
||||
- **Supports up to 128 players** on up to 128 machines and up to 4 players on each device in split screen.
|
||||
- **Every platform plays together:** Windows, Linux, Android and the browser all support cross-play! On a local network, the native builds can play LAN. Browser players can play each other in rooms over WebRTC ([port/web/README.md](port/web/README.md)).
|
||||
- **Peer-to-peer internet hosting:** join through the server browser or an invite link. A host lists a game as public in the server browser, or keeps it private for players with the invite link. With the Discord integration, the host can also send the invite through Discord.
|
||||
- **Online co-op campaign:** with settings for friendly fire, player collisions and the number of enemies as a multiplier based on per player or static.
|
||||
- **New gametypes:** SWAT, Infection, Gun Game and Hardcore (Slayer, CTF, Oddball and King), in Select Gametype's OPENCE bank; Edit Gametypes' OPENCE OPTIONS gives any gametype their rules ([port/linux/README.md](port/linux/README.md), "OpenCE gametypes").
|
||||
- **Voice Chat:** push to talk (V) or an open microphone (can be changed in the menu's audio settings)
|
||||
- **Map sharing:** a player who does not have the game's Custom Edition map downloads it from the other players.
|
||||
- **Moderation:** the host can kick and ban players from the scoreboard menu or the `kick` and `ban` console commands. Players can vote to kick.
|
||||
- **Scrollable Scoreboard:** the mouse wheel or PageUp / PageDown scrolls through list of players in large multiplayer lobbies.
|
||||
- **A new netcode:** each machine moves its own player at once, and the host makes the decision for the game. Refer to [port/linux/NETCODE.md](port/linux/NETCODE.md).
|
||||
|
||||
## Custom Edition Maps
|
||||
|
||||
Halo Custom Edition maps are placed in `custom_maps` folder, not in the `maps` folder itself. It requires `bitmaps.map`, `sounds.map`, and `loc.map` in the `custom_maps` folder, which you must copy over from your existing installed copy of Halo Custom Edition. The maps folder is only for Xbox maps that's extracted from your disc image, not Halo Custom Edition.
|
||||
|
||||
Halo PC Gearbox exclusive maps (Ice Fields, Death Island, Danger Canyon, Infinity, Timberland and Gephyrophobia) **are not included by default**. They would need to be copied over from your existing Halo Custom Edition install's `maps` folder into `custom_maps` folder too.
|
||||
|
||||
Once that's setup, the maps will appear on CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER in the map list in Campaign's New Game, and on the Map screen of Create Game (Internet or LAN). A CUSTOM SINGLEPLAYER map plays as a campaign level, alone or as
|
||||
online co-op.
|
||||
|
||||
Maps that require OpenSauce are not compatible.
|
||||
|
||||
Both the server browser and the LAN browser support automatic map downloads - when you join a game it will download the custom maps from other players, with any of `bitmaps.map`, `sounds.map` and `loc.map` you are missing. This works on a LAN and on the
|
||||
Internet, when the host has map sharing enabled (on by default).
|
||||
Refer to [docs/custom_edition_caches.md](docs/custom_edition_caches.md).
|
||||
|
||||
## Mods
|
||||
|
||||
Mods go in a `mods` folder next to `maps`. Each mod is a folder of Halo PC tag files, as the Halo Editing Kit and Invader make them, with a `mod.toml` describing it. Every mod in the folder is on by default. Mods in the main menu turns them on and off and sets their order. Think of it like a resource pack list, the mod at the top plays over the ones below it. The game also refuses mods that say they cannot be played together.
|
||||
|
||||
Refer to [docs/mods.md](docs/mods.md) to make your own.
|
||||
|
||||
|
||||
## Graphics
|
||||
|
||||
The game retains the original look and feel as it did on the Xbox by default. The video settings menus have additional options that go beyond it.
|
||||
|
||||
**Display**
|
||||
|
||||
| Option | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| DISPLAY MODE, RESOLUTION, WINDOW SIZE | Fullscreen, borderless or windowed, at any resolution (F11 switches) | Borderless, at the display's resolution |
|
||||
| RESOLUTION SCALING | Draw at the window's resolution, or at the Xbox's 640x480 and scale it up | Native resolution |
|
||||
| V-SYNC, FRAME RATE LIMIT | Wait for the display, or run free up to a frame cap; works with G-Sync and FreeSync | V-sync on |
|
||||
| SMOOTH MOTION | A frame for every refresh of the display (60, 120, 240 Hz and more), blended between the game's 30 ticks a second | On |
|
||||
| INSTANT AIM | In first person, the view turns the frame the mouse moves, not up to two ticks later | On |
|
||||
| FOV, VIEWMODEL FOV | Field of view of the view and of the weapon and hands, from 80 to 150 degrees | Stock |
|
||||
|
||||
**Image quality**
|
||||
|
||||
| Option | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| ANTI-ALIASING | FXAA, SMAA, MSAA or SSAA | Off, as on the Xbox |
|
||||
| TEXTURE FILTERING | Anisotropic filtering, from 2 to 16 - provides sharper floors, walls and terrain at an angle | Off, as on the Xbox |
|
||||
| SHADOW RESOLUTION | Object shadows at 128 (the Xbox's), 256, 512 or 1024 pixels | 128 |
|
||||
| PER-PIXEL LIGHTING | Lights models for each pixel, without the facets of the Xbox's per-vertex lighting | Off |
|
||||
| MODEL DETAIL | Keep more model detail at a distance at high resolutions, or always the most detailed | Stock |
|
||||
| HDR, HDR DISPLAY | Keeps explosions, plasma and the sun's glare past white and tone maps them; on an HDR monitor, shows them brighter than white | Off |
|
||||
| HIGH-RES HUD, HIGH-RES TEXT | The HUD from hand-drawn redraws at 8x the size, and text in the Overpass font, sharp at any resolution | On |
|
||||
|
||||
**Extras**
|
||||
|
||||
| Option | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| LEGS | See your own legs when you look down in first person | Off |
|
||||
| ARM SHIELDS | The energy shield's flare on your first-person arms too | Off |
|
||||
| SHIELD GLOW, GLOW INTENSITY | Energy shields light up what is around them as they flare | Off |
|
||||
| SHIELD COLOR | Your own shield in a multiplayer armor color, as your machine draws it | Default |
|
||||
| OVERSHIELD | The overshield's own color, rising up the body as it charges | Off |
|
||||
| PASSENGER THIRD PERSON | Third-person view for Warthog and Scorpion passengers (Campaign-only) | Off |
|
||||
|
||||
|
||||
## How to 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, or on Linux after `configure.py --windows-cross`) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
| `ninja web` | `build/web/site/`, the web site (`python tools/web_serve.py` serves it) |
|
||||
|
||||
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. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). 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". |
|
||||
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
|
||||
| `--web-emcc PATH` | The Emscripten `emcc` for `ninja web` (else the one on the PATH, or `~/emsdk`'s). |
|
||||
|
||||
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.
|
||||
|
||||
## For developers
|
||||
|
||||
### Testing
|
||||
|
||||
The tests are listed in [AGENTS.md](AGENTS.md) ("Testing"). Most run
|
||||
without the game data, in GitHub Actions too.
|
||||
|
||||
One check needs the retail maps, so it runs on your own computer (or on a
|
||||
self-hosted runner), not in GitHub Actions: `tools/net_compare.py` plays
|
||||
two builds together in a network game on one computer and checks that they
|
||||
stay in step. Run it when a change can alter how a build calculates the
|
||||
game, as a change to the simulation, the compiler or the maths does,
|
||||
before it is merged. For example, to check a build with such a change
|
||||
against a build without it:
|
||||
|
||||
```sh
|
||||
python tools/net_compare.py --a build/linux/halo --b <other checkout>/build/linux/halo \
|
||||
--data <folder with maps/> --maps bloodgulch,beavercreek,hangemhigh --swap
|
||||
```
|
||||
|
||||
Each game prints PASS or FAIL. A FAIL names the first player, tick and
|
||||
value that differ. Refer to "Comparing two builds" in
|
||||
[port/linux/NETCODE.md](port/linux/NETCODE.md).
|
||||
|
||||
### 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/`.
|
||||
|
||||
### Profiling builds
|
||||
|
||||
A profiling build records where the game spends its time.
|
||||
|
||||
1. Enter `python configure.py --profile`, then build as usual.
|
||||
2. Start a recording. Enter `profile_record` in the console (or the telnet
|
||||
console), or set `debug.profile_record = true` in `config.toml`. To
|
||||
record each game of a session, also set
|
||||
`debug.profile_record_when = "game"`.
|
||||
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
|
||||
|
||||
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
|
||||
and logs their paths. Parts are retained after the recording stops. Logs go to
|
||||
standard error; a Windows release build without standard error writes them to
|
||||
`halo.log` next to `halo.exe`.
|
||||
|
||||
`tools/net_report.py` is a helper for developers. Enter
|
||||
`python tools/net_report.py <recording.part1.json>` to read a recording
|
||||
as tables. The command also writes `<recording>.summary.txt`, a short text
|
||||
file.
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
|
||||
| `profile_stop` | Stops the recording. |
|
||||
|
||||
`debug.profile_memory` sets the memory of a recording in MB (default 256,
|
||||
from 4 to 1024). A recording has no length limit:
|
||||
the game writes a part each time the memory is full. Refer to "Settings" in
|
||||
[port/linux/README.md](port/linux/README.md).
|
||||
|
||||
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
|
||||
Enter `adb pull` to copy the recording parts to a computer.
|
||||
|
||||
A profiling build plays with normal builds. It does not use
|
||||
`--pgo=train`.
|
||||
|
||||
# Mjolnir VR
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
> **Unofficial fan project.** 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).
|
||||
|
||||
Until build 313 it was called *Ringfall: Mjolnir VR*. Installed builds
|
||||
update to the new name by themselves, and the old links lead here.
|
||||
|
||||
## Contents
|
||||
|
||||
- [What you get](#what-you-get)
|
||||
- [What you can play](#what-you-can-play)
|
||||
- [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)
|
||||
|
||||
## What you get
|
||||
|
||||
- **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**).
|
||||
- **The PC version's menus**, clicked with a laser pointer, and an
|
||||
on-screen keyboard for names, codes and passwords.
|
||||
|
||||
## What you can play
|
||||
|
||||
- **The whole campaign**: all ten missions, on every difficulty, saved as
|
||||
you go.
|
||||
- **Co-op campaign** with friends, online or on your local network, with
|
||||
settings for friendly fire and extra enemies per player.
|
||||
- **LAN multiplayer** (system link) with everyone on your network.
|
||||
- **Online multiplayer**, no server of your own needed: host a game and
|
||||
share its join code, find public games in the server browser, or protect
|
||||
a lobby with a password. Up to 128 players.
|
||||
- **Bots** to fill a game, on foot, in vehicles and at turrets; they make
|
||||
room when a person joins.
|
||||
- **All 13 multiplayer maps** and the Xbox game's game types: Slayer,
|
||||
Capture the Flag, King of the Hill, Oddball and Race.
|
||||
- **Cross-play**: VR players on the Quest and the Steam Frame, together with
|
||||
players of the PC and phone builds of
|
||||
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
|
||||
|
||||
## What you need
|
||||
|
||||
- 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. The game plays in the disc's language; on the European disc in
|
||||
Spanish and Italian it takes your headset's language (else the first).
|
||||
|
||||
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.
|
||||
|
||||
## Install on Meta Quest 3 / 3S
|
||||
|
||||
You install the game once with a computer (Windows, macOS or Linux); after
|
||||
that it updates itself.
|
||||
|
||||
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 `MjolnirVR.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 **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.
|
||||
|
||||
You can delete the disc image from `Download` afterwards. When a new
|
||||
version comes out, the game asks you at start whether to update.
|
||||
|
||||
## Install on Steam Frame
|
||||
|
||||
No computer and no keyboard needed.
|
||||
|
||||
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-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 **Mjolnir VR** to your Steam
|
||||
library and offers to switch back to Gaming Mode.
|
||||
6. Start **Mjolnir VR** from your library (under *Non-Steam*).
|
||||
|
||||
The game updates itself: when a new version is out, it is downloaded
|
||||
while you play and used from the next start on. Your settings, saves and
|
||||
game data stay. More about the Steam Frame:
|
||||
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
|
||||
|
||||
## Playing
|
||||
|
||||
- **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.
|
||||
- **Voice chat** in network games: hold the grip of the hand without the
|
||||
weapon to talk (not while it holds the weapon or the Warthog's wheel).
|
||||
The *BUTTONS* page can put push to talk on another button.
|
||||
|
||||
## Problems and questions
|
||||
|
||||
Please open an [issue](../../issues) and include:
|
||||
|
||||
- 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).
|
||||
|
||||
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
|
||||
|
||||
**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.
|
||||
|
||||
## Credits
|
||||
|
||||
The decompilation is of the Xbox build 2342 (`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
OpenCE starts from the decompilation of
|
||||
[bnunu/halo-1](https://github.com/bnunu/halo-1), which is a fork of
|
||||
[punpckhdq/halo](https://github.com/punpckhdq/halo). The artists of the
|
||||
high-res redraws are in [AUTHORS.md](AUTHORS.md). The main contributors are Jonas Volman (@bnunu) and MrBruh (@cybersecurity). Everyone else who has contributed is on the
|
||||
[contributors page](https://github.com/OpenCommunityEdition/OpenCE/graphs/contributors).
|
||||
Mjolnir VR stands on the work of others:
|
||||
|
||||
## Licence
|
||||
- **[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.
|
||||
- **[xboxdevwiki](https://xboxdevwiki.net/XDVDFS)**: its description of
|
||||
the Xbox's disc file system, which the disc image readers are written
|
||||
from.
|
||||
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
OpenCE is free software: you can give it to others and change it under the
|
||||
terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, version 3 of the License or (at your option) any later
|
||||
version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in
|
||||
[LICENSE](LICENSE).
|
||||
Thank you all.
|
||||
|
||||
- **From the change.** The AGPL applies from the commit that made the change,
|
||||
and to each release built from that commit or a later one. Earlier
|
||||
releases, and the source at every earlier commit, were released under
|
||||
[CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way.
|
||||
- **What OpenCE builds on.** The decompilation of
|
||||
[punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0
|
||||
from 17 October 2026, and under CC0 1.0 before that date.
|
||||
[bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0.
|
||||
- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a
|
||||
project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and
|
||||
the AGPL 3.0 let the two be combined (section 13 of each).
|
||||
## Building it
|
||||
|
||||
`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.
|
||||
|
||||
## Legal
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Licence
|
||||
|
||||
Mjolnir VR is free software: you can give it to others and change
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, version 3 of the License or (at your option)
|
||||
any later version (`AGPL-3.0-or-later`), as OpenCE, which it builds on, is.
|
||||
It comes with no warranty. The full text is in [LICENSE](LICENSE).
|
||||
|
||||
- **From build 312 on.** The AGPL applies from the commit that took in
|
||||
OpenCE's change of licence (19 October 2026), and to each release built
|
||||
from it or a later one. Earlier releases, and the source at every earlier
|
||||
commit, were released under [CC0 1.0](LICENSES/CC0-1.0.txt) and stay
|
||||
that way.
|
||||
- **Hosts and servers.** If you change the game and let other persons play
|
||||
with your version over a network (for example, as the host of an internet
|
||||
game, or on a web site), the AGPL asks you to offer them the source of
|
||||
your version (section 13). The game as released has its source here, and
|
||||
each release's source is its tag (`build-<n>`).
|
||||
- **Third-party code, fonts and art** keep their own licences, which are
|
||||
next to them (`port/third_party`, `port/assets/fonts`,
|
||||
`port/assets/icons`, `port/assets/text_filter`). The release downloads and
|
||||
the web site's `licenses.txt` carry their notices.
|
||||
- **Halo is not covered.** The licence covers the work of OpenCE's authors.
|
||||
It gives no rights in Halo, its maps or anything else that Microsoft owns.
|
||||
|
||||
|
||||
## Disclaimer
|
||||
OpenCE (Open Community Edition) is an unofficial fan project. It is not endorsed by or affiliated with Microsoft, Xbox Game Studios, Halo Studios, Activision or Bungie. Halo and Halo: Combat Evolved are trademarks of Microsoft Corporation; Halo © Microsoft Corporation.
|
||||
No game data is included. You must provide your own copy of the game.
|
||||
with your version over a network (for example, as the host of an
|
||||
internet game), the AGPL asks you to offer them the source of your
|
||||
version. The game as released has its source here, and each release's
|
||||
source is its tag (`build-<n>`).
|
||||
- **Third-party code and fonts** keep their own licences: see
|
||||
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
- **Halo is not covered.** The licence covers the work of this project's
|
||||
and OpenCE's authors. It gives no rights in Halo, its maps or anything
|
||||
else that Microsoft owns.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Third-party notices
|
||||
|
||||
Mjolnir VR is released under the GNU Affero General Public
|
||||
License, version 3 or later ([LICENSE](LICENSE)); releases before build 312
|
||||
were released under [CC0 1.0](LICENSES/CC0-1.0.txt). 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 | AGPL 3.0 or later (OpenCE from 19 October 2026, punpckhdq/halo from 17 October 2026; CC0 1.0 before); bnunu/halo-1: CC0 1.0 | [LICENSE](LICENSE), [LICENSES/CC0-1.0.txt](LICENSES/CC0-1.0.txt) |
|
||||
| [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) |
|
||||
|
||||
The disc image readers (`XisoExtractor.java`, `steam_frame_xiso.py`,
|
||||
`xiso.c`) are written from a description of the Xbox's disc file system
|
||||
([xboxdevwiki's XDVDFS page](https://xboxdevwiki.net/XDVDFS)), not from
|
||||
extract-xiso, which earlier releases followed.
|
||||
|
||||
## 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,240 @@
|
||||
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
|
||||
> the port 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, Android and the web
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows, Android and web browsers. 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) |
|
||||
| Web | [halocombatevolved.com](https://halocombatevolved.com/) (or [halo-web-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-release.zip) for your own server) | [halo-web-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-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). The web site offers
|
||||
each new build when it is out.
|
||||
|
||||
Each build of the `main` branch that passes on all four platforms is a new
|
||||
release, and the web site then plays it. 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.
|
||||
|
||||
### Arch-based Linux Distributions
|
||||
|
||||
The above Linux binary release is available on the [AUR](https://aur.archlinux.org/packages/opence-bin).
|
||||
|
||||
## Game data
|
||||
|
||||
The port does not include the game data. Download an Xbox disc image
|
||||
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](port/android/README.md). On
|
||||
the web, the page copies `maps/` out of the disc image into the browser's
|
||||
storage for the site; nothing is uploaded. Refer to
|
||||
[port/web/README.md](port/web/README.md).
|
||||
|
||||
## Mods
|
||||
|
||||
Mods are folders of Halo PC tag files, as the Halo Editing Kit and Invader
|
||||
write them, in `mods/` next to `maps/`: texture, sound, text and model packs
|
||||
that play over the maps' on your machine only, in any game. Each has a
|
||||
`mod.toml`. Every mod in the folder is on; the main menu's Mods turns them
|
||||
off and on and sets the order they load in.
|
||||
Refer to [docs/mods.md](docs/mods.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) |
|
||||
| Web (WebAssembly, WebGL 2, SDL3; desktop Chrome, Edge and Firefox) | [port/web/README.md](port/web/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 and browsers can play in the same
|
||||
game. On a local network, the native builds play system link; browsers
|
||||
play each other in rooms, over WebRTC
|
||||
([port/web/README.md](port/web/README.md)).
|
||||
- An invite link, or the server browser, lets a machine join a game on the
|
||||
internet, browsers too: a native build takes a browser's WebRTC
|
||||
([port/linux/README.md](port/linux/README.md), "Browsers"). 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` |
|
||||
| `ninja web` | `build/web/site/`, the web site (`python tools/web_serve.py` serves it) |
|
||||
|
||||
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. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). 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". |
|
||||
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
|
||||
| `--web-emcc PATH` | The Emscripten `emcc` for `ninja web` (else the one on the PATH, or `~/emsdk`'s). |
|
||||
|
||||
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/`.
|
||||
|
||||
### Profiling builds
|
||||
|
||||
A profiling build records where the game spends its time.
|
||||
|
||||
1. Enter `python configure.py --profile`, then build as usual.
|
||||
2. Start a recording. Enter `profile_record` in the console (or the telnet
|
||||
console), or set `debug.profile_record = true` in `config.toml`. To
|
||||
record each game of a session, also set
|
||||
`debug.profile_record_when = "game"`.
|
||||
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
|
||||
|
||||
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
|
||||
and logs their paths. Parts are retained after the recording stops. Logs go to
|
||||
standard error; a Windows release build without standard error writes them to
|
||||
`halo.log` next to `halo.exe`.
|
||||
|
||||
`tools/net_report.py` is a helper for developers. Enter
|
||||
`python tools/net_report.py <recording.part1.json>` to read a recording
|
||||
as tables. The command also writes `<recording>.summary.txt`, a short text
|
||||
file.
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
|
||||
| `profile_stop` | Stops the recording. |
|
||||
|
||||
`debug.profile_memory` sets the memory of a recording in MB (default 256,
|
||||
from 4 to 1024). A recording has no length limit:
|
||||
the game writes a part each time the memory is full. Refer to "Settings" in
|
||||
[port/linux/README.md](port/linux/README.md).
|
||||
|
||||
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
|
||||
Enter `adb pull` to copy the recording parts to a computer.
|
||||
|
||||
A profiling build plays with normal builds. It does not use
|
||||
`--pgo=train`.
|
||||
|
||||
## Licence
|
||||
|
||||
OpenCE is free software: you can give it to others and change it under the
|
||||
terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, version 3 of the License or (at your option) any later
|
||||
version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in
|
||||
[LICENSE](LICENSE).
|
||||
|
||||
- **From the change.** The AGPL applies from the commit that made the change,
|
||||
and to each release built from that commit or a later one. Earlier
|
||||
releases, and the source at every earlier commit, were released under
|
||||
[CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way.
|
||||
- **What OpenCE builds on.** The decompilation of
|
||||
[punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0
|
||||
from 17 October 2026, and under CC0 1.0 before that date.
|
||||
[bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0.
|
||||
- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a
|
||||
project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and
|
||||
the AGPL 3.0 let the two be combined (section 13 of each).
|
||||
- **Hosts and servers.** If you change the game and let other persons play
|
||||
with your version over a network (for example, as the host of an internet
|
||||
game, or on a web site), the AGPL asks you to offer them the source of
|
||||
your version (section 13). The game as released has its source here, and
|
||||
each release's source is its tag (`build-<n>`).
|
||||
- **Third-party code, fonts and art** keep their own licences, which are
|
||||
next to them (`port/third_party`, `port/assets/fonts`,
|
||||
`port/assets/icons`, `port/assets/text_filter`). The release downloads and
|
||||
the web site's `licenses.txt` carry their notices.
|
||||
- **Halo is not covered.** The licence covers the work of OpenCE's authors.
|
||||
It gives no rights in Halo, its maps or anything else that Microsoft owns.
|
||||
@@ -1,5 +1,8 @@
|
||||
# Android
|
||||
|
||||
The app of Mjolnir VR (application id `io.github.cesarus85.ringfall`, from
|
||||
the name before, "Ringfall: Mjolnir VR", kept so that installed apps update;
|
||||
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`.
|
||||
@@ -54,9 +57,9 @@ Your own builds all have the same signature (the debug key of WSL,
|
||||
(`adb install -r`).
|
||||
|
||||
To keep the saved games before you remove the app, enter
|
||||
`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles).
|
||||
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
|
||||
After the new install, before the first start of the app, enter
|
||||
`adb push u /sdcard/Android/data/com.halo.decomp/files/save/u`. The app reads
|
||||
`adb push u /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u`. The app reads
|
||||
and overwrites what adb pushes. adb cannot add files to `save/u` or `save/z` once the app made them, or
|
||||
change files that the app wrote, so push before the first start. adb cannot read
|
||||
some files that the app writes (the `blam.lst` files of `save/z`), so `adb pull`
|
||||
@@ -75,8 +78,8 @@ of the whole `save` folder stops there.
|
||||
## 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:
|
||||
|
||||
@@ -91,9 +94,9 @@ 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` |
|
||||
| Mods (`docs/mods.md`) | `mods`: `adb push <mod folder> /sdcard/Android/data/com.halo.decomp/files/mods/`; the main menu's Mods orders them and turns them off |
|
||||
@@ -101,7 +104,7 @@ To install the data from a computer:
|
||||
| Settings | `config.toml` |
|
||||
|
||||
To make a copy of the saved games, enter
|
||||
`adb pull /sdcard/Android/data/com.halo.decomp/files/save/u` (the profiles).
|
||||
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
|
||||
An `adb pull` of the whole `save` folder stops at the `blam.lst` files of
|
||||
`save/z`, which adb cannot read.
|
||||
|
||||
@@ -211,9 +214,9 @@ Removing the app's data or the app removes the layout.
|
||||
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.
|
||||
@@ -227,6 +230,8 @@ These settings are only for Android:
|
||||
| `input.touch_aim_assist` | `true` (the default): the touch controls' swipe aiming gets the aim assist of a controller (the aim slows over a target and follows a moving one). `false`: none, as with a mouse; the bullets' own autoaim stays. |
|
||||
| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps in any case. |
|
||||
| `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. |
|
||||
| `display.shader_cache` | `true` (the default): the linked shader programs are kept in `shader_cache` in the data folder (their binaries, the driver that made them and their sources), and each map's list of the programs it drew with; as a map loads, the programs on its list are made ready, so that its first frames do not wait for the driver to compile them. A driver update compiles them again from the kept sources. `false`: each program is compiled the first time it is drawn. The folder may be deleted at any time. |
|
||||
| `display.shader_worker` | `true` (the default): the shader programs are made on a thread of their own, whose GL context shares the game's (loaded from the shader cache, or compiled), and drawn with once there (`debug.shader_warm_up`: `"worker"`, `"load"` on the game's thread as a map loads, or `"off"`); a draw whose program is not made yet is left out for the frames it takes, and the frame does not wait. As a map loads, its own programs and then every map's (`shader_cache/all.txt`) are asked for. `false`: each is made on the game's thread when first drawn with. |
|
||||
| `debug.sample_seconds` | Refer to "Find problems". |
|
||||
| `debug.memory_watch` | `true` (the default): the app notices the game's writes to cached textures by page protection. `false`: it compares page contents once a frame instead, which is slower. Geometry uses the streaming path in either mode. Refer to "Limits". |
|
||||
|
||||
@@ -262,14 +267,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/u`.
|
||||
|
||||
@@ -287,6 +300,75 @@ the Xbox:
|
||||
|
||||
All the ports draw this way.
|
||||
|
||||
## VR
|
||||
|
||||
On a VR headset with an OpenXR runtime (Meta Quest 3 and 3S; the Steam
|
||||
Frame runs Android apps in Lepton) the same APK plays in VR, with 6DoF head
|
||||
tracking and the Touch controllers. The launcher, a flat panel on the
|
||||
headset, imports the game data as on a phone and then starts the game
|
||||
immersive. `vr.enabled = false` in `config.toml` shows it on a flat panel
|
||||
instead.
|
||||
|
||||
Controls in play (in the menus the buttons are the Xbox's by position):
|
||||
|
||||
| Controller | Game |
|
||||
| --- | --- |
|
||||
| right trigger / left trigger | fire / throw grenade |
|
||||
| right grip / left grip | melee / switch weapon |
|
||||
| A / B | jump / action, reload |
|
||||
| X / Y | switch grenade / flashlight |
|
||||
| left stick, click | move, crouch |
|
||||
| right stick, click | turn (snap or smooth), zoom |
|
||||
| menu | pause |
|
||||
|
||||
You aim with your head; turning your head turns the player, and the right
|
||||
stick turns you further. Recentre with the headset's own gesture (holding
|
||||
the Meta button). The `[vr]` settings in `config.toml` choose snap or
|
||||
smooth turning, the resolution, the refresh rate, and where the HUD and the
|
||||
screen for menus and cinematics float.
|
||||
|
||||
### How VR works
|
||||
|
||||
`host_main.c` asks the Khronos OpenXR loader for a headset before the game
|
||||
starts. If there is one, the game keeps the Xbox's 640x480 layout but draws
|
||||
it at the eye images' resolution (`d3d8_gl.c`, `screen_mode_choose`), and
|
||||
`port/linux/src/vr.c` drives the session the host keeps
|
||||
(`host/host_xr.c`, which the guest calls through the import stubs;
|
||||
`include/halo_xr_abi.h` has the structures both sides share):
|
||||
|
||||
- the frame starts when the last one has been presented
|
||||
(`platform_pump_events`), before the game reads its input: the host waits
|
||||
for the headset's frame and reports where the eyes, the head and the
|
||||
controllers will be when it is shown;
|
||||
- the player's facing follows the head (`halo_vr_aim` in
|
||||
`player_control.c`); a "body" yaw that the right stick turns maps the
|
||||
headset's space onto the game's;
|
||||
- `render.c` draws the player's view once per eye, from the eye's pose
|
||||
placed at the game's camera, through the eye's asymmetric frustum
|
||||
(`frustum_bounds`, as the tiled screenshots use), leaving out the HUD;
|
||||
each eye is copied to its swapchain;
|
||||
- the back buffer is then cleared to transparent black, and the HUD and the
|
||||
menus drawn over nothing; Present copies it to a quad layer that follows
|
||||
the head (premultiplied alpha, so the HUD's additive parts add to the
|
||||
scene), or, while a menu is up, stands in the room;
|
||||
- frames without a player's view (the main menu, loading, cinematics,
|
||||
unless `vr.cinematics_in_3d`) go whole onto that screen in the room.
|
||||
|
||||
The game draws in gamma space; the swapchains are sRGB, so the host copies
|
||||
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/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
|
||||
in the log (`adb logcat -s halo`). Delete the file to play.
|
||||
|
||||
Not done yet: aiming with the controller and the weapon in the hand, the
|
||||
HUD's parts on the controllers, zoom through the scope, a first-person view
|
||||
in vehicles, and a separate depth for the HUD's reticle.
|
||||
|
||||
## How the port operates
|
||||
|
||||
### ILP32 code
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Steam Frame
|
||||
|
||||
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-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 "Mjolnir VR" to the Steam library
|
||||
and switches back to Gaming Mode. Start it from the library there.
|
||||
|
||||
The game updates itself: while it runs, its launcher looks for a newer
|
||||
release, fetches it and puts it in place as the next start begins
|
||||
(`steam_frame_launch.sh`; `update.auto = false` in `config.toml` stops it;
|
||||
`launcher.log` says what it did). Opening the same file again updates it
|
||||
too (settings, saves and game data stay).
|
||||
|
||||
The desktop file (`steam_frame_installer.desktop`) downloads the latest
|
||||
release's `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. (These keep the name of builds 302 to 313, "Ringfall:
|
||||
Mjolnir VR": the Steam shortcut of an installed game starts the launcher
|
||||
there, and the launcher's update looks for them in the bundle.);
|
||||
- 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
|
||||
`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
|
||||
Development" from the library. While the headset sleeps Lepton stops, and
|
||||
adb with it.
|
||||
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
|
||||
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
|
||||
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
|
||||
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 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.
|
||||
|
||||
## What the port does for it
|
||||
|
||||
- SDL3's clipboard handler takes a clipboard service for granted, which
|
||||
Lepton has not: `tools/android_build.py` patches it to do without.
|
||||
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
|
||||
app cannot load: the APK carries a pass-through layer under its name
|
||||
(`port/android/xr_layer/fdm_injection_stub.c`).
|
||||
|
||||
## Stages
|
||||
|
||||
| # | 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/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 |
|
||||
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
|
||||
| 7 | Multiplayer, voice, cross-play with the Quest | |
|
||||
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
|
||||
|
||||
## Starting it on the Frame
|
||||
|
||||
A button in the Steam library ("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/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.
|
||||
|
||||
What the script does that Steam would for an Android title:
|
||||
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
|
||||
install Lepton links the app's data to it, a dangling link without it;
|
||||
and the shader path, which Lepton reads Steam's shader dump path against;
|
||||
- a container id of its own: with the shortcut's id, SteamVR's client in the
|
||||
container stalled in xrCreateInstance;
|
||||
- a few starts: a new APK's first start only installs it, and a start that
|
||||
ends within 30 s is tried again (up to three times), unless the game was
|
||||
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 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.
|
||||
|
||||
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
|
||||
it starts; appid 3151454348 for this name and path), its pictures in
|
||||
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
|
||||
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
|
||||
beside Lepton's `<frame-ip>:5555`.
|
||||
|
||||
## The game's data
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents` can be written, the launcher makes
|
||||
`/home/steamos/Documents/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/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
|
||||
app and the latter.)
|
||||
|
||||
## The controllers
|
||||
|
||||
SteamVR offers the Frame's controllers as themselves
|
||||
(`XR_VALVE_frame_controller_interaction`,
|
||||
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
|
||||
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
|
||||
and the left Touch controller's X and Y land on the D-pad. With them the
|
||||
game gets the Xbox's layout, as the right controller has it: A jump, B
|
||||
flashlight (held: switch grenades), X action and reload, Y switch weapon,
|
||||
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
|
||||
bumpers switch grenades (left) and the flashlight (right). The VR options'
|
||||
page of buttons shows this layout on the Frame.
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
|
||||
game's menus and HUD have none in their additive parts, so nothing showed.
|
||||
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
|
||||
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
|
||||
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
|
||||
- 2026-10-04: no online games found: the public MQTT brokers had banned the
|
||||
home connection for a while (too many connections from tests that day);
|
||||
broker.emqx.io came back within minutes. Lepton's network is the Frame's
|
||||
own (same IP), DNS works.
|
||||
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
|
||||
only that one (the game asks for the highest at or below
|
||||
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
|
||||
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
|
||||
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
|
||||
making up of the missed frames showed as a doubled picture when turning
|
||||
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
|
||||
0.85) for 90.
|
||||
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
|
||||
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
|
||||
frames). The time was not the drawing's but waits: the layer copies read
|
||||
the GL state back (glGet*) and released each image at once, and with
|
||||
Mesa's GL thread (zink) every read waits for the thread to run all that
|
||||
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
|
||||
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
|
||||
are now deferred (host_xr.c): no state read back (the game sets its own
|
||||
again), the images released together as the frame ends, and the eyes'
|
||||
work flushed to the GPU as soon as they are copied.
|
||||
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
|
||||
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
|
||||
and, with the compositor's counters, each copy's time.
|
||||
@@ -0,0 +1,600 @@
|
||||
# VR roadmap
|
||||
|
||||
What 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.
|
||||
|
||||
Several of the next steps follow
|
||||
[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) and the
|
||||
Steam Frame work it grew from,
|
||||
[astromaddie/HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) (both
|
||||
CC0): their ideas, rebuilt into this port's own structure, with the source
|
||||
named where code is taken over.
|
||||
|
||||
## Done (in `main`)
|
||||
|
||||
| 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/RingfallMjolnirVR` |
|
||||
| Rendering | The eyes drawn one by one (both at once, multiview, only with `debug.multiview`: its eyes did not always fuse, see the open points); 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; each weapon held so that its model's hand lands on the real one, as the free hand's (it learns where each weapon's animations hold the hand; the pistol's had been 15 cm before the real hand, the assault rifle's 8 cm aside), the scope's disc moved with it; `vr.weapon_hand_*` to adjust (2026-10-17) |
|
||||
| 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 |
|
||||
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
|
||||
| Scores | The menu button held shows the scores on the Quest (a press pauses, as it is let go); the Steam Frame's View button as before; the text chat's lines where the desktop has them (2026-10-18) |
|
||||
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
|
||||
| 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 32, 2026-10-20; HDR kept off in VR): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
|
||||
| Bots | Bots in multiplayer (`host_bots.c`; OpenCE's own, `bots.c`, are local split screen games' and off in VR): on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person, on the LAN too (2026-10-18); they climb ladders (Damnation's) and take paths as near their goal as a search reaches, as OpenCE's do: on Damnation 0 searches of 1747 failed, 1078 of 1605 before (2026-10-20). Later: OpenCE's bots' aiming (lead, bursts, taps for the pistol and sniper rifle, field of view), learnt obstacles and map fixes into ours |
|
||||
| Voice chat | Push to talk on any button (`vr.button_talk`, BUTTONS page; the free hand's grip by default, sending nothing while it reaches for the foregrip or the wheel), open mic, off (2026-10-13) |
|
||||
| Builds | The Android build's assembly conversion (`tools/android_asm_convert.py`) keeps string data as it is: strings and tables starting with "_" or holding \" lost a "_" (Opus's pulse cache among them: the Steam Frame's crashes in online games while talking), 2026-10-13 |
|
||||
| Languages | The European disc in Spanish and Italian (a maps folder for each, `maps_es` and `maps_it`, none called `maps`) imported on the Quest and the Steam Frame: the headset's language's, its language the game's and the menus' (`maps/language.txt`, unless `game.language` is set), 2026-10-17; not yet tried with that disc |
|
||||
| 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
|
||||
|
||||
| # | Step | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Steam Frame CPU time: Mesa's trace markers off (after astromaddie/HaloCE-VR), no `glGet` stalls in the visibility counters' copies | **done** (in `main` 2026-10-06): markers quieted, no problems; the frame's work about unchanged (11-12.5 ms of 13.9 in a bot game) |
|
||||
| 3a | Visible arms and hands: the arms by IK to the controllers, fingers by trigger and grip | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good; 0.02 ms a frame) |
|
||||
| 2 | Controls and comfort: left-handed mode, buttons remapped, flashlight by tapping the head, finer turning and vignette choices, graphics presets and refresh rate in the VR options | first part **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good): left-handed mode, graphics presets, refresh rate and resolution in the VR options. Second part **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 | **done** (in `main` 2026-10-10; Quest 3 good): `vr.body` "full" ("Full body" in the VR options; "arms" still the default), legs walking the way the player moves, feet on the ground. Also in: voice chat's push to talk on the free hand's grip. Steam Frame: its cost still to measure |
|
||||
| 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; moving by swinging the arms (in place of the stick, never faster than it: online too), and a sprint by swinging them harder (faster than the game allows: offline only) | 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). Arm swinging and its offline sprint still open |
|
||||
| 5 | Later: multiview again (both eyes in one pass, half the drawing's CPU cost): find why its eyes do not fuse in big online games on the Quest 3 (and now and then on the Steam Frame), then turn it back on | open: off by default since 2026-10-16 (`debug.multiview`); findings in the open points |
|
||||
| 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) |
|
||||
| R2 | Renamed **Mjolnir VR** ("Ringfall" is another game's name): app and Steam names, logo (`tools/mjolnir_art.py`), release assets (also under the old names for the updaters of builds 302 to 313), updaters, README; the repository renamed `Cesarus85/mjolnir-vr` (GitHub forwards the old links). Kept: the app id, the signing key's alias, the Frame's `~/Documents/RingfallMjolnirVR` and the names inside its bundle (installed games and their shortcuts) | in `feature/rename-mjolnir-vr` (2026-10-21) |
|
||||
|
||||
## 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` | `"auto"` (default, branch `fix/vr-frame-hitches`), `"low"`, `"medium"`, `"high"`, `"max"` |
|
||||
| `vr.foveation` | `"off"` (default), `"low"`, `"medium"` (the Quest only; branch `fix/vr-frame-hitches`) |
|
||||
| `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, melee and push to talk each go on one of eight
|
||||
buttons: A, B, X, Y, the move stick's click, the turning stick's, the
|
||||
weapon hand's grip, the free hand's grip. Moving an action to a button
|
||||
swaps it with the action there, so none is ever left without one.
|
||||
- The free hand's grip as a button (`free_grip_down`): pressed once held
|
||||
0.15 s at the foregrip's threshold, and not at all while it holds the
|
||||
weapon's foregrip or the Warthog's wheel (taken in that time, the press
|
||||
is theirs until the grip opens).
|
||||
- The triggers, the menu button, the Steam Frame's
|
||||
bumpers, D-pad and View button keep theirs; the menus keep the buttons
|
||||
as they are.
|
||||
- Applied after the left-handed mirroring, on the gamepad's sides: the
|
||||
same on the Touch and the Frame controllers, and a left-handed player's
|
||||
buttons are the mirror image of the right-handed ones (on the Frame,
|
||||
mirrored, the left D-pad's). The defaults are the old layout, the
|
||||
host's bindings and the ABI unchanged.
|
||||
- The melee's aim and jab follow its button; the grips on a Warthog's
|
||||
wheel press nothing, whatever the grip does.
|
||||
- The controller buttons page shows each action where its button is.
|
||||
|
||||
Comfort (COMFORT page): snap turns 10 to 45 degrees by 5, 60, 75, 90;
|
||||
smooth turning 30 to 150 degrees a second by 15, then to 300 by 30; the
|
||||
vehicle vignette Off, 15 to 100%; the on-foot vignette (off by default)
|
||||
while the stick turns smoothly, or also while the move stick moves, as
|
||||
far as the sticks are pushed, with its own strength. The old values are
|
||||
among the steps; defaults unchanged.
|
||||
|
||||
Transparent geometry: room for 2048 groups a window (the Xbox's 384,
|
||||
below).
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.button_jump` | `"a"` (default), `"b"`, `"x"`, `"y"`, `"move_stick"`, `"turn_stick"`, `"grip"`: as right-handed, mirrored with `vr.mirror_controls` |
|
||||
| `vr.button_light` | the flashlight, held switch grenades; `"b"` (default) |
|
||||
| `vr.button_action` | action, reload; `"x"` (default) |
|
||||
| `vr.button_weapon` | switch weapon; `"y"` (default) |
|
||||
| `vr.button_crouch` | `"move_stick"` (default) |
|
||||
| `vr.button_zoom` | `"turn_stick"` (default) |
|
||||
| `vr.button_melee` | `"grip"` (default) |
|
||||
| `vr.foot_vignette` | `"off"` (default), `"turning"` (smooth turns), `"moving"` (and moving) |
|
||||
| `vr.foot_vignette_strength` | 0.15 to 1, `0.7` (default) |
|
||||
| `debug.vr_options` | `false` (default); `true` shows the VR options off the headset (the emulator) |
|
||||
|
||||
Checked in the emulator (`debug.vr_options`): the four pages without
|
||||
overlaps, each row's values, swaps and reset, the controller buttons page
|
||||
right- and left-handed after swaps, the settings kept over a restart, a
|
||||
15-bot game as before.
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Defaults: every button as before (jump, light/held grenades, action,
|
||||
switch weapon, crouch, zoom, melee by grip and by swing), right- and
|
||||
left-handed, mirrored and not; the menus' A and B.
|
||||
2. BUTTONS: swap jump and melee (jump on the grip, melee on A): both work,
|
||||
the melee jabs and aims where the head looks; light on a stick click
|
||||
(tap: light, hold: grenades); the controller buttons page says so.
|
||||
At the Warthog's wheel with jump on the grip: no jump while holding
|
||||
the wheel. Reset: all back.
|
||||
3. The same left-handed with mirrored controls (on the Frame the left
|
||||
D-pad), and with `Mirrored controls: Off`.
|
||||
4. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
|
||||
100%; on-foot vignette "Smooth turns" (nothing with snap turns),
|
||||
"Moving, turning", strength 15% and 100%; no vignette in a vehicle
|
||||
from the on-foot one, none in the menus.
|
||||
5. A game of 30 and more players: effects, glass and water stay; the log
|
||||
(`adb logcat -s halo`) says `rasterizer: N transparent geometry groups
|
||||
in a window` once a window passes 384; frame time as before.
|
||||
|
||||
## Step 3a (done)
|
||||
|
||||
Tried on the Quest 3 and the Steam Frame, in `main`. The arm IK,
|
||||
the forearm's twist and the fingers' poses follow HaloCE-Quest-VR (CC0),
|
||||
in `port/linux/game/vr_arms.c`.
|
||||
|
||||
- The first-person pose (weapon, arms) is built once a frame instead of
|
||||
per eye and scope view (SteamVR: two to three times before).
|
||||
- `vr.log_performance` reports its cost (`VR: first person: ... ms a
|
||||
frame in ... builds (arms ... ms), ... kept`) and, once, how the hands'
|
||||
skin hangs from its bones (none from the root: no triangle filter
|
||||
needed).
|
||||
- Both arms bend from shoulders below the neck, square to the torso. The
|
||||
torso follows the head beyond 15 degrees and turns towards the hands;
|
||||
snap and smooth turns carry it at once.
|
||||
- The free left arm bends at the elbow to the controller (before: carried
|
||||
rigidly). The hand's roll is shared along forearm and upper arm.
|
||||
- The free hand's fingers follow the controller: index from the trigger,
|
||||
the others from the grip, the thumb from where it rests (touch on the
|
||||
Quest's, Index and Steam Frame controllers; half curled where unknown),
|
||||
closed round a held grenade. The weapon hand keeps its grip; its index
|
||||
finger lifts off the trigger when the controller says it is off.
|
||||
- host_xr: touch actions for thumb and index; `suggest()` takes 64
|
||||
bindings (was 32, silently cut) and retries a profile without the
|
||||
touch paths should the runtime refuse them.
|
||||
|
||||
Settings (`[vr]` in config.toml; the first two also in the VR options,
|
||||
Y on the main or pause menu):
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.body` | `"arms"` (default), `"hands"` (hands alone, the arms shrunk behind the wrists), `"off"` (the weapon alone) |
|
||||
| `vr.fingers` | `true` (default): fingers follow the controller; `false`: as the weapon's animation has them |
|
||||
| `vr.arm_ik` | `true` (default): the free arm bends at the elbow; `false`: rigid as before, should its skin tear |
|
||||
|
||||
After the first Quest tries (2026-10-06):
|
||||
- The shoulders 13 cm behind the eyes and 20 cm below the neck: the open
|
||||
end of the sleeve was in view. Looking hard down to the side one still
|
||||
sees into the arm (the model ends at the shoulder): left as it is.
|
||||
- The elbow bends in the arm's plane (`vr_align_elbow`): it had turned in
|
||||
with the elbow out to the side; the upper arm takes none of the hand's
|
||||
roll.
|
||||
- The free arm is solved first and its hand turned after (as
|
||||
HaloCE-Quest-VR does): turned first, its skin stretched like a rubber
|
||||
band.
|
||||
- The elbow follows the hand's roll (lifted out, the controller rolls in):
|
||||
before, it bent only towards a fixed direction.
|
||||
- Arm length, shoulder width, fingers, grenade grip, body modes, scope,
|
||||
vehicles: fine.
|
||||
|
||||
Not in it (3a.7): the glove fitted to each weapon's grip (gun anchor) and
|
||||
the left hand animated through reloads.
|
||||
|
||||
## Step 3b (done)
|
||||
|
||||
On `feature/vr-voice-and-body`, not yet tried in the headset. The neck
|
||||
pivot, the planted feet with their steps and the leg's clamped reach follow
|
||||
HaloCE-Quest-VR (CC0), its full body, rebuilt in
|
||||
`port/linux/game/vr_body.c` on this port's skeleton (`vr_arms.c`).
|
||||
|
||||
- `vr.body = "full"` (VR options, Body: "Full body") draws the player's own
|
||||
biped under the first-person arms and weapon: its torso, hips and legs, in
|
||||
the map's own model and colours. "arms" stays the default until the
|
||||
headsets have tried it.
|
||||
- Drawn among the world's objects, so it sorts against the world by depth;
|
||||
the first-person weapon and arms, drawn before the world into the
|
||||
stencil, stay over it, as before. In both eyes (multiview on the Quest,
|
||||
each eye on SteamVR), posed once a frame for every eye and the scope.
|
||||
- The torso: the torso's heading (the head's beyond 15 degrees, drawn
|
||||
towards the hands, the stick's turns at once), upright from the hips to a
|
||||
neck 14 cm behind and 20 cm below the eyes as the head turns about it
|
||||
(looking down, the eyes go forward and the chest stays behind them). The
|
||||
aiming's twist and bend of the third-person torso are taken out.
|
||||
- The hips: down with the physical crouch (and back a little, the torso
|
||||
leaning forward), a third of the way with a lean (the spine tilts the
|
||||
rest). The body's model has its neck higher than the eyes' pivot: it is
|
||||
moved down to it, so the shoulders stay below the eyes, crouched by the
|
||||
button too.
|
||||
- The legs: the game's own animation (walking, running, jumping, crouched
|
||||
by the button). Moving, the hips and legs stay as the game faces them, so
|
||||
its own choice of animation (forward, back, aside, as for any player,
|
||||
by the way it moves against the way it faces) runs along the way the
|
||||
player moves, and the spine turns the torso; standing, the hips turn to
|
||||
the torso's heading (eased, 0.12 s). Walking off, the feet ease from
|
||||
where they stood into the walk (0.2 s); stopping, they stay where the
|
||||
walk left them and step into place. Standing still, the feet stay
|
||||
planted and step, one at a time (0.22 s, 6 cm lifted), when the body has
|
||||
turned 35 degrees from them or their place moved 18 cm; the knees bend by
|
||||
IK to them as the hips go down. Moved 80 cm at once (a teleporter), they
|
||||
are put down there.
|
||||
- The ground: put down, each foot rests on the ground under it (a slope, a
|
||||
stair), its height above the ground as the animation has it, tilted with
|
||||
the slope (no steeper than 45 degrees), up to 45 cm from the ground under
|
||||
the body; a foot's ground below it lowers the hips as far. Rays straight
|
||||
down, read only, cast only as the feet are put down (three: under the
|
||||
body and each foot) and as one steps (one), none while walking, where
|
||||
the animation's feet are kept; eased (0.08 s) in and out. Nothing found
|
||||
(a ledge): level.
|
||||
- Hidden: the head and neck (shrunk onto the neck's root: no head or
|
||||
collar seen from inside), the third-person arms (shrunk onto their
|
||||
shoulders), the third-person weapon and all the biped carries.
|
||||
- The first-person arms hang from the body's shoulders: beside its neck,
|
||||
as wide as its shoulder joints within 14 to 24 cm (the aiming's shrug
|
||||
left out). Left-handed, the shoulders go into the mirror with the arms
|
||||
(the room's left one is the model's right): the body itself is the
|
||||
room's, not mirrored.
|
||||
- Seats: a passenger in an open vehicle (the Warthog's back) sits as the
|
||||
seat's animation has it, the head and arms shrunk alike; at a wheel or a
|
||||
gun the body is drawn whole as before (its hands on them, no
|
||||
first-person weapon); in a closed seat (Ghost, Banshee, tank) none.
|
||||
- Not drawn when dead, in cutscenes, in third person, in a mirror's view;
|
||||
camouflage as the game has it.
|
||||
- Only drawn, on this machine: the game's own pose, hit boxes, markers and
|
||||
network messages are unchanged.
|
||||
- Cost: `vr.log_performance` adds `body ... ms a frame in ... poses` to the
|
||||
`first person:` line. In the emulator 0.006 to 0.025 ms a pose standing
|
||||
(as busy as the emulator was), up to 0.1 ms averaged while turning with
|
||||
the feet's rays (at most a few a second); the drawing is one more biped
|
||||
model (as one more player close by).
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.body` | `"full"` (the whole body), `"arms"` (default), `"hands"`, `"off"` |
|
||||
| `debug.vr_body` | `"off"` (default); off the headset (the emulator) the body posed from the flat camera: `"eye"`, `"down"` (looking down 60 degrees, 90 degrees wide), `"front"`, `"side"`, `"back"` (the camera 2.6 m away); after a colon the head's motion: `"cycle"` (turning, crouching, leaning, 5 s each), `"still"`, `"turn"`, `"crouch"`, `"lean"`, `"raw"` (the game's pose, only shrunk). Logs its pose once a second |
|
||||
|
||||
Checked in the emulator (`debug.vr_body`): the cyborg's 19 nodes (bip01
|
||||
pelvis, spine, spine1, neck, head, clavicles, arms, thighs, calves, feet);
|
||||
the body standing, crouched physically and by the button, leaning,
|
||||
turning with the feet stepping; looking down, the shoulders below the eyes
|
||||
and no head or collar; two-minute bot games on Blood Gulch with it on and
|
||||
off, deaths and respawns, nothing in the log. Then the legs by the way of
|
||||
moving (strafing, backwards, diagonally with the touch stick: the game's
|
||||
sideways and backwards walk) and the feet on the ground: on the ramp of a
|
||||
Blood Gulch base, the upper foot 15 cm up, the lower 10 cm down, both on
|
||||
it; stairs not reached in the emulator (Prisoner's spawn was level). Three
|
||||
rays when the feet are put down, one a step, none walking. Screenshots in
|
||||
`build/body-shots/` (local).
|
||||
|
||||
Push to talk is built on the same branch: voice chat's talking on the free
|
||||
hand's grip, while it holds neither the weapon's foregrip nor the Warthog's
|
||||
wheel (`audio.voice_chat`, VR options PLAY page "Voice chat": push to talk
|
||||
(grip), open mic, off).
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame), with `vr.body = "full"`
|
||||
(VR options, Body: Full body) and `vr.log_performance = true`:
|
||||
1. Standing, looking down: torso, hips, legs and feet, the shoulders below
|
||||
the eyes, no head, collar or helmet in view, the chest not in the way;
|
||||
the arms from the shoulders, lined up with the body, the weapon as
|
||||
before.
|
||||
2. Turning the head alone: the body stays within about 15 degrees, then
|
||||
follows; turning further, the feet step after it, one at a time, and
|
||||
settle. Snap and smooth turns: the body turns, the feet step.
|
||||
3. Physical crouch: the knees bend, the feet stay; kneeling deep, nothing
|
||||
of the body in the eyes. Leaning forward and aside: the torso tilts, the
|
||||
feet stay. Crouch button: the game's crouch, shoulders below the eyes.
|
||||
4. Walking, running, jumping by the stick: the legs walk with the game's
|
||||
animation, no sliding when starting or stopping. Strafing left and
|
||||
right, walking backwards and diagonally, also with the head turned away
|
||||
from the way of walking: the legs step the way the body moves (sideways,
|
||||
backwards), the torso facing as the head and hands have it; stopping,
|
||||
the hips turn back under the torso and the feet step into place.
|
||||
4a. Ground: standing still on a slope (Blood Gulch's hills and the bases'
|
||||
ramps), across it and along it: both feet on the ground, tilted with
|
||||
it, the lower knee straighter, the hips down with the lower foot; on
|
||||
stairs (Prisoner, Chill Out, Damnation), one foot a step higher than
|
||||
the other, turning in place there: each step lands on its stair. At a
|
||||
ledge, one foot over it: level, nothing odd.
|
||||
5. Left-handed (`vr.handedness = left`, mirrored or not): the arms from
|
||||
the body's shoulders, the weapon in the left hand, the body as
|
||||
right-handed.
|
||||
6. Vehicles: Warthog driver and gunner as before (body with its hands on
|
||||
the wheel or gun); Warthog passenger: seated legs and torso, the
|
||||
first-person arms from its shoulders; Ghost, Banshee, Scorpion: no body.
|
||||
7. Dying and respawning, a teleporter, a cutscene (campaign), the menus
|
||||
and pause menu, the scope, camouflage, a game with 15 bots: nothing
|
||||
broken, no body where it should not be.
|
||||
8. Body "Arms and hands", "Hands only", "Weapon only": as before.
|
||||
9. Performance: `first person: ... body ... ms` in `adb logcat -s halo`;
|
||||
the Quest holds 90 Hz in a bot game; the Steam Frame's working time
|
||||
against "arms" (about 12 ms of 13.9 before).
|
||||
10. Push to talk (Voice chat: push to talk), in a network game with
|
||||
another player: holding the free grip away from the weapon talks;
|
||||
nothing while the free hand holds the foregrip or the Warthog's wheel;
|
||||
"Open mic" talks without the grip; "Off" sends nothing (others still
|
||||
heard).
|
||||
|
||||
## Frame hitches and GPU time (built: branch `fix/vr-frame-hitches`)
|
||||
|
||||
Measured on the Quest 3 (2026-10-12; 90 Hz, multiview, graphics "high",
|
||||
full body, a 15-bot game on Blood Gulch): the game's CPU work 5-6 ms a
|
||||
frame; the app's GPU time 5.5-6.4 ms (`xr: metrics`), the same with
|
||||
foveation off, low, medium and high: **the GPU is not the bottleneck**.
|
||||
The doubled pictures come from single long frames: shader programs compiled
|
||||
in a frame (29 new ones in a 794 ms frame 22 s into a match, single ones
|
||||
18-64 ms), draws of 254 ms (the first frame in a map) and 74 ms with
|
||||
nothing compiled (most likely the driver making a program's variants at
|
||||
its first draw), and the menus' PNG pictures (7 in 108 ms).
|
||||
|
||||
What the branch has:
|
||||
|
||||
| Part | What |
|
||||
| --- | --- |
|
||||
| Long frames | `VR: long frame N ms: input, ticks, camera, draw, present; shaders compiled or linked, programs from the cache; textures uploaded, menu pictures, render targets; draw calls N ms, the slowest N ms (vertex program, program key); N draws left out waiting for their programs`. The 5 s report counts the draws left out |
|
||||
| GPU time | XR_META_performance_metrics on with `vr.log_performance`: `xr: metrics (mean/most) app/gpu_frametime=... compositor/dropped_frame_count=...` |
|
||||
| Foveation | `vr.foveation` "off" (default) / "low" / "medium" on the MORE page (GL_QCOM_texture_foveated, the eyes only, lens centres); "high" taken out (it drew a grey rectangle below the player). Kept for later, no gain measured |
|
||||
| Shader cache | `display.shader_cache`: programs kept in `shader_cache/` (binary, driver, sources); each map's list (`maps/<map>.txt`) and every map's (`all.txt`) |
|
||||
| Worker | `display.shader_worker` (default true): programs made on a thread with a GL context sharing the game's (`gl worker: running`), from the cache's binary or compiled; a draw whose program is not made yet is left out for those frames, the frame never waits. As a map loads, its programs and then every map's are asked for |
|
||||
| Warm-up | `debug.shader_warm_up`: "worker" (default: each program drawn with once on the worker into a small target of the eyes' kinds), "load" (the map's programs drawn with on the game's thread as it loads), "off" |
|
||||
| Menus | The PC menus' pictures decoded on a thread of their own, drawn transparent for the frames that takes |
|
||||
|
||||
Second Quest test (latest build): the worker works, no long frame after the
|
||||
map's first frames (245 ms and 72 ms there, each one draw of 67-75 ms with
|
||||
vertex program 4). But doubled pictures in spells of seconds with the GPU
|
||||
at 9-13 ms a frame (GPU% 0.91-0.94, 4-37 stale frames a second), 5-7 ms in
|
||||
between: "high" is too much for 90 Hz in heavy scenes, "medium" holds.
|
||||
Hence:
|
||||
|
||||
| Part | What |
|
||||
| --- | --- |
|
||||
| Graphics "auto" | `vr.graphics = "auto"`, the new default (Graphics row: Auto): "high" while the GPU keeps up. Past 4/5 of the frame (8.9 ms at 90 Hz) for a quarter second it leaves out the next steps (shield glow, shadows, reflections, specular light, lens flares, bump maps, the rest of "low"), as many as their learnt costs say; brings one back once GPU time + its cost stays under 7/10 of the frame for 3 s (longer if it had to go again within 10 s). Logs `VR graphics auto: shadows left out (step 2): the GPU 10.2 ms a frame of 11.1`, `... saved 1.9 ms`, `... back`. On the Steam Frame (no GPU time from SteamVR) "auto" is "high" |
|
||||
| Measuring | `debug.vr_graphics_measure = true`: each effect left out in turn (1.5 s on, 1.5 s off), `VR graphics measure: shadows 2.10 ms (...)` and means each round |
|
||||
|
||||
What the effects cost, from the code (to be measured with the option above):
|
||||
object shadows are drawn once a frame with multiview (the scene is drawn
|
||||
once), twice one eye at a time; each shadow-casting object (every bot, its
|
||||
weapon, vehicles) renders its map, blurs it and projects it onto the level
|
||||
beneath: a pass of fill on both eyes per object. Reflections and specular
|
||||
light are passes over the level's geometry. The shield's glow
|
||||
(`display.shield_glow`, off by default) is a dynamic light per flaring unit
|
||||
(at most 8), each lighting the level's surfaces in its reach once more, not
|
||||
a pass per object; upstream's shield colour on the arms only changes a
|
||||
colour. The whole body adds the player's own model and its shadow.
|
||||
|
||||
Not done, and why:
|
||||
- Drawing the eyes straight into the swapchain: moot now (GPU not the
|
||||
bottleneck); it needs GL_EXT_sRGB_write_control, the vignette as a pass
|
||||
of its own and array swapchains acquired mid-frame.
|
||||
- Decoding all menu pictures ahead: 165 pictures, about 350 MB decoded with
|
||||
their mipmaps.
|
||||
- Later: XR_KHR_visibility_mask.
|
||||
|
||||
### Headset test, graphics "auto" (Quest 3)
|
||||
|
||||
1. VR options, MORE: Graphics **Auto** (a `config.toml` written before
|
||||
still says "high"), Foveation Off; `vr.log_performance = true`.
|
||||
2. Log at start: `xr: performance metrics on (... the app's GPU time read)`,
|
||||
`VR: graphics auto`.
|
||||
3. 15-bot match on Blood Gulch, a few minutes, also into the busy middle:
|
||||
- `VR graphics auto: ... left out (step N): the GPU X ms` within a
|
||||
quarter to half a second of a heavy spell, `... saved Y ms`, and
|
||||
`... back` a few seconds after it.
|
||||
- VrApi: `Stale=` 0 nearly every second (at most a few in the second a
|
||||
spell begins), no `ASW=1` spells; App GPU under about 9 ms.
|
||||
- Look for effects that pop: shadows going and coming back. If they
|
||||
change more than every ten seconds or so, note the log lines.
|
||||
4. Costs: a second match with `debug.vr_graphics_measure = true` (Graphics
|
||||
setting ignored meanwhile; stand somewhere busy, a few minutes):
|
||||
`VR graphics measure: means (ms of GPU a frame): shadows ...;
|
||||
reflections ...`. The steps' order should follow the largest costs.
|
||||
5. The first frames of a map (245/72 ms, vertex program 4): try
|
||||
`debug.shader_warm_up = "load"` once and compare those lines.
|
||||
|
||||
### Headset test, shader worker (Quest 3)
|
||||
|
||||
Before: `adb logcat -G 16M` (again after any reboot); in `config.toml`
|
||||
`vr.log_performance = true`, 90 Hz, graphics "high", multiview, full body;
|
||||
Foveation Off (VR options, MORE: the config may still say "medium"). After
|
||||
each test: `adb logcat -d -s halo VrApi > test.txt`. The shader cache from
|
||||
the earlier tests stays.
|
||||
|
||||
1. Start: `gl worker: running (ES 3, surfaceless, programs warmed up)` and
|
||||
`shader cache: ... N programs kept (... N listed) ...; programs made on
|
||||
the worker, warmed up there`. As the menus load: `shader cache:
|
||||
levels_ui_ui: N programs listed; N asked of the worker ...; N of other
|
||||
maps to ask for`.
|
||||
2. Bot match on Blood Gulch: as it loads, `... bloodgulch: N listed; ...
|
||||
N there already` (made while in the menus). In play no `VR: long frame`
|
||||
with shaders compiled; at most a few `draws left out` (objects appearing
|
||||
a frame or two late), and VrApi `Stale=0` most seconds, no `ASW=1`.
|
||||
Any long frame left: its `draw calls ... the slowest N ms (vertex program
|
||||
N, program K)` says whether a draw (a driver variant) is the cause.
|
||||
3. A map not played yet (Damnation): as it loads, `N programs listed` is 0
|
||||
but `N there already` should not matter: its programs mostly come from
|
||||
every map's list (made in the menus). New ones are compiled on the worker
|
||||
(`draws left out` for a moment, no long frame).
|
||||
4. If draws of 70-250 ms remain with nothing compiled: `debug.shader_warm_up
|
||||
= "load"` (the log: `shader cache: N programs drawn with once as the map
|
||||
loaded, in N ms`), then compare; and `"off"` for the baseline.
|
||||
5. Menus: open new screens; pictures may appear a frame late, no long
|
||||
frames with `menu pictures`.
|
||||
6. If anything breaks (missing objects for good, a crash):
|
||||
`display.shader_worker = false` brings the game's-thread compiling back.
|
||||
|
||||
Steam Frame: the worker also runs there if its EGL shares contexts (the log
|
||||
says `gl worker: running` or why not); no foveation. A short bot game to
|
||||
see that nothing is missing or slower.
|
||||
|
||||
## Open points outside the order
|
||||
|
||||
- Multiview's doubled pictures (2026-10-16): on the developer's Quest 3,
|
||||
doubled and shifted pictures from the start of big online games (26-35
|
||||
players) as the head turned, nauseating; none in a local game with 48
|
||||
bots; none online either with the eyes drawn one by one. Not the frame
|
||||
rate: one by one, online at 72 Hz, the compositor repeated as many
|
||||
frames (about 4 a second) and nothing was seen. Not the facing jumps
|
||||
(respawns only) nor the camera (it moves every frame). Before: the
|
||||
Steam Frame's (2026-10-05), each eye alone sharp. So the eyes are drawn
|
||||
one by one by default now (`debug.multiview` for both at once). To do:
|
||||
find what the shared pass draws wrongly online, e.g. by saving both
|
||||
eyes' pictures in a big online game and comparing what is doubled
|
||||
(sprites and effects projected by the game itself, `mv_depth`; programs
|
||||
made on the shader worker; the sky rows), then turn it back on: it
|
||||
halves the drawing's CPU cost.
|
||||
- Big online games (2026-10-16): doubled pictures on a 32-player server,
|
||||
none in a local game with 48 bots. Not the GPU (60–80 %): the ticks cost
|
||||
3.3–4.6 ms as a client there (1.3–1.9 ms with bots or 16 players), so
|
||||
47–70 of 150 frames with a tick missed 90 Hz's 11.1 ms (the doubling
|
||||
itself was multiview's, above). In `main` 2026-10-16 (Quest 3 good):
|
||||
`vr.refresh_rate_auto`
|
||||
(VR options MORE, "Lower it when frames are late", on by default) steps
|
||||
the rate down a step (90, 80, 72 Hz) while more than one frame in 32
|
||||
comes late, and back up once they would keep up; "VR: refresh rate" in
|
||||
the log. First try: 90 to 80 Hz a few seconds into a 30-player game, no
|
||||
doubled picture seen; 2-5 stale frames a second left at 80, hence one
|
||||
in 32 (was 16). Later:
|
||||
why a client's ticks cost that much per remote player.
|
||||
|
||||
- Doubled pictures when turning on the developer's Quest 3 (2026-10-12):
|
||||
with every build tried, back to 2026-10-06's, at 90 and at 72 Hz, even
|
||||
in seconds the compositor reported no stale frame. That Quest runs
|
||||
Horizon OS from the Public Test Channel (PTC; its build 2026-10-08),
|
||||
the likeliest cause. Released with `vr.graphics` "medium" as the
|
||||
default (smooth there). To do: try this and later builds with "high" on
|
||||
a Quest 3 outside PTC (the developer's wife's), and set the default
|
||||
back to "high" or "auto" if it holds.
|
||||
Resolved 2026-10-16: multiview (above), not the Horizon OS PTC.
|
||||
|
||||
- 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 overflowed in
|
||||
games of 30 and more players (the last submitted, glass, water,
|
||||
particles, contrails, vanished). Raised to 2048 (320 KB), in `main`;
|
||||
still to check in a game of 30 and more players; upstream raised them
|
||||
to 2048 too (2026-10-12).
|
||||
|
||||
## Headset tests (2026-10-06, all three in `main`)
|
||||
|
||||
Quest 3, 2026-10-06: menus, laser, VR options, head turning, arms and
|
||||
hands, fingers, body modes, scope, vehicles, a bot game (bots spread and
|
||||
leave their bases) and a large online game: all good.
|
||||
Steam Frame, 2026-10-06: buttons, picture (no doubled image), arms, hands,
|
||||
fingers, wheel: all good; SteamVR reprojected 18% of the frames, unseen.
|
||||
What follows is left for the two headsets together.
|
||||
|
||||
Upstream merge (`merge/upstream-2026-10-06`):
|
||||
- Menus, a bot game, head turning (no doubled image), co-op briefly.
|
||||
- A password game hosted on the Frame, joined from the Quest.
|
||||
- A bot making room for a person joining a full lobby.
|
||||
|
||||
Performance (`frame/performance-2`, and the merge's upstream speed-ups),
|
||||
with `vr.log_performance = true`:
|
||||
- Frame: the log says `trace markers: N descriptors to /dev/null`; frame
|
||||
time ("working") against before (about 12.5 ms in a bot game on Blood
|
||||
Gulch); SteamVR's reprojected share (vrcompositor.txt) against before
|
||||
(2 to 17 %); no doubled image.
|
||||
- Lens flares still appear and hide as before (their visibility counters'
|
||||
copies changed).
|
||||
- The pose cache (3a.1): `first person: ... builds` about one a frame;
|
||||
scope, crosshair, laser and the weapon in the hand as before.
|
||||
- Quest: 90 Hz held as before in a bot game and in a large online game;
|
||||
nothing slower than the last build.
|
||||
|
||||
Arms and hands (3a): on the Steam Frame, as on the Quest (fingers on its
|
||||
controllers, every button still working).
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,3 +98,11 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// the Khronos OpenXR loader (VR, port/android/host/host_xr.c): its
|
||||
// library, and the manifest entries through which it finds the
|
||||
// headset's runtime. The version matches OPENXR_VERSION in
|
||||
// tools/android_build.py, whose headers libmain.so is built against.
|
||||
implementation "org.khronos.openxr:openxr_loader_for_android:1.1.63"
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
<!-- VR (port/android/README.md, "VR"): on a headset the game runs as an
|
||||
immersive OpenXR app; elsewhere as before. Not required, so the one
|
||||
APK still installs on phones. -->
|
||||
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="false" />
|
||||
|
||||
<!-- controller rumble -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
@@ -68,26 +72,39 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- in a process of its own, with a fresh Java heap (HaloApplication) -->
|
||||
<!-- The VR categories make headsets start this activity immersive
|
||||
(without them Horizon OS shows it as a flat panel, and the OpenXR
|
||||
session never gets focus); phones ignore them. The launcher,
|
||||
a flat panel on a headset, starts it once the game data is
|
||||
there. (port: upstream runs it in a process of its own,
|
||||
":game", for handhelds whose Java heap covers the Xbox window;
|
||||
not here: on the Steam Frame Lepton waits for the app's process
|
||||
to end, and the headsets have no such overlap) -->
|
||||
<activity
|
||||
android:name=".HaloActivity"
|
||||
android:process=":game"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:screenOrientation="landscape"
|
||||
android:resizeableActivity="false"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.category"
|
||||
android:value="game" />
|
||||
</activity>
|
||||
|
||||
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|questpro|quest3|quest3s" />
|
||||
|
||||
<!-- 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,6 +35,7 @@ import java.nio.channels.FileChannel;
|
||||
*/
|
||||
public class LauncherActivity extends Activity {
|
||||
private static final int PICK_IMAGE = 1;
|
||||
static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
|
||||
|
||||
private File dataRoot;
|
||||
private TextView status;
|
||||
@@ -46,6 +47,17 @@ public class LauncherActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
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, 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)
|
||||
@@ -111,8 +123,10 @@ public class LauncherActivity extends Activity {
|
||||
partial.delete();
|
||||
}
|
||||
|
||||
/** The game data, or the marker file of the VR self-test (host_xr.c). */
|
||||
private boolean haveData() {
|
||||
return dataRoot != null && new File(dataRoot, "maps/ui.map").isFile();
|
||||
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
|
||||
new File(dataRoot, "xr_test").isFile());
|
||||
}
|
||||
|
||||
private void startGame() {
|
||||
@@ -133,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("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);
|
||||
|
||||
@@ -36,7 +36,8 @@ import java.util.zip.ZipInputStream;
|
||||
* the latest release when it starts, on a thread of its own, and if it is
|
||||
* newer asks the player whether to update:
|
||||
*
|
||||
* - Yes: the release's app (halo-android-release.zip or -debug.zip) is
|
||||
* - Yes: the release's app (mjolnir-vr-android-release.zip or
|
||||
* -debug.zip) is
|
||||
* downloaded and handed to Android's package installer, which replaces the
|
||||
* game (closing it) and offers to open the new version.
|
||||
* - No: nothing, until the next start.
|
||||
@@ -45,9 +46,15 @@ import java.util.zip.ZipInputStream;
|
||||
*
|
||||
* Every build of main is signed with the same key (the workflow's), which an
|
||||
* app must keep for Android to install a new version over it.
|
||||
*
|
||||
* Not on the Steam Frame: Lepton installs the APK in the game's folder there
|
||||
* at every start, so its launcher updates that (steam_frame_launch.sh).
|
||||
*/
|
||||
final class Updater {
|
||||
private static final String REPOSITORY = "OpenCommunityEdition/OpenCE";
|
||||
/* 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 APP_NAME = "Mjolnir VR";
|
||||
private static final String REPOSITORY = "Cesarus85/mjolnir-vr";
|
||||
private static final String USER_AGENT = "halo-ce-universal-updater";
|
||||
private static final int TIMEOUT_MILLISECONDS = 20000;
|
||||
/** the most a download (a release's zip, about 35 MB) or the app in it may be */
|
||||
@@ -60,7 +67,8 @@ final class Updater {
|
||||
static void start(Activity activity) {
|
||||
File config = configFile(activity);
|
||||
|
||||
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config))
|
||||
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config) ||
|
||||
new File(LauncherActivity.SHARED_DATA_ROOT).isDirectory())
|
||||
return;
|
||||
new Thread(() -> {
|
||||
int latest = latestRelease();
|
||||
@@ -202,8 +210,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 +223,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 +240,7 @@ final class Updater {
|
||||
/* ---------- updating */
|
||||
|
||||
private static void update(Activity activity, int latest) {
|
||||
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
String asset = "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 +255,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 +283,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();
|
||||
|
||||
@@ -153,26 +153,32 @@ final class XisoExtractor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* a multi-language European disc has no "maps" but a "maps_xx" per
|
||||
language: the one for the device's language, else English, else the
|
||||
first (as port/linux/src/xiso.c) */
|
||||
// (Mjolnir VR: a European disc in two languages, Spanish and Italian,
|
||||
// has no maps folder but one for each, maps_es and maps_it: the
|
||||
// device's language's is taken, else English's, else the first, and
|
||||
// its language written to maps/language.txt, which the game asks the
|
||||
// Xbox for while game.language is unset: port/linux/src/xbox_xapi.c)
|
||||
String language = null;
|
||||
if (maps == null) {
|
||||
String wanted = "maps_" + Locale.getDefault().getLanguage();
|
||||
String wanted = Locale.getDefault().getLanguage();
|
||||
Entry english = null, first = null;
|
||||
for (Entry entry : directories) {
|
||||
if (entry.name.length() != 7 || !entry.name.regionMatches(true, 0, "maps_", 0, 5))
|
||||
|
||||
for (Entry entry : walkTable(root, true)) {
|
||||
String code = mapsLanguage(entry.name);
|
||||
|
||||
if (code == null)
|
||||
continue;
|
||||
if (entry.name.equalsIgnoreCase(wanted)) {
|
||||
if (code.equals(wanted) && maps == null)
|
||||
maps = entry;
|
||||
break;
|
||||
}
|
||||
if (english == null && entry.name.equalsIgnoreCase("maps_en"))
|
||||
if (code.equals("en") && english == null)
|
||||
english = entry;
|
||||
if (first == null)
|
||||
first = entry;
|
||||
}
|
||||
if (maps == null)
|
||||
maps = english != null ? english : first;
|
||||
if (maps != null)
|
||||
language = mapsLanguage(maps.name);
|
||||
}
|
||||
if (maps == null)
|
||||
throw new ExtractException("The disc image has no maps folder: it is not a Halo disc.");
|
||||
@@ -223,6 +229,16 @@ final class XisoExtractor {
|
||||
File mapsFolder = new File(destination, "maps");
|
||||
if (!mapsFolder.isDirectory() && !mapsFolder.mkdirs() && !mapsFolder.isDirectory())
|
||||
throw new ExtractException("Could not create " + mapsFolder.getPath() + ".");
|
||||
// (the language of a language's maps folder: before ui.map is there)
|
||||
File languageFile = new File(mapsFolder, "language.txt");
|
||||
languageFile.delete();
|
||||
if (language != null) {
|
||||
try (FileOutputStream out = new FileOutputStream(languageFile)) {
|
||||
out.write((language + "\n").getBytes(StandardCharsets.US_ASCII));
|
||||
} catch (IOException e) {
|
||||
throw new ExtractException("Could not write " + languageFile.getPath() + " (is the storage full?).");
|
||||
}
|
||||
}
|
||||
List<Entry> order = new ArrayList<>(files);
|
||||
order.remove(userInterface);
|
||||
order.add(userInterface);
|
||||
@@ -235,6 +251,14 @@ final class XisoExtractor {
|
||||
partial.delete();
|
||||
}
|
||||
|
||||
/** a language's maps folder's language ("maps_it": "it"), else null */
|
||||
static String mapsLanguage(String name) {
|
||||
if (name.length() != 7 || !name.regionMatches(true, 0, "maps_", 0, 5))
|
||||
return null;
|
||||
String code = name.substring(5).toLowerCase(Locale.ROOT);
|
||||
return Character.isLetter(code.charAt(0)) && Character.isLetter(code.charAt(1)) ? code : null;
|
||||
}
|
||||
|
||||
/** No path, no . or .., and only printable ASCII that every file system takes as a plain name. */
|
||||
static boolean isPlainName(String name) {
|
||||
if (name == null || name.isEmpty() || name.equals(".") || name.equals(".."))
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- the launcher icon's background (tools/app_icon.py) -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient android:angle="270" android:startColor="#0A2B54" android:endColor="#030610" />
|
||||
</shape>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- the launcher icon (tools/app_icon.py, from port/assets/icon) -->
|
||||
<!-- the launcher icon (tools/android_icon.py, from port/android/art/android-icon.png) -->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- the launcher icon's background (tools/android_icon.py) -->
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1E538A</color>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Halo</string>
|
||||
<string name="app_name">Mjolnir VR</string>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -108,10 +108,70 @@ the GPU's writes to it */
|
||||
void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data);
|
||||
/* unsynchronized write into the buffer bound to target */
|
||||
void host_gl_buffer_write(unsigned int target, unsigned int offset, unsigned int size, const void *data);
|
||||
/* persistent, coherent storage for the buffer bound to target (0: the driver
|
||||
cannot); writes into such a buffer (0: it has none) */
|
||||
int host_gl_buffer_storage(unsigned int target, unsigned int size);
|
||||
int host_gl_persistent_write(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
|
||||
/* fences the GPU work queued so far as that of ring slot `slot`; waits for
|
||||
the GPU to finish the work last fenced for a slot */
|
||||
void host_gl_fence_frame(unsigned int slot);
|
||||
void host_gl_wait_frame(unsigned int slot);
|
||||
/* copies the visibility counters into snapshot `slot` at the end of a frame;
|
||||
reads a snapshot once its frame is finished */
|
||||
void host_gl_counters_snapshot(unsigned int buffer, unsigned int size, unsigned int slot);
|
||||
int host_gl_counters_read(unsigned int slot, void *out, unsigned int size);
|
||||
/* the shader programs' worker (port/android/host/host_gl_worker.c), for
|
||||
port/linux/src/xgpu_program_cache.c: started on the game's thread with its
|
||||
context current (1: running); a program asked for (its binary tried first,
|
||||
else its sources compiled; urgent: a draw waits for it), a job's number or
|
||||
0; hurried along; polled (1 done: *program, its kept binary's size, the
|
||||
time it took; 0 not yet; -1 failed); its kept binary copied out; the job
|
||||
forgotten; how many jobs are not done; and a warm-up draw with a program on
|
||||
the calling thread's context */
|
||||
int host_gl_worker_start(int warm_up);
|
||||
unsigned int host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
|
||||
unsigned int binary_format, const void *binary, unsigned int binary_size, int keep_binary);
|
||||
void host_gl_program_hurry(unsigned int id);
|
||||
int host_gl_program_poll(unsigned int id, unsigned int *program, unsigned int *binary_size, unsigned int *microseconds);
|
||||
unsigned int host_gl_program_binary(unsigned int id, void *buffer, unsigned int size, unsigned int *format);
|
||||
void host_gl_program_release(unsigned int id);
|
||||
unsigned int host_gl_worker_waiting(void);
|
||||
void host_gl_program_warm_up(unsigned int program, int multiview);
|
||||
|
||||
/* ---------- OpenXR (port/android/host/host_xr.c; the structures are in
|
||||
halo_xr_abi.h) */
|
||||
struct halo_xr_info;
|
||||
struct halo_xr_frame;
|
||||
struct halo_xr_submit;
|
||||
/* creates the session on the current GL context (the host found the
|
||||
headset before the game started, and set HALO_VR); 0 on failure */
|
||||
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
|
||||
/* waits for the next frame and reports the predicted poses */
|
||||
int host_xr_begin_frame(struct halo_xr_frame *frame);
|
||||
/* copies a render target texture (0: clears) into a layer's image: 0 not
|
||||
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
|
||||
state left (the deferred copies, host_xr.c), which the game sets again */
|
||||
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
|
||||
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
|
||||
top left */
|
||||
int host_xr_copy_layer_region(int layer, unsigned int texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom);
|
||||
/* submits the frame begun by host_xr_begin_frame */
|
||||
int host_xr_end_frame(const struct halo_xr_submit *submit);
|
||||
void host_xr_haptic(int hand, float amplitude, float seconds);
|
||||
/* copies a layer of a texture array (the eyes drawn at once, multiview)
|
||||
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);
|
||||
/* the app's GPU time of a recent frame (ms; 0: the runtime does not tell
|
||||
it), for vr.graphics "auto" */
|
||||
float host_xr_gpu_time(void);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
|
||||
@@ -106,5 +106,26 @@ void *host_resolve_import(const char *name);
|
||||
/* ---------- SDL / GL (host_sdl.c, host_gl.c) */
|
||||
|
||||
void *host_gl_resolve(const char *name);
|
||||
int host_gl_quiet_trace_markers(void);
|
||||
/* the shader programs' worker (host_gl_worker.c; the guest's imports) */
|
||||
int host_gl_worker_start(int warm_up);
|
||||
uint32_t host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
|
||||
uint32_t binary_format, const void *binary, uint32_t binary_size, int keep_binary);
|
||||
void host_gl_program_hurry(uint32_t id);
|
||||
int host_gl_program_poll(uint32_t id, uint32_t *program, uint32_t *binary_size, uint32_t *microseconds);
|
||||
uint32_t host_gl_program_binary(uint32_t id, void *buffer, uint32_t size, uint32_t *format);
|
||||
void host_gl_program_release(uint32_t id);
|
||||
uint32_t host_gl_worker_waiting(void);
|
||||
void host_gl_program_warm_up(uint32_t program, int multiview);
|
||||
|
||||
/* ---------- VR (host_xr.c) */
|
||||
|
||||
/* whether there is an OpenXR headset, and the size of its eye images;
|
||||
before the game starts */
|
||||
int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height);
|
||||
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom);
|
||||
/* the VR self-test, instead of the game; does not return */
|
||||
void host_xr_test(void) __attribute__((noreturn));
|
||||
|
||||
#endif
|
||||
@@ -10,8 +10,53 @@ already have host types by then. Only strings need copying back.
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl32.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Valve's Mesa (the Steam Frame's Lepton) writes a marker to the kernel's
|
||||
trace_marker around every flush and context switch, a couple of hundred a
|
||||
frame, whether or not anything traces. The descriptors it holds open on that
|
||||
file are pointed at /dev/null instead (not closed: a reused descriptor would
|
||||
take the markers elsewhere). After astromaddie/HaloCE-VR (CC0). Returns how
|
||||
many were. */
|
||||
int host_gl_quiet_trace_markers(void)
|
||||
{
|
||||
DIR *directory = opendir("/proc/self/fd");
|
||||
struct dirent *entry;
|
||||
int quieted = 0, null_fd;
|
||||
|
||||
if (!directory)
|
||||
return 0;
|
||||
null_fd = open("/dev/null", O_WRONLY | O_CLOEXEC);
|
||||
while (null_fd >= 0 && (entry = readdir(directory)) != NULL)
|
||||
{
|
||||
char link[64], target[256];
|
||||
ssize_t length;
|
||||
int fd = atoi(entry->d_name);
|
||||
|
||||
if (entry->d_name[0] < '0' || entry->d_name[0] > '9' || fd == null_fd)
|
||||
continue;
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
|
||||
length = readlink(link, target, sizeof(target) - 1);
|
||||
if (length <= 0)
|
||||
continue;
|
||||
target[length] = 0;
|
||||
if (strstr(target, "tracing/trace_marker") && dup3(null_fd, fd, O_CLOEXEC) == fd)
|
||||
quieted++;
|
||||
}
|
||||
if (null_fd >= 0)
|
||||
close(null_fd);
|
||||
closedir(directory);
|
||||
if (quieted)
|
||||
host_logf(HOST_LOG_INFO, "trace markers: %d descriptors to /dev/null", quieted);
|
||||
return quieted;
|
||||
}
|
||||
|
||||
void *host_gl_resolve(const char *name)
|
||||
{
|
||||
@@ -78,6 +123,57 @@ void host_gl_read_buffer(uint32_t buffer, uint32_t offset, uint32_t size, void *
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
}
|
||||
|
||||
/* The visibility tests' counters (d3d8_gl.c), copied at the end of each
|
||||
frame into a ring of buffers that are read once the GPU has finished that
|
||||
frame: reading the counters themselves (host_gl_read_buffer_word) waits for
|
||||
the GPU in the middle of a frame, which on a tiler (Adreno) also writes the
|
||||
half-drawn targets out of tile memory, for every lens flare. */
|
||||
#define COUNTER_SNAPSHOTS 8
|
||||
|
||||
static GLuint counter_snapshots[COUNTER_SNAPSHOTS];
|
||||
static uint32_t counter_snapshot_sizes[COUNTER_SNAPSHOTS];
|
||||
|
||||
/* (the copy bindings are left unbound after: the renderer binds them itself
|
||||
for each copy. Asking GL what they were would wait for Mesa's GL thread, on
|
||||
the Frame, to run everything queued) */
|
||||
void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
{
|
||||
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
|
||||
return;
|
||||
if (!counter_snapshots[slot])
|
||||
glGenBuffers(1, &counter_snapshots[slot]);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
if (counter_snapshot_sizes[slot] != size)
|
||||
{
|
||||
glBufferData(GL_COPY_WRITE_BUFFER, size, NULL, GL_STREAM_READ);
|
||||
counter_snapshot_sizes[slot] = size;
|
||||
}
|
||||
/* the counts come from shaders' atomic writes */
|
||||
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, buffer);
|
||||
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, size);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
}
|
||||
|
||||
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
|
||||
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
{
|
||||
const void *mapping;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
|
||||
return 0;
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
|
||||
if (mapping)
|
||||
{
|
||||
memcpy(out, mapping, size);
|
||||
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
|
||||
}
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
return mapping != NULL;
|
||||
}
|
||||
|
||||
/* The renderer streams each frame's vertices and indices into the next of
|
||||
a ring of buffers (d3d8_gl.c). A fence marks the end of each frame's work,
|
||||
and a buffer is written again only once the GPU has passed the fence of the
|
||||
@@ -106,6 +202,92 @@ void host_gl_wait_frame(uint32_t slot)
|
||||
frame_fences[slot] = NULL;
|
||||
}
|
||||
|
||||
/* Buffers the renderer writes into for every draw (d3d8_gl.c: the stream
|
||||
and index rings, the visibility counters), mapped once for good where the
|
||||
driver has GL_EXT_buffer_storage. Adreno (Quest) waits for the GPU in every
|
||||
glMapBufferRange, unsynchronized or not, and the game streams several
|
||||
hundred small uploads a frame; into a persistent mapping each is a memcpy.
|
||||
The renderer's frame fences keep it from writing what the GPU still reads,
|
||||
as with the unsynchronized mappings. */
|
||||
#define PERSISTENT_BUFFERS 48 /* the rings, the counters and the mirror's 32 segments */
|
||||
|
||||
static struct persistent_buffer
|
||||
{
|
||||
GLuint name;
|
||||
uint32_t size;
|
||||
uint8_t *mapping;
|
||||
} persistent_buffers[PERSISTENT_BUFFERS];
|
||||
static int persistent_buffer_count;
|
||||
|
||||
static GLenum binding_of(uint32_t target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case GL_ARRAY_BUFFER: return GL_ARRAY_BUFFER_BINDING;
|
||||
case GL_ELEMENT_ARRAY_BUFFER: return GL_ELEMENT_ARRAY_BUFFER_BINDING;
|
||||
case GL_ATOMIC_COUNTER_BUFFER: return GL_ATOMIC_COUNTER_BUFFER_BINDING;
|
||||
case GL_COPY_WRITE_BUFFER: return GL_COPY_WRITE_BUFFER_BINDING;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* gives the buffer bound to target persistent, coherent storage of size
|
||||
bytes, instead of glBufferData; 0 if the driver cannot */
|
||||
int host_gl_buffer_storage(uint32_t target, uint32_t size)
|
||||
{
|
||||
static int checked;
|
||||
static PFNGLBUFFERSTORAGEEXTPROC buffer_storage;
|
||||
const GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT;
|
||||
GLint name = 0;
|
||||
void *mapping;
|
||||
|
||||
if (!checked)
|
||||
{
|
||||
checked = 1;
|
||||
if (host_gl_has_extension("GL_EXT_buffer_storage"))
|
||||
buffer_storage = (PFNGLBUFFERSTORAGEEXTPROC)eglGetProcAddress("glBufferStorageEXT");
|
||||
host_logf(HOST_LOG_INFO, "persistent buffers: %s", buffer_storage ? "yes" : "no");
|
||||
}
|
||||
if (!buffer_storage || !binding_of(target) || persistent_buffer_count == PERSISTENT_BUFFERS)
|
||||
return 0;
|
||||
glGetIntegerv(binding_of(target), &name);
|
||||
if (!name)
|
||||
return 0;
|
||||
/* dynamic storage keeps glBufferSubData working on it too */
|
||||
buffer_storage(target, size, NULL, flags | GL_DYNAMIC_STORAGE_BIT_EXT);
|
||||
mapping = glMapBufferRange(target, 0, size, flags);
|
||||
if (!mapping)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "persistent buffer %d cannot be mapped", name);
|
||||
return 0;
|
||||
}
|
||||
persistent_buffers[persistent_buffer_count].name = (GLuint)name;
|
||||
persistent_buffers[persistent_buffer_count].size = size;
|
||||
persistent_buffers[persistent_buffer_count].mapping = mapping;
|
||||
persistent_buffer_count++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* writes into a buffer given persistent storage; 0 if it has none */
|
||||
int host_gl_persistent_write(uint32_t buffer, uint32_t offset, uint32_t size, const void *data)
|
||||
{
|
||||
int index;
|
||||
|
||||
for (index = 0; index < persistent_buffer_count; index++)
|
||||
{
|
||||
struct persistent_buffer *entry = &persistent_buffers[index];
|
||||
|
||||
if (entry->name == buffer)
|
||||
{
|
||||
if (offset > entry->size || size > entry->size - offset)
|
||||
return 0;
|
||||
memcpy(entry->mapping + offset, data, size);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* writes data into the buffer bound to target. The renderer streams a
|
||||
range per draw, so a frame makes hundreds of these, and the cost per call
|
||||
rather than per byte is what a frame is made of.
|
||||
|
||||
@@ -0,0 +1,671 @@
|
||||
/*
|
||||
HOST_GL_WORKER.C
|
||||
|
||||
The shader programs' worker (port/linux/src/xgpu_program_cache.c): a thread
|
||||
with a GL context of its own, sharing the game's objects, that makes the
|
||||
programs the game asks for while the game goes on drawing. The drivers
|
||||
compile a program the first time it is drawn with, 10 to 70 ms each on the
|
||||
Quest's, and the frame waited for it: in VR a frame shown late, the picture
|
||||
doubled as the head turns.
|
||||
|
||||
- host_gl_worker_start, on the game's thread with its context current:
|
||||
makes a context that shares the game's (surfaceless, or on a 1x1
|
||||
pbuffer) and the thread that keeps it current.
|
||||
- host_gl_program_request: a program of these sources, from their binary
|
||||
where one is given and the driver takes it, else compiled and linked
|
||||
(its binary kept for the cache's file). The sources and the binary are
|
||||
copied: the caller's may go at once. Urgent requests (a draw waits for
|
||||
them) go before the others (a map's programs made ready as it loads).
|
||||
- On the worker each program is linked, its samplers set to their units
|
||||
(as d3d8_gl.c sets them), drawn with once into a small target of its own
|
||||
(a degenerate triangle: the driver makes the variants it makes at a
|
||||
first draw now, not in the game's frame; "warm up"), and a fence put
|
||||
after it all.
|
||||
- host_gl_program_poll, on the game's thread: done once the fence has
|
||||
passed (GL's rule for an object made in one context and used in another:
|
||||
the work finished before the other binds it); then the program is the
|
||||
game's (a program name is the same in both contexts).
|
||||
|
||||
host_gl_program_warm_up draws once with a program on the calling thread's
|
||||
context instead (debug.shader_warm_up "load": on the game's, as a map
|
||||
loads), for comparing the two.
|
||||
*/
|
||||
|
||||
#include "host.h"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl32.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define JOB_BUCKETS 1024
|
||||
/* the warm-up's target, each way */
|
||||
#define WARM_UP_SIZE 16
|
||||
|
||||
enum
|
||||
{
|
||||
JOB_QUEUED,
|
||||
JOB_WORKING,
|
||||
JOB_DONE,
|
||||
JOB_FAILED,
|
||||
};
|
||||
|
||||
struct job
|
||||
{
|
||||
struct job *next_in_bucket;
|
||||
struct job *next_in_queue;
|
||||
uint32_t id;
|
||||
int urgent;
|
||||
char *sources[2];
|
||||
void *binary;
|
||||
uint32_t binary_size;
|
||||
uint32_t binary_format;
|
||||
int keep_binary;
|
||||
int state;
|
||||
/* the results */
|
||||
GLuint program;
|
||||
GLsync sync;
|
||||
int from_binary;
|
||||
void *kept_binary;
|
||||
uint32_t kept_size;
|
||||
uint32_t kept_format;
|
||||
uint32_t microseconds;
|
||||
};
|
||||
|
||||
/* a context's warm-up target: the eyes' kinds, one layer and two
|
||||
(multiview), RGBA8 with a depth and stencil buffer */
|
||||
struct warm_up_targets
|
||||
{
|
||||
int made;
|
||||
/* each kind's framebuffer complete */
|
||||
int complete[2];
|
||||
GLuint framebuffers[2];
|
||||
GLuint textures[4];
|
||||
GLuint vertex_array;
|
||||
GLuint counters;
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
int started;
|
||||
int running;
|
||||
int warm_up;
|
||||
EGLDisplay display;
|
||||
EGLContext context;
|
||||
EGLSurface surface;
|
||||
pthread_t thread;
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t wake;
|
||||
pthread_cond_t ready;
|
||||
struct job *buckets[JOB_BUCKETS];
|
||||
struct job *urgent_first, *urgent_last;
|
||||
struct job *later_first, *later_last;
|
||||
uint32_t next_id;
|
||||
uint32_t waiting;
|
||||
int failures_logged;
|
||||
PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC framebuffer_texture_multiview;
|
||||
struct warm_up_targets targets[2];
|
||||
} worker;
|
||||
|
||||
static uint64_t now_us(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (uint64_t)now.tv_sec * 1000000u + (uint64_t)now.tv_nsec / 1000u;
|
||||
}
|
||||
|
||||
/* ---------- the warm-up draw */
|
||||
|
||||
static int targets_make(struct warm_up_targets *targets)
|
||||
{
|
||||
GLint major = 0, minor = 0;
|
||||
int kind;
|
||||
|
||||
if (targets->made)
|
||||
return targets->made > 0;
|
||||
targets->made = -1;
|
||||
glGenTextures(4, targets->textures);
|
||||
glGenFramebuffers(2, targets->framebuffers);
|
||||
for (kind = 0; kind < 2; kind++)
|
||||
{
|
||||
GLenum target = kind ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D;
|
||||
|
||||
glBindTexture(target, targets->textures[kind * 2]);
|
||||
if (kind)
|
||||
glTexStorage3D(target, 1, GL_RGBA8, WARM_UP_SIZE, WARM_UP_SIZE, 2);
|
||||
else
|
||||
glTexStorage2D(target, 1, GL_RGBA8, WARM_UP_SIZE, WARM_UP_SIZE);
|
||||
glBindTexture(target, targets->textures[kind * 2 + 1]);
|
||||
if (kind)
|
||||
glTexStorage3D(target, 1, GL_DEPTH24_STENCIL8, WARM_UP_SIZE, WARM_UP_SIZE, 2);
|
||||
else
|
||||
glTexStorage2D(target, 1, GL_DEPTH24_STENCIL8, WARM_UP_SIZE, WARM_UP_SIZE);
|
||||
glBindTexture(target, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, targets->framebuffers[kind]);
|
||||
if (kind)
|
||||
{
|
||||
if (!worker.framebuffer_texture_multiview)
|
||||
continue;
|
||||
worker.framebuffer_texture_multiview(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, targets->textures[2], 0, 0, 2);
|
||||
worker.framebuffer_texture_multiview(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, targets->textures[3], 0,
|
||||
0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targets->textures[0], 0);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, targets->textures[1], 0);
|
||||
}
|
||||
targets->complete[kind] = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
if (!targets->complete[kind])
|
||||
host_logf(HOST_LOG_WARN, "gl worker: the %s warm-up target is incomplete; no warm-up there",
|
||||
kind ? "multiview" : "plain");
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glGenVertexArrays(1, &targets->vertex_array);
|
||||
/* (the visibility tests' programs count samples into binding 0) */
|
||||
glGetIntegerv(GL_MAJOR_VERSION, &major);
|
||||
glGetIntegerv(GL_MINOR_VERSION, &minor);
|
||||
if (major > 3 || (major == 3 && minor >= 1))
|
||||
{
|
||||
glGenBuffers(1, &targets->counters);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, targets->counters);
|
||||
glBufferData(GL_ATOMIC_COUNTER_BUFFER, 64, NULL, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
||||
}
|
||||
targets->made = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* one degenerate triangle with the program into the target of its kind,
|
||||
depth tested and blended as most of the game's draws are; the GL state it
|
||||
changes is left changed (the worker's context has no other use, and the
|
||||
game's sets it again: d3d8_host_state_changed) */
|
||||
static void warm_up_draw(struct warm_up_targets *targets, GLuint program, int multiview)
|
||||
{
|
||||
if (!targets_make(targets) || !targets->complete[multiview ? 1 : 0])
|
||||
return;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, targets->framebuffers[multiview ? 1 : 0]);
|
||||
glViewport(0, 0, WARM_UP_SIZE, WARM_UP_SIZE);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glBindVertexArray(targets->vertex_array);
|
||||
if (targets->counters)
|
||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, targets->counters);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void host_gl_program_warm_up(uint32_t program, int multiview)
|
||||
{
|
||||
if (!worker.running || !program)
|
||||
return;
|
||||
warm_up_draw(&worker.targets[0], program, multiview);
|
||||
}
|
||||
|
||||
/* ---------- the worker's jobs */
|
||||
|
||||
static struct job *job_find(uint32_t id)
|
||||
{
|
||||
struct job *job;
|
||||
|
||||
for (job = worker.buckets[id % JOB_BUCKETS]; job; job = job->next_in_bucket)
|
||||
{
|
||||
if (job->id == id)
|
||||
return job;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void queue_append(struct job *job)
|
||||
{
|
||||
struct job **first = job->urgent ? &worker.urgent_first : &worker.later_first;
|
||||
struct job **last = job->urgent ? &worker.urgent_last : &worker.later_last;
|
||||
|
||||
job->next_in_queue = NULL;
|
||||
if (*last)
|
||||
(*last)->next_in_queue = job;
|
||||
else
|
||||
*first = job;
|
||||
*last = job;
|
||||
}
|
||||
|
||||
static struct job *queue_take(void)
|
||||
{
|
||||
struct job *job = worker.urgent_first;
|
||||
|
||||
if (job)
|
||||
{
|
||||
worker.urgent_first = job->next_in_queue;
|
||||
if (!worker.urgent_first)
|
||||
worker.urgent_last = NULL;
|
||||
return job;
|
||||
}
|
||||
job = worker.later_first;
|
||||
if (job)
|
||||
{
|
||||
worker.later_first = job->next_in_queue;
|
||||
if (!worker.later_first)
|
||||
worker.later_last = NULL;
|
||||
}
|
||||
return job;
|
||||
}
|
||||
|
||||
static GLuint shader_compile(GLenum type, const char *source)
|
||||
{
|
||||
GLuint shader = glCreateShader(type);
|
||||
GLint status = 0;
|
||||
|
||||
glShaderSource(shader, 1, &source, NULL);
|
||||
glCompileShader(shader);
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (!status)
|
||||
{
|
||||
char log[1024];
|
||||
|
||||
log[0] = 0;
|
||||
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
|
||||
if (worker.failures_logged++ < 4)
|
||||
host_logf(HOST_LOG_WARN, "gl worker: a %s shader does not compile: %s",
|
||||
type == GL_VERTEX_SHADER ? "vertex" : "pixel", log);
|
||||
glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
/* the job's program (outside the lock: only the worker touches a job
|
||||
being worked on) */
|
||||
static void job_work(struct job *job)
|
||||
{
|
||||
uint64_t start = now_us();
|
||||
GLuint program = 0;
|
||||
GLint status = 0;
|
||||
|
||||
if (job->binary && job->binary_size)
|
||||
{
|
||||
program = glCreateProgram();
|
||||
glProgramBinary(program, (GLenum)job->binary_format, job->binary, (GLsizei)job->binary_size);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (status)
|
||||
job->from_binary = 1;
|
||||
else
|
||||
{
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
while (glGetError() != GL_NO_ERROR)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!program && job->sources[0] && job->sources[1])
|
||||
{
|
||||
GLuint vertex = shader_compile(GL_VERTEX_SHADER, job->sources[0]);
|
||||
GLuint fragment = vertex ? shader_compile(GL_FRAGMENT_SHADER, job->sources[1]) : 0;
|
||||
|
||||
if (vertex && fragment)
|
||||
{
|
||||
program = glCreateProgram();
|
||||
glProgramParameteri(program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GL_TRUE);
|
||||
glAttachShader(program, vertex);
|
||||
glAttachShader(program, fragment);
|
||||
glLinkProgram(program);
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
if (!status)
|
||||
{
|
||||
char log[1024];
|
||||
|
||||
log[0] = 0;
|
||||
glGetProgramInfoLog(program, sizeof(log), NULL, log);
|
||||
if (worker.failures_logged++ < 4)
|
||||
host_logf(HOST_LOG_WARN, "gl worker: a program does not link: %s", log);
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
}
|
||||
/* (attached, they go with the program) */
|
||||
if (vertex)
|
||||
glDeleteShader(vertex);
|
||||
if (fragment)
|
||||
glDeleteShader(fragment);
|
||||
if (program && job->keep_binary)
|
||||
{
|
||||
GLint length = 0;
|
||||
GLsizei written = 0;
|
||||
GLenum format = 0;
|
||||
|
||||
glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &length);
|
||||
if (length > 0 && (job->kept_binary = malloc((size_t)length)) != NULL)
|
||||
{
|
||||
glGetProgramBinary(program, length, &written, &format, job->kept_binary);
|
||||
if (written > 0 && written <= length)
|
||||
{
|
||||
job->kept_size = (uint32_t)written;
|
||||
job->kept_format = (uint32_t)format;
|
||||
}
|
||||
else
|
||||
{
|
||||
free(job->kept_binary);
|
||||
job->kept_binary = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (program)
|
||||
{
|
||||
int unit;
|
||||
|
||||
/* the samplers' units, as d3d8_gl.c's program_get sets them (a
|
||||
first draw with two kinds of sampler on one unit draws nothing) */
|
||||
glUseProgram(program);
|
||||
for (unit = 0; unit < 4; unit++)
|
||||
{
|
||||
char name[8];
|
||||
|
||||
snprintf(name, sizeof(name), "tex%d", unit);
|
||||
glUniform1i(glGetUniformLocation(program, name), unit);
|
||||
}
|
||||
glUseProgram(0);
|
||||
if (worker.warm_up)
|
||||
warm_up_draw(&worker.targets[1], program,
|
||||
job->sources[0] && strstr(job->sources[0], "GL_OVR_multiview") != NULL);
|
||||
job->sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
}
|
||||
glFlush();
|
||||
job->program = program;
|
||||
job->microseconds = (uint32_t)(now_us() - start);
|
||||
}
|
||||
|
||||
static void *worker_main(void *unused)
|
||||
{
|
||||
(void)unused;
|
||||
if (!eglMakeCurrent(worker.display, worker.surface, worker.surface, worker.context))
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "gl worker: its context cannot be made current (0x%x)", eglGetError());
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
worker.started = -1;
|
||||
pthread_cond_broadcast(&worker.ready);
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
return NULL;
|
||||
}
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
worker.started = 1;
|
||||
pthread_cond_broadcast(&worker.ready);
|
||||
for (;;)
|
||||
{
|
||||
struct job *job;
|
||||
|
||||
while ((job = queue_take()) == NULL)
|
||||
pthread_cond_wait(&worker.wake, &worker.lock);
|
||||
job->state = JOB_WORKING;
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
job_work(job);
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
free(job->binary);
|
||||
job->binary = NULL;
|
||||
free(job->sources[0]);
|
||||
free(job->sources[1]);
|
||||
job->sources[0] = job->sources[1] = NULL;
|
||||
job->state = job->program ? JOB_DONE : JOB_FAILED;
|
||||
worker.waiting--;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---------- the game's */
|
||||
|
||||
int host_gl_worker_start(int warm_up)
|
||||
{
|
||||
EGLDisplay display = eglGetCurrentDisplay();
|
||||
EGLContext shared = eglGetCurrentContext();
|
||||
EGLint config_id = 0, count = 0, version = 3;
|
||||
EGLint config_attributes[] = { EGL_CONFIG_ID, 0, EGL_NONE };
|
||||
EGLint context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
|
||||
EGLConfig config;
|
||||
const char *extensions;
|
||||
int started;
|
||||
|
||||
if (worker.running)
|
||||
return 1;
|
||||
if (worker.started)
|
||||
return 0;
|
||||
worker.warm_up = warm_up;
|
||||
if (display == EGL_NO_DISPLAY || shared == EGL_NO_CONTEXT)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "gl worker: no current context to share");
|
||||
worker.started = -1;
|
||||
return 0;
|
||||
}
|
||||
eglQueryContext(display, shared, EGL_CONFIG_ID, &config_id);
|
||||
eglQueryContext(display, shared, EGL_CONTEXT_CLIENT_VERSION, &version);
|
||||
config_attributes[1] = config_id;
|
||||
if (!eglChooseConfig(display, config_attributes, &config, 1, &count) || count != 1)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "gl worker: the game's context's configuration is not found");
|
||||
worker.started = -1;
|
||||
return 0;
|
||||
}
|
||||
context_attributes[1] = version;
|
||||
extensions = eglQueryString(display, EGL_EXTENSIONS);
|
||||
worker.surface = EGL_NO_SURFACE;
|
||||
if (!extensions || !strstr(extensions, "EGL_KHR_surfaceless_context"))
|
||||
{
|
||||
/* (a 1x1 pbuffer to be current on, of a configuration that has one) */
|
||||
EGLint pbuffer_config[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||
EGL_NONE };
|
||||
EGLint pbuffer_size[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE };
|
||||
|
||||
if (!eglChooseConfig(display, pbuffer_config, &config, 1, &count) || count != 1 ||
|
||||
(worker.surface = eglCreatePbufferSurface(display, config, pbuffer_size)) == EGL_NO_SURFACE)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "gl worker: neither surfaceless contexts nor a pbuffer");
|
||||
worker.started = -1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
worker.context = eglCreateContext(display, config, shared, context_attributes);
|
||||
if (worker.context == EGL_NO_CONTEXT)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "gl worker: no context sharing the game's (0x%x)", eglGetError());
|
||||
worker.started = -1;
|
||||
return 0;
|
||||
}
|
||||
worker.display = display;
|
||||
worker.next_id = 1;
|
||||
worker.framebuffer_texture_multiview = (PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC)eglGetProcAddress(
|
||||
"glFramebufferTextureMultiviewOVR");
|
||||
pthread_mutex_init(&worker.lock, NULL);
|
||||
pthread_cond_init(&worker.wake, NULL);
|
||||
pthread_cond_init(&worker.ready, NULL);
|
||||
if (pthread_create(&worker.thread, NULL, worker_main, NULL) != 0)
|
||||
{
|
||||
eglDestroyContext(display, worker.context);
|
||||
worker.started = -1;
|
||||
return 0;
|
||||
}
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
while (!worker.started)
|
||||
pthread_cond_wait(&worker.ready, &worker.lock);
|
||||
started = worker.started > 0;
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
worker.running = started;
|
||||
/* (Mesa's trace markers, for the new context's too: host_gl.c) */
|
||||
if (started)
|
||||
host_gl_quiet_trace_markers();
|
||||
host_logf(HOST_LOG_INFO, "gl worker: %s (ES %d, %s%s)", started ? "running" : "failed", (int)version,
|
||||
worker.surface == EGL_NO_SURFACE ? "surfaceless" : "on a pbuffer", warm_up ? ", programs warmed up" : "");
|
||||
return started;
|
||||
}
|
||||
|
||||
uint32_t host_gl_program_request(int urgent, const char *vertex_source, const char *fragment_source,
|
||||
uint32_t binary_format, const void *binary, uint32_t binary_size, int keep_binary)
|
||||
{
|
||||
struct job *job;
|
||||
uint32_t id;
|
||||
|
||||
if (!worker.running)
|
||||
return 0;
|
||||
job = calloc(1, sizeof(*job));
|
||||
if (!job)
|
||||
return 0;
|
||||
job->urgent = urgent != 0;
|
||||
job->keep_binary = keep_binary != 0;
|
||||
job->sources[0] = vertex_source ? strdup(vertex_source) : NULL;
|
||||
job->sources[1] = fragment_source ? strdup(fragment_source) : NULL;
|
||||
if (binary && binary_size && (job->binary = malloc(binary_size)) != NULL)
|
||||
{
|
||||
memcpy(job->binary, binary, binary_size);
|
||||
job->binary_size = binary_size;
|
||||
job->binary_format = binary_format;
|
||||
}
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
id = job->id = worker.next_id++;
|
||||
if (!worker.next_id)
|
||||
worker.next_id = 1;
|
||||
job->state = JOB_QUEUED;
|
||||
job->next_in_bucket = worker.buckets[id % JOB_BUCKETS];
|
||||
worker.buckets[id % JOB_BUCKETS] = job;
|
||||
queue_append(job);
|
||||
worker.waiting++;
|
||||
pthread_cond_signal(&worker.wake);
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
return id;
|
||||
}
|
||||
|
||||
/* a queued job a draw now waits for: before the others */
|
||||
void host_gl_program_hurry(uint32_t id)
|
||||
{
|
||||
struct job *job, **link;
|
||||
|
||||
if (!worker.running)
|
||||
return;
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
job = job_find(id);
|
||||
if (job && job->state == JOB_QUEUED && !job->urgent)
|
||||
{
|
||||
for (link = &worker.later_first; *link; link = &(*link)->next_in_queue)
|
||||
{
|
||||
if (*link == job)
|
||||
{
|
||||
struct job *previous = NULL, *walk;
|
||||
|
||||
*link = job->next_in_queue;
|
||||
/* (the last of the later queue found again) */
|
||||
for (walk = worker.later_first; walk; walk = walk->next_in_queue)
|
||||
previous = walk;
|
||||
worker.later_last = previous;
|
||||
job->urgent = 1;
|
||||
queue_append(job);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
}
|
||||
|
||||
int host_gl_program_poll(uint32_t id, uint32_t *program, uint32_t *binary_size, uint32_t *microseconds)
|
||||
{
|
||||
struct job *job;
|
||||
int result = 0;
|
||||
|
||||
if (!worker.running)
|
||||
return -1;
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
job = job_find(id);
|
||||
if (!job || job->state == JOB_FAILED)
|
||||
result = -1;
|
||||
else if (job->state == JOB_DONE)
|
||||
{
|
||||
/* (the fence passed: everything the worker did with the program is
|
||||
done, and this context may use it) */
|
||||
if (job->sync)
|
||||
{
|
||||
GLenum waited = glClientWaitSync(job->sync, 0, 0);
|
||||
|
||||
if (waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED || waited == GL_WAIT_FAILED)
|
||||
{
|
||||
glDeleteSync(job->sync);
|
||||
job->sync = NULL;
|
||||
}
|
||||
}
|
||||
if (!job->sync)
|
||||
{
|
||||
*program = job->program;
|
||||
*binary_size = job->kept_size;
|
||||
*microseconds = job->microseconds;
|
||||
result = 1;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t host_gl_program_binary(uint32_t id, void *buffer, uint32_t size, uint32_t *format)
|
||||
{
|
||||
struct job *job;
|
||||
uint32_t copied = 0;
|
||||
|
||||
if (!worker.running)
|
||||
return 0;
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
job = job_find(id);
|
||||
if (job && job->state == JOB_DONE && job->kept_binary && size >= job->kept_size)
|
||||
{
|
||||
memcpy(buffer, job->kept_binary, job->kept_size);
|
||||
*format = job->kept_format;
|
||||
copied = job->kept_size;
|
||||
}
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
return copied;
|
||||
}
|
||||
|
||||
/* forgets a job (its program stays: the game's); a job still queued or
|
||||
being worked on is let go when it is done */
|
||||
void host_gl_program_release(uint32_t id)
|
||||
{
|
||||
struct job *job, **link;
|
||||
|
||||
if (!worker.running)
|
||||
return;
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
for (link = &worker.buckets[id % JOB_BUCKETS]; (job = *link) != NULL; link = &job->next_in_bucket)
|
||||
{
|
||||
if (job->id != id)
|
||||
continue;
|
||||
if (job->state == JOB_QUEUED || job->state == JOB_WORKING)
|
||||
break;
|
||||
*link = job->next_in_bucket;
|
||||
if (job->sync)
|
||||
glDeleteSync(job->sync);
|
||||
free(job->kept_binary);
|
||||
free(job);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
}
|
||||
|
||||
uint32_t host_gl_worker_waiting(void)
|
||||
{
|
||||
uint32_t waiting;
|
||||
|
||||
if (!worker.running)
|
||||
return 0;
|
||||
pthread_mutex_lock(&worker.lock);
|
||||
waiting = worker.waiting;
|
||||
pthread_mutex_unlock(&worker.lock);
|
||||
return waiting;
|
||||
}
|
||||
@@ -65,7 +65,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, "Mjolnir VR", message, NULL);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ 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/RingfallMjolnirVR"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -229,6 +231,28 @@ static int config_sample_seconds(const char *path, char *text, size_t size)
|
||||
return found;
|
||||
}
|
||||
|
||||
/* a number from config.toml, or fallback when it is not set */
|
||||
static double config_number(const char *path, const char *key, double fallback)
|
||||
{
|
||||
toml_result_t result = toml_parse_file_ex(path);
|
||||
double value = fallback;
|
||||
|
||||
if (!result.ok)
|
||||
return fallback;
|
||||
{
|
||||
toml_datum_t datum = toml_seek(result.toptab, key);
|
||||
|
||||
if (datum.type == TOML_FP64)
|
||||
value = datum.u.fp64;
|
||||
else if (datum.type == TOML_INT64)
|
||||
value = (double)datum.u.int64;
|
||||
else if (datum.type == TOML_BOOLEAN)
|
||||
value = datum.u.boolean ? 1.0 : 0.0;
|
||||
}
|
||||
toml_free(result);
|
||||
return value;
|
||||
}
|
||||
|
||||
/* a boolean setting of config.toml (a dotted name), or otherwise when the
|
||||
file, the setting or a boolean is missing */
|
||||
static int config_boolean_or(const char *path, const char *name, int otherwise)
|
||||
@@ -331,51 +355,34 @@ static void *game_main(void *unused)
|
||||
external = SDL_GetAndroidExternalStoragePath();
|
||||
if (!external)
|
||||
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, 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);
|
||||
/* readable by adb (the shell user), for managing saves */
|
||||
mkdir(save_root, 0775);
|
||||
share_save_tree(save_root);
|
||||
{
|
||||
struct stat information;
|
||||
|
||||
/* the VR self-test (host_xr.c), which needs no game data */
|
||||
snprintf(path, sizeof(path), "%s/xr_test", data_root);
|
||||
if (stat(path, &information) == 0)
|
||||
host_xr_test();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
environment_set(&environment, "HOME", save_root);
|
||||
environment_set(&environment, "HALO_DATA_ROOT", data_root);
|
||||
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
|
||||
{
|
||||
/* the game renders 480 lines at the display's aspect ratio
|
||||
(landscape) unless display.screen_width says otherwise (d3d8_gl.c) */
|
||||
const SDL_DisplayMode *mode;
|
||||
char width[16];
|
||||
|
||||
SDL_InitSubSystem(SDL_INIT_VIDEO);
|
||||
mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
|
||||
if (mode && mode->w > 0 && mode->h > 0)
|
||||
{
|
||||
int longer = mode->w > mode->h ? mode->w : mode->h;
|
||||
int shorter = mode->w > mode->h ? mode->h : mode->w;
|
||||
|
||||
snprintf(width, sizeof(width), "%d", (480 * longer / shorter) & ~1);
|
||||
environment_set(&environment, "HALO_DISPLAY_WIDTH", width);
|
||||
host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width);
|
||||
}
|
||||
{
|
||||
/* dp for the touch controls (port/linux/src/touch_input.c): SDL
|
||||
gives Android's densityDpi / 160 */
|
||||
float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
|
||||
char text[16];
|
||||
|
||||
if (density > 0.0f)
|
||||
{
|
||||
snprintf(text, sizeof(text), "%g", density);
|
||||
environment_set(&environment, "HALO_DISPLAY_DENSITY", text);
|
||||
host_logf(HOST_LOG_INFO, "display density %s", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
time_zone(zone, sizeof(zone));
|
||||
environment_set(&environment, "TZ", zone);
|
||||
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
|
||||
@@ -413,6 +420,85 @@ static void *game_main(void *unused)
|
||||
}
|
||||
SDL_free(image);
|
||||
|
||||
/* after the image, whose loading reserves the guest's fixed address
|
||||
ranges: the headset's runtime, loaded by the probe, maps its libraries
|
||||
wherever there is room, the Xbox window at 0x80000000 included */
|
||||
snprintf(path, sizeof(path), "%s/config.toml", data_root);
|
||||
{
|
||||
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
|
||||
mesa_glthread, before the GL context is made; -1 leaves it be) */
|
||||
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
|
||||
|
||||
if (gl_thread >= 0.0)
|
||||
{
|
||||
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
|
||||
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
|
||||
}
|
||||
}
|
||||
{
|
||||
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
|
||||
host_xr.c, or one by one; -1 as the runtime has it, deferred on
|
||||
SteamVR) */
|
||||
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
|
||||
|
||||
if (deferred >= 0.0)
|
||||
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
|
||||
}
|
||||
/* (vr.log_performance: the compositor's counters too, host_xr.c) */
|
||||
if (config_boolean_or(path, "vr.log_performance", 0))
|
||||
setenv("HALO_XR_LOG_PERFORMANCE", "1", 1);
|
||||
{
|
||||
int eye_width, eye_height;
|
||||
|
||||
/* on a headset the game runs in VR (port/linux/src/vr.c): it
|
||||
keeps the Xbox's 640x480 layout and draws it at the eye images'
|
||||
resolution (d3d8_gl.c) */
|
||||
if (config_number(path, "vr.enabled", 1.0) != 0.0 &&
|
||||
host_xr_probe((float)config_number(path, "vr.resolution_scale", 1.0), &eye_width, &eye_height))
|
||||
{
|
||||
char text[16];
|
||||
|
||||
environment_set(&environment, "HALO_VR", "1");
|
||||
snprintf(text, sizeof(text), "%d", eye_width);
|
||||
environment_set(&environment, "HALO_VR_EYE_WIDTH", text);
|
||||
snprintf(text, sizeof(text), "%d", eye_height);
|
||||
environment_set(&environment, "HALO_VR_EYE_HEIGHT", text);
|
||||
environment_set(&environment, "HALO_DISPLAY_WIDTH", "640");
|
||||
}
|
||||
else
|
||||
{
|
||||
/* the game renders 480 lines at the display's aspect ratio
|
||||
(landscape) unless display.screen_width says otherwise (d3d8_gl.c) */
|
||||
const SDL_DisplayMode *mode;
|
||||
char width[16];
|
||||
char text_density[16];
|
||||
|
||||
SDL_InitSubSystem(SDL_INIT_VIDEO);
|
||||
mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
|
||||
if (mode && mode->w > 0 && mode->h > 0)
|
||||
{
|
||||
int longer = mode->w > mode->h ? mode->w : mode->h;
|
||||
int shorter = mode->w > mode->h ? mode->h : mode->w;
|
||||
|
||||
snprintf(width, sizeof(width), "%d", (480 * longer / shorter) & ~1);
|
||||
environment_set(&environment, "HALO_DISPLAY_WIDTH", width);
|
||||
host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width);
|
||||
}
|
||||
{
|
||||
/* dp for the touch controls (port/linux/src/touch_input.c): SDL
|
||||
gives Android's densityDpi / 160 */
|
||||
float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
|
||||
|
||||
if (density > 0.0f)
|
||||
{
|
||||
snprintf(text_density, sizeof(text_density), "%g", density);
|
||||
environment_set(&environment, "HALO_DISPLAY_DENSITY", text_density);
|
||||
host_logf(HOST_LOG_INFO, "display density %s", text_density);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
char seconds[32];
|
||||
|
||||
@@ -454,7 +540,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, "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");
|
||||
|
||||
@@ -178,7 +178,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
|
||||
{
|
||||
SDL_Window *object = handle_get(window, _handle_window);
|
||||
|
||||
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
|
||||
/* (the driver opens its trace markers with the context) */
|
||||
host_gl_quiet_trace_markers();
|
||||
return context;
|
||||
}
|
||||
|
||||
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
|
||||
|
||||
@@ -53,8 +53,31 @@ host_gl_get_string
|
||||
host_gl_has_extension
|
||||
host_gl_read_buffer
|
||||
host_gl_buffer_write
|
||||
host_gl_buffer_storage
|
||||
host_gl_persistent_write
|
||||
host_gl_fence_frame
|
||||
host_gl_wait_frame
|
||||
host_gl_counters_snapshot
|
||||
host_gl_counters_read
|
||||
host_gl_worker_start
|
||||
host_gl_program_request
|
||||
host_gl_program_hurry
|
||||
host_gl_program_poll
|
||||
host_gl_program_binary
|
||||
host_gl_program_release
|
||||
host_gl_worker_waiting
|
||||
host_gl_program_warm_up
|
||||
host_xr_initialize
|
||||
host_xr_begin_frame
|
||||
host_xr_copy_layer
|
||||
host_xr_copy_layer_region
|
||||
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_xr_gpu_time
|
||||
host_android_path
|
||||
host_gesture_insets
|
||||
host_touch_read
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
HALO_XR_ABI.H
|
||||
|
||||
The structures the game (the ILP32 guest, port/linux/src/vr.c) and the
|
||||
host's OpenXR session (port/android/host/host_xr.c) exchange. Both sides
|
||||
compile this header, so every member is a 32-bit int or float: the layout
|
||||
is the same under ILP32 and LP64.
|
||||
|
||||
Poses are in OpenXR's convention: metres, right-handed, +x right, +y up,
|
||||
-z forward, relative to the LOCAL reference space (the head's position and
|
||||
heading when the user last recentred). Quaternions are x, y, z, w.
|
||||
*/
|
||||
|
||||
#ifndef __HALO_XR_ABI_H
|
||||
#define __HALO_XR_ABI_H
|
||||
|
||||
enum
|
||||
{
|
||||
HALO_XR_EYE_LEFT = 0,
|
||||
HALO_XR_EYE_RIGHT = 1,
|
||||
HALO_XR_EYE_COUNT = 2,
|
||||
|
||||
/* the images the game fills each frame (host_xr_copy_layer) */
|
||||
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 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 */
|
||||
HALO_XR_LAYER_COUNT = 7,
|
||||
/* the names layer's slots, one a name, stacked from its top */
|
||||
HALO_XR_MAXIMUM_NAMES = 8,
|
||||
|
||||
/* host_xr_copy_layer's clear_alpha: the source's alpha kept, made
|
||||
opaque, or cut to a disc (premultiplied) */
|
||||
HALO_XR_COPY_ALPHA = 0,
|
||||
HALO_XR_COPY_OPAQUE = 1,
|
||||
HALO_XR_COPY_DISC = 2,
|
||||
|
||||
HALO_XR_HAND_LEFT = 0,
|
||||
HALO_XR_HAND_RIGHT = 1,
|
||||
HALO_XR_HAND_COUNT = 2,
|
||||
};
|
||||
|
||||
/* halo_xr_hand.buttons */
|
||||
enum
|
||||
{
|
||||
HALO_XR_BUTTON_A = 1 << 0, /* right hand: A; left hand: X */
|
||||
HALO_XR_BUTTON_B = 1 << 1, /* right hand: B; left hand: Y */
|
||||
HALO_XR_BUTTON_THUMBSTICK = 1 << 2, /* stick click */
|
||||
HALO_XR_BUTTON_MENU = 1 << 3, /* left hand's menu button */
|
||||
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
|
||||
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
|
||||
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
|
||||
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
|
||||
View button and D-pad, either one's bumper (their right one's X, Y and
|
||||
menu button come as the left hand's A, B and MENU: the game's X, Y and
|
||||
Start, as on the Quest) */
|
||||
HALO_XR_BUTTON_VIEW = 1 << 7,
|
||||
HALO_XR_BUTTON_BUMPER = 1 << 8,
|
||||
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
|
||||
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
|
||||
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
|
||||
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
|
||||
/* the fingers' touch (the hands drawn: vr.c): the thumb on the stick, a
|
||||
button or the rest; the index finger on the trigger; and whether the
|
||||
controller tells the thumb's at all */
|
||||
HALO_XR_BUTTON_THUMB_TOUCH = 1 << 13,
|
||||
HALO_XR_BUTTON_INDEX_TOUCH = 1 << 14,
|
||||
HALO_XR_BUTTON_TOUCH_KNOWN = 1 << 15,
|
||||
};
|
||||
|
||||
/* the controllers' layout, for what the game tells the player */
|
||||
enum
|
||||
{
|
||||
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
|
||||
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
|
||||
};
|
||||
|
||||
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
|
||||
enum
|
||||
{
|
||||
HALO_XR_POSE_VALID = 1 << 0,
|
||||
HALO_XR_POSE_TRACKED = 1 << 1,
|
||||
};
|
||||
|
||||
struct halo_xr_pose
|
||||
{
|
||||
float orientation[4];
|
||||
float position[3];
|
||||
};
|
||||
|
||||
struct halo_xr_view
|
||||
{
|
||||
int flags;
|
||||
struct halo_xr_pose pose;
|
||||
/* tangents of the view's edges (left and down negative) */
|
||||
float tangent_left;
|
||||
float tangent_right;
|
||||
float tangent_up;
|
||||
float tangent_down;
|
||||
};
|
||||
|
||||
struct halo_xr_hand
|
||||
{
|
||||
int flags;
|
||||
int active; /* a controller of this hand is connected */
|
||||
struct halo_xr_pose grip;
|
||||
struct halo_xr_pose aim;
|
||||
float trigger; /* 0..1 */
|
||||
float squeeze; /* 0..1 */
|
||||
float thumbstick[2]; /* x right, y up; -1..1 */
|
||||
int buttons; /* HALO_XR_BUTTON_* */
|
||||
};
|
||||
|
||||
struct halo_xr_info
|
||||
{
|
||||
int eye_width; /* the eye images' size in pixels */
|
||||
int eye_height;
|
||||
int overlay_width;
|
||||
int overlay_height;
|
||||
float refresh_rate; /* Hz; 0 if unknown */
|
||||
char system_name[64];
|
||||
};
|
||||
|
||||
struct halo_xr_frame
|
||||
{
|
||||
int session_running; /* frames are being submitted to the headset */
|
||||
int should_render; /* the eye images will be shown this frame */
|
||||
int focused; /* input goes to the app (no system menu over it) */
|
||||
int recentred; /* the LOCAL space moved since the last frame */
|
||||
float predicted_display_seconds;
|
||||
int head_flags;
|
||||
struct halo_xr_pose head;
|
||||
struct halo_xr_view views[HALO_XR_EYE_COUNT];
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int controller_layout; /* HALO_XR_LAYOUT_* */
|
||||
};
|
||||
|
||||
/* halo_xr_submit.overlay_mode */
|
||||
enum
|
||||
{
|
||||
HALO_XR_OVERLAY_NONE = 0,
|
||||
HALO_XR_OVERLAY_HEAD_LOCKED = 1, /* the HUD: follows the head */
|
||||
HALO_XR_OVERLAY_SCREEN = 2, /* menus and cinematics: a screen fixed in the room */
|
||||
};
|
||||
|
||||
struct halo_xr_submit
|
||||
{
|
||||
int eyes; /* the eye layers were filled this frame */
|
||||
int overlay_mode; /* HALO_XR_OVERLAY_* */
|
||||
int overlay_blend; /* the overlay has alpha (premultiplied): it is drawn over the eyes */
|
||||
float overlay_distance; /* metres */
|
||||
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 (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 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;
|
||||
float scope_size;
|
||||
struct halo_xr_pose scope_pose;
|
||||
/* the crosshair layer was filled: shown this wide (metres), here in
|
||||
LOCAL space, facing the eyes */
|
||||
int crosshair;
|
||||
float crosshair_size;
|
||||
struct halo_xr_pose crosshair_pose;
|
||||
/* the names layer was filled: its first name_count slots, each shown
|
||||
this wide (metres, a slot's height following from its shape), here in
|
||||
LOCAL space, facing the eyes */
|
||||
int name_count;
|
||||
float name_widths[HALO_XR_MAXIMUM_NAMES];
|
||||
struct halo_xr_pose name_poses[HALO_XR_MAXIMUM_NAMES];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 70 KiB |
@@ -0,0 +1,308 @@
|
||||
#!/bin/bash
|
||||
# Mjolnir VR's installer for the Steam Frame, run in its Desktop
|
||||
# Mode from the bundle the release's "Install 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).
|
||||
# MJOLNIR_UNATTENDED=1 answers every question with its default and prints
|
||||
# instead of showing dialogs (MJOLNIR_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="Mjolnir VR"
|
||||
NAME="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="Mjolnir VR"
|
||||
# the builds before the name: their folder and Steam shortcut
|
||||
OLD_DIR="$HOME/Documents/HaloCE"
|
||||
OLD_NAME="Halo VR"
|
||||
# the name of builds 302 to 313, which their shortcut keeps (Steam writes
|
||||
# its shortcuts back from memory: the player renames it)
|
||||
FORMER_NAME="Ringfall: Mjolnir 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/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() { [ "${MJOLNIR_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
|
||||
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
|
||||
add_pictures
|
||||
return 0
|
||||
}
|
||||
|
||||
# the library pictures of the game's shortcut, for every Steam user who has
|
||||
# it (again on an update: the builds before the name "Mjolnir VR" had
|
||||
# Ringfall's)
|
||||
add_pictures()
|
||||
{
|
||||
local file id
|
||||
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)
|
||||
}
|
||||
|
||||
# --- 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 build's number: the launcher updates from it, steam_frame_launch.sh;
|
||||
# an update fetched for an older build is dropped)
|
||||
cp "$BUNDLE/build.txt" "$GAME_DIR/build.txt" 2>/dev/null || rm -f "$GAME_DIR/build.txt"
|
||||
rm -rf "$GAME_DIR/update"
|
||||
|
||||
# --- the game data
|
||||
|
||||
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
image="${MJOLNIR_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"
|
||||
add_pictures
|
||||
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 have_shortcut_to "$FORMER_NAME"; then
|
||||
info "The library shows the game by its former name, \"$FORMER_NAME\". To call it $NAME there: its gear > Properties, then its name at the top."
|
||||
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]
|
||||
# Mjolnir VR's installer for the Steam Frame
|
||||
# (port/android/STEAM_FRAME.md): a release asset,
|
||||
# "Install-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 Mjolnir VR
|
||||
Comment=Installs Mjolnir VR for the Steam Frame, or updates it
|
||||
Icon=steam
|
||||
Terminal=false
|
||||
Exec=bash -c "if curl -fL --retry 3 -o /tmp/mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/mjolnir-vr-steam-frame.zip && rm -rf /tmp/mjolnir-vr-install && python3 -m zipfile -e /tmp/mjolnir-vr-steam-frame.zip /tmp/mjolnir-vr-install; then exec bash /tmp/mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Mjolnir VR' --text='Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/bin/bash
|
||||
# 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/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}"
|
||||
# (what Steam gives an Android title of its own, and a non-Steam game not:
|
||||
# the app's data in the container links to the compat data path's
|
||||
# internal/, a dangling link without it; and the shader paths: Lepton reads
|
||||
# the dump path against the other)
|
||||
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
|
||||
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
|
||||
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"
|
||||
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="$DATA/quit_requested.txt"
|
||||
rm -f "$QUIT"
|
||||
CONTEXT="steamlaunch-$SteamAppId"
|
||||
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
log "start (pid $$, parent $PPID)"
|
||||
|
||||
# Steam's "Exit game" ends the game's processes as Steam knows them (this
|
||||
# script, under Steam's reaper), not the container, which podman runs: a
|
||||
# signal here is passed on to Lepton, and a watcher of its own takes the
|
||||
# container down once this script is gone (killed)
|
||||
stopping=0
|
||||
child=0
|
||||
stop()
|
||||
{
|
||||
log "signal $1"
|
||||
stopping=1
|
||||
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
|
||||
}
|
||||
trap 'stop TERM' TERM
|
||||
trap 'stop INT' INT
|
||||
trap 'stop HUP' HUP
|
||||
|
||||
script=$$
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
|
||||
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
|
||||
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
# the game's log while it runs (Lepton's logcat of its container), for
|
||||
# looking into problems: logcat.txt, anew each start
|
||||
LOGCAT="$DATA/logcat.txt"
|
||||
: > "$LOGCAT"
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do
|
||||
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
|
||||
sleep 3
|
||||
done
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
run()
|
||||
{
|
||||
"$LEPTON" waitforexitandrun -- "$APK" &
|
||||
child=$!
|
||||
# (wait returns early when a signal comes: until Lepton is gone)
|
||||
while kill -0 "$child" 2>/dev/null; do
|
||||
wait "$child"
|
||||
done
|
||||
child=0
|
||||
}
|
||||
|
||||
# Updates (update.auto in config.toml; the game's own updater, the Quest's,
|
||||
# does nothing here: Lepton installs the APK in this folder each start).
|
||||
# While the game runs, the latest release is looked for; a newer one's
|
||||
# bundle is fetched into update/, checked, and put in place as the next
|
||||
# start begins, before Lepton runs the APK (it installs the newer one).
|
||||
# A build that knows no number of its own (build.txt: a local one) never
|
||||
# looks. Nothing changes without the internet, or when anything fails.
|
||||
REPOSITORY="@REPOSITORY@"
|
||||
UPDATE="$DATA/update"
|
||||
BUILD_FILE="$DATA/build.txt"
|
||||
SELF="$DATA/ringfall-mjolnir-vr.sh"
|
||||
|
||||
update_apply()
|
||||
{
|
||||
local new="$UPDATE/bundle" build
|
||||
[ -f "$UPDATE/ready" ] || { rm -rf "$UPDATE"; return 0; }
|
||||
build=$(cat "$new/build.txt" 2>/dev/null)
|
||||
# (the launcher renamed over this one: bash goes on reading its own copy)
|
||||
if cp "$new/ringfall-mjolnir-vr.sh" "$SELF.tmp" && chmod 755 "$SELF.tmp" &&
|
||||
cp "$new/RingfallMjolnirVR.apk" "$APK.tmp" && chmod 666 "$APK.tmp" &&
|
||||
mv -f "$SELF.tmp" "$SELF" && mv -f "$APK.tmp" "$APK" && touch "$APK" &&
|
||||
cp "$new/build.txt" "$BUILD_FILE"
|
||||
then
|
||||
log "update: build $build put in place"
|
||||
else
|
||||
log "update: build $build could not be put in place"
|
||||
rm -f "$SELF.tmp" "$APK.tmp"
|
||||
fi
|
||||
rm -rf "$UPDATE"
|
||||
}
|
||||
|
||||
update_wanted()
|
||||
{
|
||||
# (update.auto = false in config.toml's [update]: never)
|
||||
! awk '/^\[/ { section = $0 } section == "[update]" && /^[ \t]*auto[ \t]*=/ { print $3 }' \
|
||||
"$DATA/config.toml" 2>/dev/null | grep -q false
|
||||
}
|
||||
|
||||
update_check()
|
||||
{
|
||||
local installed latest
|
||||
installed=$(cat "$BUILD_FILE" 2>/dev/null)
|
||||
[[ "$installed" =~ ^[0-9]+$ ]] && (( installed > 0 )) || return 0
|
||||
update_wanted || return 0
|
||||
latest=$(curl -fsSL --max-time 20 -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/$REPOSITORY/releases/latest" |
|
||||
python3 -I -c 'import json, sys; print(json.load(sys.stdin)["tag_name"].rpartition("-")[2])' 2>/dev/null)
|
||||
[[ "$latest" =~ ^[0-9]+$ ]] && (( latest > installed )) || return 0
|
||||
log "update: build $latest found (this is $installed), fetching it"
|
||||
rm -rf "$UPDATE" && mkdir -p "$UPDATE" || return 0
|
||||
if curl -fsSL --retry 2 --max-time 900 -o "$UPDATE/bundle.zip" \
|
||||
"https://github.com/$REPOSITORY/releases/download/build-$latest/mjolnir-vr-steam-frame.zip" &&
|
||||
python3 -I -m zipfile -e "$UPDATE/bundle.zip" "$UPDATE/bundle" &&
|
||||
[ "$(cat "$UPDATE/bundle/build.txt" 2>/dev/null)" = "$latest" ] &&
|
||||
[ -s "$UPDATE/bundle/RingfallMjolnirVR.apk" ] && [ -s "$UPDATE/bundle/ringfall-mjolnir-vr.sh" ]
|
||||
then
|
||||
rm -f "$UPDATE/bundle.zip"
|
||||
touch "$UPDATE/ready"
|
||||
log "update: build $latest fetched, put in place at the next start"
|
||||
else
|
||||
log "update: build $latest could not be fetched"
|
||||
rm -rf "$UPDATE"
|
||||
fi
|
||||
}
|
||||
|
||||
update_apply
|
||||
update_check </dev/null >/dev/null 2>&1 &
|
||||
updater=$!
|
||||
|
||||
# (a new APK is only installed on its first start, not started; and a start
|
||||
# right after a game that was ended hard can fail in SteamVR, which then
|
||||
# needs a moment: again, up to three times, while it ends within 30 s)
|
||||
for attempt in 1 2 3; do
|
||||
started=$SECONDS
|
||||
run
|
||||
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
|
||||
break
|
||||
fi
|
||||
log "ended within $((SECONDS - started)) s: again"
|
||||
sleep 5
|
||||
done
|
||||
rm -f "$QUIT"
|
||||
# (a fetch still under way is given up: the next start looks again)
|
||||
pkill -P "$updater" 2>/dev/null
|
||||
kill "$updater" 2>/dev/null
|
||||
log "end"
|
||||
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copies the maps folder out of the player's Xbox disc image of Halo (an
|
||||
"xiso", or a whole disc's ".iso") on the Steam Frame's SteamOS side, for
|
||||
its installer (steam_frame_install.sh), 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, ui.map moved in last, 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.
|
||||
|
||||
Written from a description of the Xbox's disc file system, XDVDFS
|
||||
(xboxdevwiki's XDVDFS page, https://xboxdevwiki.net/XDVDFS), as
|
||||
XisoExtractor.java is:
|
||||
|
||||
- Sectors are 2048 bytes and integers little-endian. Sector numbers count
|
||||
from the start of the game partition: an image of the partition alone has
|
||||
it at 0, an image of a whole disc further in (PARTITION_STARTS, tried in
|
||||
that order).
|
||||
- The volume descriptor is the partition's sector 32 (at 0x10000). It holds
|
||||
"MICROSOFT*XBOX*MEDIA" at its start and again at 0x7EC, and the root
|
||||
directory table's start sector (u32 at 0x14) and size in bytes (u32 at
|
||||
0x18).
|
||||
- A directory table is a binary tree of entries whose root is the entry at
|
||||
offset 0. An entry is a left and a right subtree link (u16 each, in 4-byte
|
||||
units from the table's start, 0 for none), the start sector and the size
|
||||
of the file or of the subdirectory's table (u32 each), the attributes (u8,
|
||||
0x10 for a directory), the name's length (u8) and the name. Unused bytes
|
||||
are 0xFF, so a left link of 0xFFFF is padding, not an entry.
|
||||
- A file is one contiguous run of bytes from the start of its sector.
|
||||
|
||||
The image is the player's file and nothing read from it is trusted: only
|
||||
the root's table and the maps folder's are read, each whole and at most
|
||||
4 MB; a table's walk visits each offset once, goes at most 64 links deep
|
||||
and looks at most 4096 entries; and a file is written only under a plain
|
||||
name that no file before it had (ignoring case).
|
||||
|
||||
A European disc in two languages (Spanish and Italian) has no maps folder
|
||||
but one for each, maps_es and maps_it: the one for the language asked for
|
||||
(MJOLNIR_LANGUAGE, else the system's) is taken, else English's, else the
|
||||
first, and its language written to maps/language.txt, which the game takes
|
||||
as the Xbox's language while game.language is unset.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
SECTOR_SIZE = 2048
|
||||
DESCRIPTOR_OFFSET = 0x10000
|
||||
MAGIC = b"MICROSOFT*XBOX*MEDIA"
|
||||
SECOND_MAGIC_OFFSET = 0x7EC
|
||||
ENTRY_FIXED_SIZE = 14
|
||||
ATTRIBUTE_DIRECTORY = 0x10
|
||||
PADDING_LINK = 0xFFFF
|
||||
# where the game partition may start: an image of the partition alone, a
|
||||
# whole XGD2, XGD3 and XGD1 (the original Xbox's) disc
|
||||
PARTITION_STARTS = (0x00000000, 0x0FD90000, 0x02080000, 0x18300000)
|
||||
# directory tables are a few sectors; anything much larger is not one
|
||||
MAXIMUM_TABLE_SIZE = 4 << 20
|
||||
MAXIMUM_DEPTH = 64
|
||||
MAXIMUM_VISITS = 4096
|
||||
MAXIMUM_ENTRIES = 256
|
||||
COPY_CHUNK = 1 << 20
|
||||
PARTIAL_DIRECTORY = "maps.partial"
|
||||
|
||||
|
||||
class ExtractError(Exception):
|
||||
"""a reason the player can act on"""
|
||||
|
||||
|
||||
def read_exactly(image, position, size):
|
||||
"""size bytes of the image from position; None if it ends first"""
|
||||
image.seek(position)
|
||||
data = image.read(size)
|
||||
return data if len(data) == size else None
|
||||
|
||||
|
||||
def find_partition(image, image_size):
|
||||
"""the game partition's start, and the root table's sector and size"""
|
||||
for start in PARTITION_STARTS:
|
||||
if start + DESCRIPTOR_OFFSET + SECTOR_SIZE > image_size:
|
||||
continue
|
||||
descriptor = read_exactly(image, start + DESCRIPTOR_OFFSET, SECTOR_SIZE)
|
||||
if (descriptor is None or descriptor[:len(MAGIC)] != MAGIC or
|
||||
descriptor[SECOND_MAGIC_OFFSET:SECOND_MAGIC_OFFSET + len(MAGIC)] != MAGIC):
|
||||
continue
|
||||
sector, size = struct.unpack_from("<II", descriptor, 0x14)
|
||||
return start, sector, size
|
||||
raise ExtractError("This is not an Xbox disc image.")
|
||||
|
||||
|
||||
def read_table(image, partition, sector, size, damaged):
|
||||
if size == 0 or size > MAXIMUM_TABLE_SIZE:
|
||||
raise ExtractError(damaged)
|
||||
table = read_exactly(image, partition + sector * SECTOR_SIZE, size)
|
||||
if table is None:
|
||||
raise ExtractError("Could not read the disc image (is it complete?).")
|
||||
return table
|
||||
|
||||
|
||||
def is_plain_name(name):
|
||||
"""no path, no . or .., and only printable ASCII that every file system
|
||||
takes as a plain name"""
|
||||
if not name or name in (".", ".."):
|
||||
return False
|
||||
return all(0x20 <= ord(character) <= 0x7E and character not in '/\\:*?"<>|' for character in name)
|
||||
|
||||
|
||||
def walk_table(table, directories):
|
||||
"""the folders (or the files) of a table with plain names, as (name,
|
||||
sector, size), in the tree's order: each entry's left subtree, the entry,
|
||||
its right subtree; a link to padding, past the table or back to an entry
|
||||
already visited ends that branch"""
|
||||
entries, visited = [], set()
|
||||
visits = [0]
|
||||
|
||||
def walk(offset, depth):
|
||||
if depth > MAXIMUM_DEPTH or visits[0] >= MAXIMUM_VISITS:
|
||||
return
|
||||
if offset >= len(table) or len(table) - offset < ENTRY_FIXED_SIZE:
|
||||
return
|
||||
left, right = struct.unpack_from("<HH", table, offset)
|
||||
if left == PADDING_LINK or offset // 4 in visited:
|
||||
return
|
||||
visited.add(offset // 4)
|
||||
visits[0] += 1
|
||||
if left:
|
||||
walk(left * 4, depth + 1)
|
||||
name_length = table[offset + 13]
|
||||
if name_length and name_length <= len(table) - offset - ENTRY_FIXED_SIZE:
|
||||
is_directory = bool(table[offset + 12] & ATTRIBUTE_DIRECTORY)
|
||||
name = table[offset + ENTRY_FIXED_SIZE:offset + ENTRY_FIXED_SIZE + name_length].decode("latin-1")
|
||||
# (the destination's file system may ignore case)
|
||||
if (is_directory == directories and len(entries) < MAXIMUM_ENTRIES and is_plain_name(name) and
|
||||
all(entry[0].lower() != name.lower() for entry in entries)):
|
||||
sector, size = struct.unpack_from("<II", table, offset + 4)
|
||||
entries.append((name, sector, size))
|
||||
if right:
|
||||
walk(right * 4, depth + 1)
|
||||
|
||||
walk(0, 0)
|
||||
return entries
|
||||
|
||||
|
||||
def maps_language(name):
|
||||
"""a language's maps folder's language ("maps_it": "it"), else None"""
|
||||
if len(name) == 7 and name[:5].lower() == "maps_" and name[5:].isalpha():
|
||||
return name[5:].lower()
|
||||
return None
|
||||
|
||||
|
||||
def choose_maps(folders):
|
||||
"""the maps folder, and its language (None for a plain "maps")"""
|
||||
for folder in folders:
|
||||
if folder[0].lower() == "maps":
|
||||
return folder, None
|
||||
languages = [(maps_language(folder[0]), folder) for folder in folders if maps_language(folder[0])]
|
||||
wanted = (os.environ.get("MJOLNIR_LANGUAGE") or os.environ.get("LC_ALL") or
|
||||
os.environ.get("LC_MESSAGES") or os.environ.get("LANG") or "")[:2].lower()
|
||||
for preferred in (wanted, "en"):
|
||||
for language, folder in languages:
|
||||
if language == preferred:
|
||||
return folder, language
|
||||
if languages:
|
||||
return languages[0][1], languages[0][0]
|
||||
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
|
||||
|
||||
|
||||
def extract(image_path, destination):
|
||||
with open(image_path, "rb") as image:
|
||||
image_size = os.fstat(image.fileno()).st_size
|
||||
partition, root_sector, root_size = find_partition(image, image_size)
|
||||
|
||||
# the root's maps folder
|
||||
root = read_table(image, partition, root_sector, root_size, "The disc image's file system is damaged.")
|
||||
(_, maps_sector, maps_size), language = choose_maps(walk_table(root, True))
|
||||
|
||||
# its files
|
||||
table = read_table(image, partition, maps_sector, maps_size, "The disc image's maps folder is damaged.")
|
||||
files = walk_table(table, False)
|
||||
user_interface = next((file for file in files if file[0].lower() == "ui.map"), None)
|
||||
if user_interface is None:
|
||||
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
|
||||
# (an image that ends before a file does is refused before anything
|
||||
# is written; an empty file's sector need not be anywhere)
|
||||
for name, sector, size in files:
|
||||
if size and partition + sector * SECTOR_SIZE + size > image_size:
|
||||
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
|
||||
total = sum(size for _, _, size in files) or 1
|
||||
|
||||
# the copy, to maps.partial (emptied of what an earlier attempt left)
|
||||
partial = os.path.join(destination, PARTIAL_DIRECTORY)
|
||||
try:
|
||||
os.makedirs(partial, exist_ok=True)
|
||||
for leftover in os.listdir(partial):
|
||||
os.remove(os.path.join(partial, leftover))
|
||||
except OSError:
|
||||
raise ExtractError("Could not create %s." % partial)
|
||||
done, shown = 0, -1
|
||||
for name, sector, size in files:
|
||||
path = os.path.join(partial, name)
|
||||
position, remaining = partition + sector * SECTOR_SIZE, size
|
||||
print("# Copying maps/%s" % name, flush=True)
|
||||
try:
|
||||
with open(path, "wb") as out:
|
||||
while remaining > 0:
|
||||
count = min(remaining, COPY_CHUNK)
|
||||
data = read_exactly(image, position, count)
|
||||
if data is None:
|
||||
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
|
||||
out.write(data)
|
||||
position += 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 OSError:
|
||||
raise ExtractError("Could not write %s (is the storage full?)." % path)
|
||||
|
||||
# then into maps (which may be there, empty), the language before and
|
||||
# ui.map (the sign that the data is there) last
|
||||
finished = os.path.join(destination, "maps")
|
||||
try:
|
||||
os.makedirs(finished, exist_ok=True)
|
||||
language_file = os.path.join(finished, "language.txt")
|
||||
if os.path.exists(language_file):
|
||||
os.remove(language_file)
|
||||
if language:
|
||||
with open(language_file, "w") as out:
|
||||
out.write(language + "\n")
|
||||
order = [file for file in files if file is not user_interface] + [user_interface]
|
||||
for name, _, _ in order:
|
||||
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())
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
FDM_INJECTION_STUB.C
|
||||
|
||||
An OpenXR API layer that does nothing, named as the Steam Frame's
|
||||
XR_APILAYER_VALVE_fdm_injection names its library
|
||||
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
|
||||
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
|
||||
Android app (in Lepton) cannot load the system's library from its own
|
||||
namespace, and the Khronos loader then fails xrCreateInstance altogether;
|
||||
nor does the Android loader read the variable that would turn the layer
|
||||
off. With this one in the app's libraries the loader finds a layer of that
|
||||
name, which hands every call on to the next. (The real layer adds fragment
|
||||
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
|
||||
manifest names it and it is never loaded.
|
||||
*/
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function)
|
||||
{
|
||||
if (!next_get_instance_proc_addr)
|
||||
return XR_ERROR_HANDLE_INVALID;
|
||||
return next_get_instance_proc_addr(instance, name, function);
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
|
||||
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
|
||||
{
|
||||
XrApiLayerCreateInfo next_layer_info;
|
||||
|
||||
if (!layer_info || !layer_info->nextInfo)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
|
||||
next_layer_info = *layer_info;
|
||||
next_layer_info.nextInfo = layer_info->nextInfo->next;
|
||||
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default")))
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
|
||||
const char *layer_name, XrNegotiateApiLayerRequest *request)
|
||||
{
|
||||
(void)layer_name;
|
||||
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
|
||||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
|
||||
{
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
|
||||
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
|
||||
XR_CURRENT_API_VERSION;
|
||||
request->getInstanceProcAddr = layer_get_instance_proc_addr;
|
||||
request->createApiLayerInstance = layer_create_api_layer_instance;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ keep their place and size at any field of view.
|
||||
#include "rasterizer/xbox/rasterizer_xbox.h"
|
||||
|
||||
#include "object_mesh.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
|
||||
#include <string.h>
|
||||
@@ -74,6 +75,13 @@ boolean first_person_legs_wanted(
|
||||
{
|
||||
struct object_datum *object = object_try_and_get(object_index);
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* port: not in VR, where the eye is the headset's and the whole body is
|
||||
vr_body.c's (vr.body "full"); set back behind a flat view's eye, these
|
||||
legs would stand behind the player there */
|
||||
if (halo_vr_active())
|
||||
return FALSE;
|
||||
#endif
|
||||
return first_person_legs_enabled() && object && object->object.type == _object_type_biped &&
|
||||
object->object.parent_object_index == NONE &&
|
||||
!TEST_FLAG(object->object.damage_flags, _object_dead_bit) &&
|
||||
|
||||
@@ -2860,6 +2860,14 @@ int p2p_peer_address(unsigned char const *identifier, unsigned long *address);
|
||||
int platform_clipboard_get(char *text, int size);
|
||||
void platform_clipboard_set(char const *text);
|
||||
void platform_text_field(int typing, int password);
|
||||
#ifdef HALO_ANDROID
|
||||
/* (VR: the Xbox's on-screen keyboard, virtual_keyboard.c) */
|
||||
int halo_vr_active(void);
|
||||
boolean virtual_keyboard_launch(wchar_t *text_buffer, word buffer_size, short caption_index);
|
||||
boolean virtual_keyboard_active(void);
|
||||
boolean virtual_keyboard_last_exit_saved_text(void);
|
||||
void virtual_keyboard_set_caption(wchar_t const *caption);
|
||||
#endif
|
||||
int config_boolean(char const *name);
|
||||
void ui_widget_port_post_button(short controller_index, short button_index);
|
||||
|
||||
@@ -2922,6 +2930,30 @@ static struct
|
||||
of: its screen has gone) */
|
||||
static unsigned long text_field_shown_time;
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: no keyboard to type on; the Xbox's on-screen one types the field,
|
||||
which is done when it closes (its ENTER) or as it was (its BACK) */
|
||||
static struct
|
||||
{
|
||||
boolean open;
|
||||
wchar_t text[TEXT_FIELD_LENGTH];
|
||||
} text_field_keyboard;
|
||||
|
||||
static void text_field_keyboard_launch(void)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < TEXT_FIELD_LENGTH - 1 && text_field.text[index]; index++)
|
||||
text_field_keyboard.text[index] = (wchar_t)(unsigned char)text_field.text[index];
|
||||
text_field_keyboard.text[index] = 0;
|
||||
virtual_keyboard_set_caption(L"Type, then ENTER");
|
||||
text_field_keyboard.open = virtual_keyboard_launch(text_field_keyboard.text,
|
||||
(word)(MIN(text_field.maximum + 1, TEXT_FIELD_LENGTH) * sizeof(wchar_t)), 8);
|
||||
if (!text_field_keyboard.open)
|
||||
virtual_keyboard_set_caption(NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
static boolean text_field_editing(struct widget_instance *row)
|
||||
{
|
||||
return text_field.row && (!row || text_field.row == row);
|
||||
@@ -2942,6 +2974,10 @@ static void text_field_open(struct widget_instance *row, char const *text, short
|
||||
while (input_get_key(&key))
|
||||
;
|
||||
platform_text_field(TRUE, masked);
|
||||
#ifdef HALO_ANDROID
|
||||
if (halo_vr_active())
|
||||
text_field_keyboard_launch();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void text_field_begin(struct widget_instance *row, char const *text, short maximum,
|
||||
@@ -2990,6 +3026,30 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
|
||||
wchar_t shown[TEXT_FIELD_LENGTH + 2];
|
||||
short index;
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* (VR: the on-screen keyboard closed, the field done with what it typed) */
|
||||
if (editing && text_field_keyboard.open && !virtual_keyboard_active())
|
||||
{
|
||||
boolean typed = virtual_keyboard_last_exit_saved_text();
|
||||
|
||||
text_field_keyboard.open = FALSE;
|
||||
virtual_keyboard_set_caption(NULL);
|
||||
if (typed)
|
||||
{
|
||||
short length;
|
||||
|
||||
for (length = 0; length < text_field.maximum && text_field_keyboard.text[length]; length++)
|
||||
{
|
||||
wchar_t character = text_field_keyboard.text[length];
|
||||
|
||||
text_field.text[length] = character >= ' ' && character <= '~' ? (char)character : '?';
|
||||
}
|
||||
text_field.text[length] = 0;
|
||||
}
|
||||
text_field_end(typed);
|
||||
editing = FALSE;
|
||||
}
|
||||
#endif
|
||||
if (editing)
|
||||
{
|
||||
struct key_stroke key;
|
||||
@@ -7411,7 +7471,12 @@ void pc_menu_game_data_function_invoke(
|
||||
|
||||
if (!name)
|
||||
return;
|
||||
/* (a text field whose screen has gone: let go of) */
|
||||
/* (a text field whose screen has gone: let go of; not while VR's
|
||||
on-screen keyboard types it) */
|
||||
#ifdef HALO_ANDROID
|
||||
if (text_field_keyboard.open)
|
||||
text_field_shown_time = system_milliseconds();
|
||||
#endif
|
||||
if (text_field.row && system_milliseconds() - text_field_shown_time > 500)
|
||||
text_field_end(FALSE);
|
||||
if (!strcmp(name, "solo map list update"))
|
||||
|
||||
@@ -42,6 +42,7 @@ menu gets FRIENDLY FIRE too.
|
||||
#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h"
|
||||
|
||||
#include "halo_menus.h"
|
||||
#include "halo_vr.h"
|
||||
#include "custom_edition_cache.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
@@ -2198,6 +2199,9 @@ void menu_tags_loaded(
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* (in VR, the Xbox's unless vr.menus asks for the PC version's) */
|
||||
if (halo_vr_active() && strcmp(config_string("vr.menus"), "pc"))
|
||||
return;
|
||||
menus = halo_menus_load();
|
||||
if (!menus)
|
||||
return;
|
||||
|
||||
@@ -3825,6 +3825,8 @@ void network_distributed_new_game(
|
||||
update_queues_distributed_reset();
|
||||
network_objects_new_game();
|
||||
network_damage_new_game();
|
||||
network_vr_new_game();
|
||||
bots_new_game();
|
||||
network_actors_new_game();
|
||||
network_coop_new_game();
|
||||
network_aim_assist_reset();
|
||||
@@ -3883,6 +3885,8 @@ void network_distributed_tick(
|
||||
profile_scope(distributed_tick_send_structure_bsp_section, distributed_send_structure_bsp();)
|
||||
}
|
||||
profile_scope(distributed_tick_send_players_section, distributed_host_send_players();)
|
||||
network_vr_tick();
|
||||
bots_tick();
|
||||
profile_scope(distributed_tick_actors_host_tick_section, network_actors_host_tick();)
|
||||
profile_scope(distributed_tick_coop_host_tick_section, network_coop_host_tick();)
|
||||
profile_scope(distributed_tick_send_pickups_section, distributed_send_pickups();)
|
||||
@@ -3896,6 +3900,7 @@ void network_distributed_tick(
|
||||
if (game_time_get() % PING_REFRESH_TICKS == 0 && latency_meter_enabled())
|
||||
distributed_refresh_pings();
|
||||
profile_scope(distributed_tick_send_inputs_section, distributed_client_send_inputs();)
|
||||
network_vr_tick();
|
||||
profile_scope(distributed_tick_send_predictions_section, distributed_client_send_predictions();)
|
||||
profile_scope(distributed_tick_objects_client_tick_section, network_objects_client_tick();)
|
||||
profile_scope(distributed_tick_damage_client_tick_section, network_damage_client_tick();)
|
||||
@@ -3985,6 +3990,7 @@ static boolean distributed_message_stale(
|
||||
case _distributed_message_player_inputs:
|
||||
case _distributed_message_relayed_actions:
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_vr_poses:
|
||||
case _distributed_message_pings:
|
||||
case _distributed_message_actor_states:
|
||||
case _distributed_message_structure_bsp:
|
||||
@@ -4748,6 +4754,7 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_notice:
|
||||
case _distributed_message_client_ready: entry_size = 0; break;
|
||||
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
|
||||
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
|
||||
case _distributed_message_client_build: entry_size = sizeof(struct distributed_client_build); break;
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_hit_reports:
|
||||
@@ -4789,7 +4796,19 @@ void network_distributed_handle_message(
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* (each kind from the host, or from a client) */
|
||||
/* (each kind from the host, or from a client; a VR player's hands
|
||||
both ways) */
|
||||
if (header.type == _distributed_message_vr_poses)
|
||||
{
|
||||
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
|
||||
game_connection() != _game_connection_network_server)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!distributed_message_stale(machine_index, &header))
|
||||
network_vr_handle_poses(machine_index, entries, header.count);
|
||||
return;
|
||||
}
|
||||
switch (header.type)
|
||||
{
|
||||
case _distributed_message_player_prediction:
|
||||
|
||||
@@ -150,6 +150,13 @@ enum
|
||||
while they do (network_damage.c): its line of sight check's */
|
||||
_distributed_message_fire_origins = 93,
|
||||
|
||||
/* The VR port's own messages, numbered far past the game's (240 on),
|
||||
so that messages upstream adds never take their numbers: a build
|
||||
without them drops them, as it drops any type past its own. */
|
||||
/* a VR player's hands, every tick: a client's own to the host, the
|
||||
host's of the others to each client (unreliable; network_vr.c) */
|
||||
_distributed_message_vr_poses = 240,
|
||||
|
||||
NUMBER_OF_DISTRIBUTED_MESSAGES
|
||||
};
|
||||
|
||||
@@ -487,4 +494,14 @@ the camera; NULL (the default) for a build whose shots always do */
|
||||
typedef boolean (*network_damage_fire_origin_source)(short local_player_index, real_vector3d *offset);
|
||||
void network_damage_set_fire_origin_source(network_damage_fire_origin_source source);
|
||||
|
||||
/* VR players' hands (network_vr.c) */
|
||||
void network_vr_new_game(void);
|
||||
void network_vr_tick(void);
|
||||
word network_vr_entry_size(void);
|
||||
void network_vr_handle_poses(long machine_index, void const *entries, short count);
|
||||
|
||||
/* bots (bots.c) */
|
||||
void bots_new_game(void);
|
||||
void bots_tick(void);
|
||||
|
||||
#endif // __NETWORK_DISTRIBUTED_H
|
||||
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
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 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 (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); a VR
|
||||
player's weapon hand also goes to the host as their fire origin, which its
|
||||
line of sight check looks from too.
|
||||
|
||||
A machine that does not know the message (a build without this, of the same
|
||||
network version) drops it, as it drops any type past its own
|
||||
(network_distributed_handle_message): VR and other builds still play
|
||||
together, the others seeing a VR player aim where the head looks.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "game/game.h"
|
||||
#include "game/players.h"
|
||||
#include "units/units.h"
|
||||
#include "network_distributed.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
_vr_pose_aim_bit,
|
||||
_vr_pose_weapon_offset_bit,
|
||||
_vr_pose_grenade_held_bit,
|
||||
_vr_pose_throw_bit,
|
||||
};
|
||||
|
||||
/* how long a player's hands drive them after the last came (ticks): longer
|
||||
and they are driven as before (the hands stopped coming: the player left
|
||||
VR, or the network). Where the weapon aims and fires from is held a
|
||||
second, over the gaps of a network that delivers in bursts (as the input
|
||||
the netcode relays is); a grenade held, only a third of one, so that it
|
||||
does not hang in the hand after the hand let go. The host relays only
|
||||
hands as fresh as that. */
|
||||
#define VR_POSE_FRESH_TICKS 10
|
||||
#define VR_AIM_HOLD_TICKS 30
|
||||
/* how long a throw is sent after it (ticks), and taken after it came */
|
||||
#define VR_THROW_TICKS 30
|
||||
/* offsets from the unit's camera: in 1/4096 of a world unit, no further
|
||||
than a hand reaches */
|
||||
#define VR_OFFSET_SCALE 4096.0f
|
||||
#define VR_OFFSET_MAXIMUM 1.0f
|
||||
/* how often what was sent and heard is logged (ticks) */
|
||||
#define VR_LOG_TICKS 150
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
struct distributed_vr_pose
|
||||
{
|
||||
byte player_index;
|
||||
byte flags;
|
||||
byte throw_sequence;
|
||||
byte throw_strength;
|
||||
struct distributed_vector aim;
|
||||
struct distributed_vector weapon_offset;
|
||||
struct distributed_vector throw_offset;
|
||||
struct distributed_vector throw_direction;
|
||||
};
|
||||
|
||||
typedef char distributed_vr_pose_size_assert[sizeof(struct distributed_vr_pose) == 28 ? 1 : -1];
|
||||
|
||||
/* a player's hands, as last heard of (this machine's own VR players on the
|
||||
host too, to send) */
|
||||
struct network_vr_player
|
||||
{
|
||||
boolean valid;
|
||||
long received_time;
|
||||
struct distributed_vr_pose pose;
|
||||
/* the throw this machine has taken for the player's grenade */
|
||||
boolean throw_taken;
|
||||
byte throw_taken_sequence;
|
||||
long throw_received_time;
|
||||
byte throw_received_sequence;
|
||||
};
|
||||
|
||||
/* a local VR player's last throw */
|
||||
struct network_vr_throw
|
||||
{
|
||||
boolean valid;
|
||||
long time;
|
||||
byte sequence;
|
||||
real_vector3d offset;
|
||||
real_vector3d direction;
|
||||
real strength;
|
||||
};
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
static struct network_vr_player network_vr_players[MAXIMUM_TRACKED_PLAYERS];
|
||||
static struct network_vr_throw network_vr_throws[MAXIMUM_LOCAL_PLAYERS];
|
||||
static byte network_vr_throw_sequences[MAXIMUM_LOCAL_PLAYERS];
|
||||
/* what was sent and heard since last logged */
|
||||
static struct
|
||||
{
|
||||
long sent;
|
||||
long heard;
|
||||
long aimed;
|
||||
} network_vr_counts;
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean network_vr_in_network_game(
|
||||
void)
|
||||
{
|
||||
short connection = game_connection();
|
||||
|
||||
return connection == _game_connection_network_server || connection == _game_connection_network_client;
|
||||
}
|
||||
|
||||
static void network_vr_offset_pack(
|
||||
real_vector3d const *offset,
|
||||
struct distributed_vector *result)
|
||||
{
|
||||
distributed_vector_pack(offset, VR_OFFSET_SCALE, result);
|
||||
}
|
||||
|
||||
/* (FALSE: not a hand's offset) */
|
||||
static boolean network_vr_offset_unpack(
|
||||
struct distributed_vector const *packed,
|
||||
real_vector3d *offset)
|
||||
{
|
||||
distributed_vector_unpack(packed, VR_OFFSET_SCALE, offset);
|
||||
return fabsf(offset->i) <= VR_OFFSET_MAXIMUM && fabsf(offset->j) <= VR_OFFSET_MAXIMUM &&
|
||||
fabsf(offset->k) <= VR_OFFSET_MAXIMUM;
|
||||
}
|
||||
|
||||
/* (FALSE: no direction) */
|
||||
static boolean network_vr_direction_unpack(
|
||||
struct distributed_vector const *packed,
|
||||
real_vector3d *direction)
|
||||
{
|
||||
distributed_unit_vector_unpack(packed, direction);
|
||||
return direction->i * direction->i + direction->j * direction->j + direction->k * direction->k > 0.5f;
|
||||
}
|
||||
|
||||
/* a player of another machine whose hands came lately, or NULL */
|
||||
static struct network_vr_player *network_vr_remote_player(
|
||||
long player_index,
|
||||
long fresh_ticks)
|
||||
{
|
||||
struct network_vr_player *vr_player;
|
||||
long absolute_index;
|
||||
|
||||
if (player_index == NONE || !network_vr_in_network_game() || distributed_player_is_local(player_index))
|
||||
return NULL;
|
||||
absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index);
|
||||
if (absolute_index < 0 || absolute_index >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return NULL;
|
||||
vr_player = &network_vr_players[absolute_index];
|
||||
if (!vr_player->valid || game_time_get() - vr_player->received_time > fresh_ticks ||
|
||||
game_time_get() < vr_player->received_time)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
return vr_player;
|
||||
}
|
||||
|
||||
/* what is said of a player's hands, kept (checked: a client's of its own) */
|
||||
static void network_vr_take_pose(
|
||||
struct distributed_vr_pose const *pose,
|
||||
boolean heard)
|
||||
{
|
||||
struct network_vr_player *vr_player;
|
||||
real_vector3d vector;
|
||||
|
||||
if (pose->player_index >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return;
|
||||
vr_player = &network_vr_players[pose->player_index];
|
||||
vr_player->pose = *pose;
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_aim_bit) && !network_vr_direction_unpack(&pose->aim, &vector))
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_aim_bit, FALSE);
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_weapon_offset_bit) && !network_vr_offset_unpack(&pose->weapon_offset, &vector))
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit, FALSE);
|
||||
if (TEST_FLAG(pose->flags, _vr_pose_throw_bit) &&
|
||||
(!network_vr_offset_unpack(&pose->throw_offset, &vector) ||
|
||||
!network_vr_direction_unpack(&pose->throw_direction, &vector)))
|
||||
{
|
||||
SET_FLAG(vr_player->pose.flags, _vr_pose_throw_bit, FALSE);
|
||||
}
|
||||
/* (a throw new to this machine: when it came) */
|
||||
if (TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) &&
|
||||
(!vr_player->valid || vr_player->throw_received_sequence != pose->throw_sequence ||
|
||||
vr_player->throw_received_time == NONE))
|
||||
{
|
||||
vr_player->throw_received_sequence = pose->throw_sequence;
|
||||
vr_player->throw_received_time = game_time_get();
|
||||
}
|
||||
vr_player->valid = TRUE;
|
||||
vr_player->received_time = game_time_get();
|
||||
if (heard)
|
||||
network_vr_counts.heard++;
|
||||
}
|
||||
|
||||
/* this machine's VR player's hands, to send; FALSE when it has none */
|
||||
static boolean network_vr_local_pose(
|
||||
short local_player_index,
|
||||
struct distributed_vr_pose *pose)
|
||||
{
|
||||
long player_index = local_player_get_player_index(local_player_index);
|
||||
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
struct unit_datum *unit = player && player->unit_index != NONE ? unit_try_and_get(player->unit_index) : NULL;
|
||||
struct network_vr_throw *last_throw = &network_vr_throws[local_player_index];
|
||||
float vector[3];
|
||||
|
||||
if (!unit || !halo_vr_active())
|
||||
return FALSE;
|
||||
csmemset(pose, 0, sizeof(*pose));
|
||||
pose->player_index = distributed_player_to_byte(player_index);
|
||||
/* (on foot: seated, the seat aims, and the input says where) */
|
||||
if (unit->object.parent_object_index == NONE)
|
||||
{
|
||||
if (halo_vr_aim_vector(local_player_index, vector))
|
||||
{
|
||||
real_vector3d aim;
|
||||
|
||||
aim.i = vector[0];
|
||||
aim.j = vector[1];
|
||||
aim.k = vector[2];
|
||||
distributed_vector_pack(&aim, DISTRIBUTED_UNIT_SCALE, &pose->aim);
|
||||
SET_FLAG(pose->flags, _vr_pose_aim_bit, TRUE);
|
||||
}
|
||||
if (halo_vr_weapon_origin_offset(local_player_index, vector))
|
||||
{
|
||||
real_vector3d offset;
|
||||
|
||||
offset.i = vector[0];
|
||||
offset.j = vector[1];
|
||||
offset.k = vector[2];
|
||||
if (fabsf(offset.i) <= VR_OFFSET_MAXIMUM && fabsf(offset.j) <= VR_OFFSET_MAXIMUM &&
|
||||
fabsf(offset.k) <= VR_OFFSET_MAXIMUM)
|
||||
{
|
||||
network_vr_offset_pack(&offset, &pose->weapon_offset);
|
||||
SET_FLAG(pose->flags, _vr_pose_weapon_offset_bit, TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (halo_vr_grenade_held(local_player_index))
|
||||
SET_FLAG(pose->flags, _vr_pose_grenade_held_bit, TRUE);
|
||||
if (last_throw->valid && game_time_get() - last_throw->time <= VR_THROW_TICKS &&
|
||||
game_time_get() >= last_throw->time)
|
||||
{
|
||||
network_vr_offset_pack(&last_throw->offset, &pose->throw_offset);
|
||||
distributed_vector_pack(&last_throw->direction, DISTRIBUTED_UNIT_SCALE, &pose->throw_direction);
|
||||
pose->throw_sequence = last_throw->sequence;
|
||||
pose->throw_strength = (byte)(PIN(last_throw->strength, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
SET_FLAG(pose->flags, _vr_pose_throw_bit, TRUE);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* where a local VR player's shots leave from, from the unit's camera: the
|
||||
weapon hand, on foot (network_damage.c's fire origins, which the host's
|
||||
line of sight check also looks from) */
|
||||
static boolean network_vr_fire_origin(
|
||||
short local_player_index,
|
||||
real_vector3d *offset)
|
||||
{
|
||||
long player_index = local_player_get_player_index(local_player_index);
|
||||
struct player_datum *player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
struct unit_datum *unit = player && player->unit_index != NONE ? unit_try_and_get(player->unit_index) : NULL;
|
||||
float vector[3];
|
||||
|
||||
if (!unit || !halo_vr_active() || unit->object.parent_object_index != NONE ||
|
||||
!halo_vr_weapon_origin_offset(local_player_index, vector))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
offset->i = vector[0];
|
||||
offset->j = vector[1];
|
||||
offset->k = vector[2];
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void network_vr_new_game(
|
||||
void)
|
||||
{
|
||||
network_damage_set_fire_origin_source(network_vr_fire_origin);
|
||||
short index;
|
||||
|
||||
csmemset(network_vr_players, 0, sizeof(network_vr_players));
|
||||
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
|
||||
network_vr_players[index].throw_received_time = NONE;
|
||||
csmemset(network_vr_throws, 0, sizeof(network_vr_throws));
|
||||
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
|
||||
}
|
||||
|
||||
word network_vr_entry_size(
|
||||
void)
|
||||
{
|
||||
return sizeof(struct distributed_vr_pose);
|
||||
}
|
||||
|
||||
/* after each tick (network_distributed_tick): a client its own VR players'
|
||||
hands to the host; the host its own and its clients' to each client */
|
||||
void network_vr_tick(
|
||||
void)
|
||||
{
|
||||
short connection = game_connection();
|
||||
short local_player_index;
|
||||
struct
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
struct distributed_vr_pose poses[MAXIMUM_TRACKED_PLAYERS];
|
||||
} message;
|
||||
short count = 0;
|
||||
|
||||
if (game_time_get() % VR_LOG_TICKS == 0 &&
|
||||
(network_vr_counts.sent || network_vr_counts.heard || network_vr_counts.aimed))
|
||||
{
|
||||
platform_log("VR network: hands sent %ld, heard %ld, other players aimed by them %ld (in %d ticks)",
|
||||
network_vr_counts.sent, network_vr_counts.heard, network_vr_counts.aimed, VR_LOG_TICKS);
|
||||
csmemset(&network_vr_counts, 0, sizeof(network_vr_counts));
|
||||
}
|
||||
if (connection == _game_connection_network_client)
|
||||
{
|
||||
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
|
||||
{
|
||||
if (network_vr_local_pose(local_player_index, &message.poses[count]))
|
||||
count++;
|
||||
}
|
||||
network_vr_counts.sent += count;
|
||||
if (count)
|
||||
{
|
||||
distributed_send(&message, _distributed_message_vr_poses, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)), _distributed_to_host);
|
||||
}
|
||||
}
|
||||
else if (connection == _game_connection_network_server)
|
||||
{
|
||||
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
short machine_count;
|
||||
short machine_number;
|
||||
short player_index;
|
||||
|
||||
for (local_player_index = 0; local_player_index < MAXIMUM_LOCAL_PLAYERS; local_player_index++)
|
||||
{
|
||||
struct distributed_vr_pose pose;
|
||||
|
||||
if (network_vr_local_pose(local_player_index, &pose))
|
||||
network_vr_take_pose(&pose, FALSE);
|
||||
}
|
||||
machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
|
||||
for (machine_number = 0; machine_number < machine_count; machine_number++)
|
||||
{
|
||||
long machine_index = machine_indices[machine_number];
|
||||
|
||||
count = 0;
|
||||
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS && count < 255; player_index++)
|
||||
{
|
||||
struct network_vr_player const *vr_player = &network_vr_players[player_index];
|
||||
|
||||
if (vr_player->valid && game_time_get() - vr_player->received_time <= VR_POSE_FRESH_TICKS &&
|
||||
game_time_get() >= vr_player->received_time &&
|
||||
distributed_player(player_index) &&
|
||||
!distributed_machine_has_player(machine_index, player_index))
|
||||
{
|
||||
message.poses[count++] = vr_player->pose;
|
||||
}
|
||||
}
|
||||
network_vr_counts.sent += count;
|
||||
if (count)
|
||||
{
|
||||
distributed_send_to_machine(machine_index, &message, _distributed_message_vr_poses, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_vr_pose)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* the hands a machine sent: the host's of a client's players, a client's
|
||||
of the others */
|
||||
void network_vr_handle_poses(
|
||||
long machine_index,
|
||||
void const *entries,
|
||||
short count)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
struct distributed_vr_pose pose;
|
||||
|
||||
csmemcpy(&pose, (byte const *)entries + index * sizeof(pose), sizeof(pose));
|
||||
if (pose.player_index >= MAXIMUM_TRACKED_PLAYERS || !distributed_player(pose.player_index))
|
||||
continue;
|
||||
if (machine_index != NONE)
|
||||
{
|
||||
/* (the host: only of that client's own players) */
|
||||
if (!distributed_machine_has_player(machine_index, pose.player_index))
|
||||
continue;
|
||||
}
|
||||
else if (distributed_player_is_local(distributed_player_from_byte(pose.player_index)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
network_vr_take_pose(&pose, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- the game's side (halo_vr.h) */
|
||||
|
||||
int halo_vr_network_aim(long player_index, float vector[3])
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
|
||||
real_vector3d aim;
|
||||
|
||||
if (!vr_player || !TEST_FLAG(vr_player->pose.flags, _vr_pose_aim_bit) ||
|
||||
!network_vr_direction_unpack(&vr_player->pose.aim, &aim))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
vector[0] = aim.i;
|
||||
vector[1] = aim.j;
|
||||
vector[2] = aim.k;
|
||||
network_vr_counts.aimed++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3])
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_AIM_HOLD_TICKS);
|
||||
real_vector3d offset;
|
||||
real_point3d camera;
|
||||
|
||||
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_weapon_offset_bit) ||
|
||||
!network_vr_offset_unpack(&vr_player->pose.weapon_offset, &offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
origin[0] = camera.x + offset.i;
|
||||
origin[1] = camera.y + offset.j;
|
||||
origin[2] = camera.z + offset.k;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int halo_vr_network_grenade_held(long player_index)
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_POSE_FRESH_TICKS);
|
||||
|
||||
return vr_player && TEST_FLAG(vr_player->pose.flags, _vr_pose_grenade_held_bit);
|
||||
}
|
||||
|
||||
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
|
||||
float *strength)
|
||||
{
|
||||
struct network_vr_player *vr_player = network_vr_remote_player(player_index, VR_THROW_TICKS);
|
||||
real_vector3d offset;
|
||||
real_vector3d way;
|
||||
real_point3d camera;
|
||||
|
||||
if (!vr_player || unit_index == NONE || !TEST_FLAG(vr_player->pose.flags, _vr_pose_throw_bit) ||
|
||||
vr_player->throw_received_time == NONE || game_time_get() - vr_player->throw_received_time > VR_THROW_TICKS ||
|
||||
(vr_player->throw_taken && vr_player->throw_taken_sequence == vr_player->pose.throw_sequence) ||
|
||||
!network_vr_offset_unpack(&vr_player->pose.throw_offset, &offset) ||
|
||||
!network_vr_direction_unpack(&vr_player->pose.throw_direction, &way))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
vr_player->throw_taken = TRUE;
|
||||
vr_player->throw_taken_sequence = vr_player->pose.throw_sequence;
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
origin[0] = camera.x + offset.i;
|
||||
origin[1] = camera.y + offset.j;
|
||||
origin[2] = camera.z + offset.k;
|
||||
direction[0] = way.i;
|
||||
direction[1] = way.j;
|
||||
direction[2] = way.k;
|
||||
*strength = (float)vr_player->pose.throw_strength / 255.0f;
|
||||
platform_log("VR network: player %ld's grenade thrown as their hand threw it (%.2f)",
|
||||
DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index), *strength);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
|
||||
const float direction[3], float strength)
|
||||
{
|
||||
struct network_vr_throw *last_throw;
|
||||
real_point3d camera;
|
||||
|
||||
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS || unit_index == NONE ||
|
||||
!network_vr_in_network_game())
|
||||
{
|
||||
return;
|
||||
}
|
||||
last_throw = &network_vr_throws[local_player_index];
|
||||
unit_get_camera_position(unit_index, &camera);
|
||||
last_throw->offset.i = origin[0] - camera.x;
|
||||
last_throw->offset.j = origin[1] - camera.y;
|
||||
last_throw->offset.k = origin[2] - camera.z;
|
||||
/* (a hand further off than it reaches: none sent) */
|
||||
if (fabsf(last_throw->offset.i) > VR_OFFSET_MAXIMUM || fabsf(last_throw->offset.j) > VR_OFFSET_MAXIMUM ||
|
||||
fabsf(last_throw->offset.k) > VR_OFFSET_MAXIMUM)
|
||||
{
|
||||
last_throw->valid = FALSE;
|
||||
return;
|
||||
}
|
||||
last_throw->direction.i = direction[0];
|
||||
last_throw->direction.j = direction[1];
|
||||
last_throw->direction.k = direction[2];
|
||||
last_throw->strength = strength;
|
||||
last_throw->time = game_time_get();
|
||||
last_throw->sequence = ++network_vr_throw_sequences[local_player_index];
|
||||
last_throw->valid = TRUE;
|
||||
platform_log("VR network: grenade throw #%d sent (%.2f)", last_throw->sequence, strength);
|
||||
}
|
||||
@@ -45,6 +45,11 @@ int config_boolean(const char *name);
|
||||
const char *config_string(const char *name);
|
||||
unsigned long config_changes(void);
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* port/linux/game/vr_graphics.c */
|
||||
boolean vr_graphics_allows_shield_glow(void);
|
||||
#endif
|
||||
|
||||
/* object_lights.c (port) */
|
||||
long light_port_glow_definition_get(void);
|
||||
boolean light_port_glow_set(long light_index, long object_index, real_rgb_color const *color, real radius);
|
||||
@@ -117,6 +122,11 @@ static boolean shield_glow_enabled(
|
||||
enabled = config_boolean("display.shield_glow") != 0;
|
||||
shield_glow_scale = intensity && !strcmp(intensity, "light_show") ? 3.f : 1.f;
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
/* (VR's "auto" graphics leaves it out first: vr_graphics.c) */
|
||||
if (enabled && !vr_graphics_allows_shield_glow())
|
||||
return FALSE;
|
||||
#endif
|
||||
return enabled;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ that draw the first-person weapon (rasterizer and render sources).
|
||||
#include "render/render_cameras_internal.h"
|
||||
#include "units/unit_definitions.h"
|
||||
#include "units/units.h"
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
|
||||
#include <math.h>
|
||||
|
||||
@@ -38,6 +41,18 @@ double config_real(const char *name);
|
||||
int config_boolean(const char *name);
|
||||
unsigned long config_changes(void);
|
||||
|
||||
/* port: in VR the headset gives each eye's view (vr.c) and the weapon is
|
||||
held in the hand: display.fov, display.viewmodel_fov and
|
||||
display.viewmodel_visible do nothing */
|
||||
static boolean view_fov_vr(void)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
return halo_vr_active() != 0;
|
||||
#else
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---------- the world's field of view */
|
||||
|
||||
static real reticle_scales[MAXIMUM_LOCAL_PLAYERS];
|
||||
@@ -65,7 +80,7 @@ static real render_fov_adjust(short local_player_index, real native_vertical_fie
|
||||
requested_tangent = degrees >= 20.0 && degrees <= 150.0 ?
|
||||
(real)(tan(degrees * _pi / 360.0) * (9.0 / 16.0)) : 0.0f;
|
||||
}
|
||||
if (!requested_tangent || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
|
||||
if (!requested_tangent || view_fov_vr() || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
|
||||
cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
|
||||
director_get_perspective(local_player_index) != _director_perspective_first_person ||
|
||||
director_inhibited_facing(local_player_index))
|
||||
@@ -201,6 +216,8 @@ void viewmodel_projection_begin(void)
|
||||
if (projection_depth++ != 0)
|
||||
return;
|
||||
projection_applied = FALSE;
|
||||
if (view_fov_vr())
|
||||
return;
|
||||
/* (display.viewmodel_fov's angle; by default the view's own, from before
|
||||
display.fov widened it: arms and a gun right against the camera stretch
|
||||
at a wide angle) */
|
||||
@@ -260,7 +277,7 @@ boolean viewmodel_is_visible(void)
|
||||
initialized = TRUE;
|
||||
read_at = changes;
|
||||
}
|
||||
return visible;
|
||||
return visible || view_fov_vr();
|
||||
}
|
||||
|
||||
/* ---------- whether the energy shield's flare is drawn on the arms */
|
||||
|
||||
@@ -0,0 +1,875 @@
|
||||
/*
|
||||
VR_ARMS.C
|
||||
|
||||
The VR arms' bones (vr_arms.h): the first-person hands' arms bent from the
|
||||
shoulders to the hands (interface/first_person_weapons.c poses them).
|
||||
|
||||
The solver, the forearm's twist and their helpers are after
|
||||
moistman42069/HaloCE-Quest-VR (CC0), port/linux/game/vr_render.c, nearly
|
||||
as they are there: the skeleton is read through its animation graph's
|
||||
index (the nodes under each node found once a frame, not per node), and
|
||||
the time is the headset's frame's (halo_vr_pose_time).
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "math/real_math.h"
|
||||
#include "models/model_animation_definitions.h"
|
||||
#include "halo_vr.h"
|
||||
#include "vr_arms.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
#define VR_UNITS_PER_METRE (1.0f / 3.048f)
|
||||
|
||||
/* ---------- the skeleton */
|
||||
|
||||
boolean vr_skeleton_build(
|
||||
struct vr_skeleton *skeleton,
|
||||
long graph_index,
|
||||
short node_count)
|
||||
{
|
||||
short node_index;
|
||||
|
||||
skeleton->graph_index = NONE;
|
||||
if (graph_index == NONE || node_count <= 0 || node_count > MAXIMUM_NODES_PER_ANIMATION ||
|
||||
node_count != animation_graph_node_count(graph_index))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
short parent = animation_graph_node_parent(graph_index, node_index);
|
||||
|
||||
/* (parents before their children: else no parent) */
|
||||
skeleton->parents[node_index] = parent >= 0 && parent < node_index ? parent : NONE;
|
||||
skeleton->under[node_index] = 0;
|
||||
}
|
||||
/* each node under itself and all its ancestors */
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
short ancestor = node_index;
|
||||
|
||||
while (ancestor != NONE)
|
||||
{
|
||||
skeleton->under[ancestor] |= 1ULL << node_index;
|
||||
ancestor = skeleton->parents[ancestor];
|
||||
}
|
||||
}
|
||||
skeleton->graph_index = graph_index;
|
||||
skeleton->node_count = node_count;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
boolean vr_skeleton_under(
|
||||
struct vr_skeleton const *skeleton,
|
||||
short node_index,
|
||||
short root)
|
||||
{
|
||||
return root >= 0 && root < skeleton->node_count && node_index >= 0 && node_index < skeleton->node_count &&
|
||||
(skeleton->under[root] & (1ULL << node_index)) != 0;
|
||||
}
|
||||
|
||||
/* ---------- rotations (column vectors) */
|
||||
|
||||
/* v += a * scale */
|
||||
static void vr_vector_scale_add_local(
|
||||
real_vector3d *v,
|
||||
real_vector3d const *a,
|
||||
real scale)
|
||||
{
|
||||
v->i += a->i * scale;
|
||||
v->j += a->j * scale;
|
||||
v->k += a->k * scale;
|
||||
}
|
||||
|
||||
static real vr_length(
|
||||
real_vector3d const *v)
|
||||
{
|
||||
return (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k);
|
||||
}
|
||||
|
||||
static void vr_point_minus(
|
||||
real_point3d const *a,
|
||||
real_point3d const *b,
|
||||
real_vector3d *out)
|
||||
{
|
||||
out->i = a->x - b->x;
|
||||
out->j = a->y - b->y;
|
||||
out->k = a->z - b->z;
|
||||
}
|
||||
|
||||
static boolean vr_unit_vector(
|
||||
real_vector3d *v)
|
||||
{
|
||||
real length = vr_length(v);
|
||||
|
||||
if (!(length > 1e-5f && length < 1e8f))
|
||||
return FALSE;
|
||||
v->i /= length;
|
||||
v->j /= length;
|
||||
v->k /= length;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* the rotation taking direction a to direction b, the shortest way */
|
||||
void vr_rotation_between(
|
||||
real_vector3d const *a,
|
||||
real_vector3d const *b,
|
||||
real rotation[3][3])
|
||||
{
|
||||
real_vector3d u = *a, v = *b, axis;
|
||||
real c, s, t;
|
||||
|
||||
normalize3d(&u);
|
||||
normalize3d(&v);
|
||||
cross_product3d(&u, &v, &axis);
|
||||
s = vr_length(&axis);
|
||||
c = u.i * v.i + u.j * v.j + u.k * v.k;
|
||||
if (s < 1e-6f)
|
||||
{
|
||||
memset(rotation, 0, sizeof(real) * 9);
|
||||
if (c >= 0.0f)
|
||||
{
|
||||
rotation[0][0] = rotation[1][1] = rotation[2][2] = 1.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* half a turn about an axis across */
|
||||
real_vector3d basis;
|
||||
int row, column;
|
||||
|
||||
basis.i = 1.0f;
|
||||
basis.j = basis.k = 0.0f;
|
||||
if (fabs(u.i) > 0.8f)
|
||||
{
|
||||
basis.i = 0.0f;
|
||||
basis.j = 1.0f;
|
||||
}
|
||||
cross_product3d(&u, &basis, &axis);
|
||||
normalize3d(&axis);
|
||||
for (row = 0; row < 3; row++)
|
||||
{
|
||||
for (column = 0; column < 3; column++)
|
||||
rotation[row][column] = 2.0f * axis.n[row] * axis.n[column] - (row == column ? 1.0f : 0.0f);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
axis.i /= s;
|
||||
axis.j /= s;
|
||||
axis.k /= s;
|
||||
t = 1.0f - c;
|
||||
rotation[0][0] = c + axis.i * axis.i * t;
|
||||
rotation[0][1] = axis.i * axis.j * t - axis.k * s;
|
||||
rotation[0][2] = axis.i * axis.k * t + axis.j * s;
|
||||
rotation[1][0] = axis.j * axis.i * t + axis.k * s;
|
||||
rotation[1][1] = c + axis.j * axis.j * t;
|
||||
rotation[1][2] = axis.j * axis.k * t - axis.i * s;
|
||||
rotation[2][0] = axis.k * axis.i * t - axis.j * s;
|
||||
rotation[2][1] = axis.k * axis.j * t + axis.i * s;
|
||||
rotation[2][2] = c + axis.k * axis.k * t;
|
||||
}
|
||||
|
||||
/* a rotation of angle radians about the unit axis */
|
||||
static void vr_axis_rotation(
|
||||
real_vector3d const *axis,
|
||||
real angle,
|
||||
real rotation[3][3])
|
||||
{
|
||||
real c = (real)cos(angle), s = (real)sin(angle), t = 1.0f - c;
|
||||
real x = axis->i, y = axis->j, z = axis->k;
|
||||
|
||||
rotation[0][0] = c + x * x * t;
|
||||
rotation[0][1] = x * y * t - z * s;
|
||||
rotation[0][2] = x * z * t + y * s;
|
||||
rotation[1][0] = y * x * t + z * s;
|
||||
rotation[1][1] = c + y * y * t;
|
||||
rotation[1][2] = y * z * t - x * s;
|
||||
rotation[2][0] = z * x * t - y * s;
|
||||
rotation[2][1] = z * y * t + x * s;
|
||||
rotation[2][2] = c + z * z * t;
|
||||
}
|
||||
|
||||
static void vr_rotate_vector(
|
||||
real rotation[3][3],
|
||||
real_vector3d const *in,
|
||||
real_vector3d *out)
|
||||
{
|
||||
real_vector3d v = *in;
|
||||
|
||||
out->i = rotation[0][0] * v.i + rotation[0][1] * v.j + rotation[0][2] * v.k;
|
||||
out->j = rotation[1][0] * v.i + rotation[1][1] * v.j + rotation[1][2] * v.k;
|
||||
out->k = rotation[2][0] * v.i + rotation[2][1] * v.j + rotation[2][2] * v.k;
|
||||
}
|
||||
|
||||
static void vr_multiply_rotations(
|
||||
real a[3][3],
|
||||
real b[3][3],
|
||||
real out[3][3])
|
||||
{
|
||||
real r[3][3];
|
||||
int row, column;
|
||||
|
||||
for (row = 0; row < 3; row++)
|
||||
{
|
||||
for (column = 0; column < 3; column++)
|
||||
r[row][column] = a[row][0] * b[0][column] + a[row][1] * b[1][column] + a[row][2] * b[2][column];
|
||||
}
|
||||
memcpy(out, r, sizeof(r));
|
||||
}
|
||||
|
||||
/* a node moved from `from` to `to` and turned by rotation about itself: its
|
||||
matrix, or that of a node it carries (whose old matrix is node) */
|
||||
static void vr_carry(
|
||||
real_matrix4x3 *node,
|
||||
real_point3d const *from,
|
||||
real_point3d const *to,
|
||||
real rotation[3][3])
|
||||
{
|
||||
real_vector3d offset;
|
||||
|
||||
vr_point_minus(&node->position, from, &offset);
|
||||
vr_rotate_vector(rotation, &offset, &offset);
|
||||
node->position.x = to->x + offset.i;
|
||||
node->position.y = to->y + offset.j;
|
||||
node->position.z = to->z + offset.k;
|
||||
vr_rotate_vector(rotation, &node->forward, &node->forward);
|
||||
vr_rotate_vector(rotation, &node->left, &node->left);
|
||||
vr_rotate_vector(rotation, &node->up, &node->up);
|
||||
}
|
||||
|
||||
/* ---------- the arm */
|
||||
|
||||
void vr_solve_arm(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
real_point3d shoulder,
|
||||
real_point3d const *target,
|
||||
real_vector3d const *pole,
|
||||
boolean hand_stays,
|
||||
struct vr_limb_history *history)
|
||||
{
|
||||
real_matrix4x3 old[NUMBER_OF_VR_ARM_BONES];
|
||||
real rotation[NUMBER_OF_VR_ARM_BONES][3][3];
|
||||
real_point3d moved_to[NUMBER_OF_VR_ARM_BONES];
|
||||
real_vector3d upper, fore, reach, direction, bend, old_direction;
|
||||
real upper_length, fore_length, distance, along, across;
|
||||
real_point3d solved_target = *target;
|
||||
short moved[MAXIMUM_NODES_PER_ANIMATION];
|
||||
short index, bone;
|
||||
|
||||
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
|
||||
{
|
||||
if (bones[bone] < 0 || bones[bone] >= skeleton->node_count)
|
||||
return;
|
||||
old[bone] = matrices[bones[bone]];
|
||||
}
|
||||
vr_point_minus(&old[_vr_arm_fore].position, &old[_vr_arm_upper].position, &upper);
|
||||
vr_point_minus(&old[_vr_arm_hand].position, &old[_vr_arm_fore].position, &fore);
|
||||
upper_length = vr_length(&upper);
|
||||
fore_length = vr_length(&fore);
|
||||
if (!isfinite(upper_length) || !isfinite(fore_length) || upper_length < 1e-4f || fore_length < 1e-4f)
|
||||
return;
|
||||
vr_point_minus(target, &shoulder, &reach);
|
||||
distance = vr_length(&reach);
|
||||
if (!isfinite(distance) || distance < 1e-4f)
|
||||
return;
|
||||
direction = reach;
|
||||
normalize3d(&direction);
|
||||
/* out of reach: the shoulder reaches a little (8 cm), then the arm
|
||||
stretches a little (18%); the hand stays where it is held */
|
||||
if (distance > (upper_length + fore_length) * (history ? 0.996f : 0.999f))
|
||||
{
|
||||
real maximum = (upper_length + fore_length) * (history ? 0.996f : 0.999f);
|
||||
real travel = distance - maximum;
|
||||
|
||||
travel = MIN(travel, 0.08f * VR_UNITS_PER_METRE);
|
||||
shoulder.x += direction.i * travel;
|
||||
shoulder.y += direction.j * travel;
|
||||
shoulder.z += direction.k * travel;
|
||||
distance -= travel;
|
||||
if (history && distance > maximum)
|
||||
{
|
||||
real stretch = MIN(distance / maximum, 1.18f);
|
||||
|
||||
upper_length *= stretch;
|
||||
fore_length *= stretch;
|
||||
maximum *= stretch;
|
||||
}
|
||||
if (hand_stays && distance > maximum)
|
||||
{
|
||||
/* (further still, as this port had it: the shoulder follows the
|
||||
hand, and the arm stays whole) */
|
||||
shoulder.x += direction.i * (distance - maximum);
|
||||
shoulder.y += direction.j * (distance - maximum);
|
||||
shoulder.z += direction.k * (distance - maximum);
|
||||
}
|
||||
distance = MIN(distance, maximum);
|
||||
}
|
||||
if (history)
|
||||
{
|
||||
/* bent no further than 145 degrees: nearer, the bones shorten (to
|
||||
65% at most) */
|
||||
real minimum = (real)sqrt(upper_length * upper_length + fore_length * fore_length -
|
||||
1.638304f * upper_length * fore_length);
|
||||
|
||||
if (distance < minimum)
|
||||
{
|
||||
real compress = MAX(0.65f, distance / minimum);
|
||||
|
||||
upper_length *= compress;
|
||||
fore_length *= compress;
|
||||
}
|
||||
}
|
||||
if (distance < (real)fabs(upper_length - fore_length) + 1e-3f)
|
||||
distance = (real)fabs(upper_length - fore_length) + 1e-3f;
|
||||
solved_target.x = shoulder.x + direction.i * distance;
|
||||
solved_target.y = shoulder.y + direction.j * distance;
|
||||
solved_target.z = shoulder.z + direction.k * distance;
|
||||
/* the elbow: in the plane of the reach and the pole */
|
||||
{
|
||||
real dot = pole->i * direction.i + pole->j * direction.j + pole->k * direction.k;
|
||||
|
||||
bend.i = pole->i - direction.i * dot;
|
||||
bend.j = pole->j - direction.j * dot;
|
||||
bend.k = pole->k - direction.k * dot;
|
||||
if (vr_length(&bend) < 1e-4f)
|
||||
{
|
||||
/* reaching along the pole: the old bend, else a steady side */
|
||||
dot = dot_product3d(&upper, &direction);
|
||||
bend = upper;
|
||||
bend.i -= dot * direction.i;
|
||||
bend.j -= dot * direction.j;
|
||||
bend.k -= dot * direction.k;
|
||||
if (vr_length(&bend) < 1e-4f)
|
||||
{
|
||||
real_vector3d basis;
|
||||
|
||||
basis.i = basis.j = 0.0f;
|
||||
basis.k = 1.0f;
|
||||
if (fabs(direction.k) > 0.9f)
|
||||
{
|
||||
basis.j = 1.0f;
|
||||
basis.k = 0.0f;
|
||||
}
|
||||
cross_product3d(&direction, &basis, &bend);
|
||||
}
|
||||
}
|
||||
if (history)
|
||||
{
|
||||
/* the elbow's swing about the reach, eased (18 a second, at most 8
|
||||
radians a second) unless the arm was away or jumped */
|
||||
double now = halo_vr_pose_time();
|
||||
double dt = now - history->time;
|
||||
real_vector3d previous = history->bend, travel;
|
||||
real confidence = PIN(vr_length(&bend) / 0.35f, 0.0f, 1.0f);
|
||||
|
||||
vr_point_minus(&shoulder, &history->shoulder, &travel);
|
||||
dot = dot_product3d(&previous, &direction);
|
||||
previous.i -= dot * direction.i;
|
||||
previous.j -= dot * direction.j;
|
||||
previous.k -= dot * direction.k;
|
||||
if (history->valid && history->graph_index == skeleton->graph_index && dt >= 0.0 && dt < 0.25 &&
|
||||
vr_length(&travel) < 0.5f * VR_UNITS_PER_METRE && vr_unit_vector(&previous) && vr_unit_vector(&bend))
|
||||
{
|
||||
real_vector3d tangent;
|
||||
real angle, change;
|
||||
|
||||
cross_product3d(&previous, &bend, &tangent);
|
||||
angle = (real)atan2(dot_product3d(&tangent, &direction), dot_product3d(&previous, &bend));
|
||||
change = angle * (1.0f - (real)exp(-18.0 * MIN(dt, 0.05))) * confidence;
|
||||
change = PIN(change, (real)(-8.0 * dt), (real)(8.0 * dt));
|
||||
cross_product3d(&direction, &previous, &tangent);
|
||||
bend.i = previous.i * (real)cos(change) + tangent.i * (real)sin(change);
|
||||
bend.j = previous.j * (real)cos(change) + tangent.j * (real)sin(change);
|
||||
bend.k = previous.k * (real)cos(change) + tangent.k * (real)sin(change);
|
||||
}
|
||||
else
|
||||
{
|
||||
history->roll_valid = FALSE;
|
||||
}
|
||||
normalize3d(&bend);
|
||||
history->valid = TRUE;
|
||||
history->graph_index = skeleton->graph_index;
|
||||
history->time = now;
|
||||
history->shoulder = shoulder;
|
||||
history->bend = bend;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalize3d(&bend);
|
||||
}
|
||||
}
|
||||
along = (upper_length * upper_length + distance * distance - fore_length * fore_length) / (2.0f * distance);
|
||||
across = upper_length * upper_length - along * along;
|
||||
across = across > 0.0f ? (real)sqrt(across) : 0.0f;
|
||||
moved_to[_vr_arm_upper] = shoulder;
|
||||
moved_to[_vr_arm_fore].x = shoulder.x + direction.i * along + bend.i * across;
|
||||
moved_to[_vr_arm_fore].y = shoulder.y + direction.j * along + bend.j * across;
|
||||
moved_to[_vr_arm_fore].z = shoulder.z + direction.k * along + bend.k * across;
|
||||
moved_to[_vr_arm_hand] = solved_target;
|
||||
|
||||
/* each bone turned to point at the next joint, and the hand carried by
|
||||
the forearm's turn */
|
||||
vr_point_minus(&moved_to[_vr_arm_fore], &moved_to[_vr_arm_upper], &reach);
|
||||
vr_rotation_between(&upper, &reach, rotation[_vr_arm_upper]);
|
||||
vr_rotate_vector(rotation[_vr_arm_upper], &fore, &old_direction);
|
||||
vr_point_minus(&moved_to[_vr_arm_hand], &moved_to[_vr_arm_fore], &reach);
|
||||
vr_rotation_between(&old_direction, &reach, rotation[_vr_arm_fore]);
|
||||
vr_multiply_rotations(rotation[_vr_arm_fore], rotation[_vr_arm_upper], rotation[_vr_arm_fore]);
|
||||
if (hand_stays)
|
||||
{
|
||||
/* the hand stays as it was put, and what it holds */
|
||||
memset(rotation[_vr_arm_hand], 0, sizeof(rotation[_vr_arm_hand]));
|
||||
rotation[_vr_arm_hand][0][0] = rotation[_vr_arm_hand][1][1] = rotation[_vr_arm_hand][2][2] = 1.0f;
|
||||
moved_to[_vr_arm_hand] = old[_vr_arm_hand].position;
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(rotation[_vr_arm_hand], rotation[_vr_arm_fore], sizeof(rotation[_vr_arm_hand]));
|
||||
}
|
||||
|
||||
/* every node under a moved bone goes with the nearest one above it
|
||||
(parents come first) */
|
||||
for (index = 0; index < skeleton->node_count; index++)
|
||||
{
|
||||
short parent = skeleton->parents[index];
|
||||
|
||||
moved[index] = NONE;
|
||||
for (bone = 0; bone < NUMBER_OF_VR_ARM_BONES; bone++)
|
||||
{
|
||||
if (bones[bone] == index)
|
||||
moved[index] = bone;
|
||||
}
|
||||
if (moved[index] == NONE && parent != NONE)
|
||||
moved[index] = moved[parent];
|
||||
if (moved[index] != NONE)
|
||||
{
|
||||
bone = moved[index];
|
||||
vr_carry(&matrices[index], &old[bone].position, &moved_to[bone], rotation[bone]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The controller's roll shared along the arm rather than all of it at the
|
||||
wrist's skin: the hand's turn about the forearm from where the animation
|
||||
has it against its forearm (old_fore, old_hand), 85% of it on the forearm
|
||||
(none on the upper arm: vr_align_elbow rolls it), within 110 degrees, running on across half a
|
||||
turn (history). The bones' places and the hand stay. */
|
||||
void vr_distribute_arm_twist(
|
||||
real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
real_matrix4x3 const *old_fore,
|
||||
real_matrix4x3 const *old_hand,
|
||||
struct vr_limb_history *history)
|
||||
{
|
||||
real_matrix4x3 *fore = &matrices[bones[_vr_arm_fore]];
|
||||
real_matrix4x3 *hand = &matrices[bones[_vr_arm_hand]];
|
||||
real_matrix3x3 delta;
|
||||
real_vector3d expected[3], desired[3], axis;
|
||||
real_quaternion q;
|
||||
real angle, rotation[3][3];
|
||||
int a, b, row, column;
|
||||
|
||||
vr_point_minus(&hand->position, &fore->position, &axis);
|
||||
if (!vr_unit_vector(&axis))
|
||||
return;
|
||||
for (a = 0; a < 3; a++)
|
||||
{
|
||||
expected[a].i = expected[a].j = expected[a].k = 0.0f;
|
||||
desired[a].i = hand->n[a][0];
|
||||
desired[a].j = hand->n[a][1];
|
||||
desired[a].k = hand->n[a][2];
|
||||
for (b = 0; b < 3; b++)
|
||||
{
|
||||
real amount = old_fore->n[b][0] * old_hand->n[a][0] + old_fore->n[b][1] * old_hand->n[a][1] +
|
||||
old_fore->n[b][2] * old_hand->n[a][2];
|
||||
|
||||
for (row = 0; row < 3; row++)
|
||||
expected[a].n[row] += fore->n[b][row] * amount;
|
||||
}
|
||||
if (!vr_unit_vector(&expected[a]) || !vr_unit_vector(&desired[a]))
|
||||
return;
|
||||
}
|
||||
for (row = 0; row < 3; row++)
|
||||
{
|
||||
for (column = 0; column < 3; column++)
|
||||
{
|
||||
delta.n[row][column] = 0.0f;
|
||||
for (a = 0; a < 3; a++)
|
||||
delta.n[row][column] += desired[a].n[row] * expected[a].n[column];
|
||||
}
|
||||
}
|
||||
matrix3x3_rotation_to_quaternion(&delta, &q);
|
||||
angle = 2.0f * (real)atan2(dot_product3d(&q.v, &axis), q.w);
|
||||
if (!isfinite(angle))
|
||||
return;
|
||||
angle = (real)atan2(sin(angle), cos(angle));
|
||||
/* (continuous across half a turn before the limit) */
|
||||
if (history && history->roll_valid)
|
||||
angle = history->roll + (real)atan2(sin(angle - history->roll), cos(angle - history->roll));
|
||||
angle = PIN(angle, -1.92f, 1.92f);
|
||||
if (history)
|
||||
{
|
||||
history->roll = angle;
|
||||
history->roll_valid = TRUE;
|
||||
}
|
||||
vr_axis_rotation(&axis, angle * 0.85f, rotation);
|
||||
vr_rotate_vector(rotation, &fore->forward, &fore->forward);
|
||||
vr_rotate_vector(rotation, &fore->left, &fore->left);
|
||||
vr_rotate_vector(rotation, &fore->up, &fore->up);
|
||||
}
|
||||
|
||||
/* The upper arm rolled about its length so that the elbow bends in the
|
||||
plane the arm now bends in, as the animation has it bend: the shortest
|
||||
turn that took the upper arm to its new direction leaves its hinge where
|
||||
it was, and with the elbow out to the side the elbow's skin turned in
|
||||
(Quest, 2026-10-06). The hinge is the animation's bend (upper arm by
|
||||
forearm, old_upper, old_fore, old_hand) in the upper arm's own axes. A
|
||||
straight arm, then or now, has no bend: left as it is. */
|
||||
void vr_align_elbow(
|
||||
real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
real_matrix4x3 const *old_upper,
|
||||
real_matrix4x3 const *old_fore,
|
||||
real_matrix4x3 const *old_hand)
|
||||
{
|
||||
real_matrix4x3 *upper = &matrices[bones[_vr_arm_upper]];
|
||||
real_matrix4x3 const *fore = &matrices[bones[_vr_arm_fore]];
|
||||
real_matrix4x3 const *hand = &matrices[bones[_vr_arm_hand]];
|
||||
real_vector3d old_along, old_below, old_bend, along, below, bend, hinge;
|
||||
real c_forward, c_left, c_up, angle, rotation[3][3];
|
||||
|
||||
vr_point_minus(&old_fore->position, &old_upper->position, &old_along);
|
||||
vr_point_minus(&old_hand->position, &old_fore->position, &old_below);
|
||||
cross_product3d(&old_along, &old_below, &old_bend);
|
||||
vr_point_minus(&fore->position, &upper->position, &along);
|
||||
vr_point_minus(&hand->position, &fore->position, &below);
|
||||
cross_product3d(&along, &below, &bend);
|
||||
if (vr_length(&old_bend) < 1e-4f * vr_length(&old_along) * vr_length(&old_below) ||
|
||||
vr_length(&bend) < 0.05f * vr_length(&along) * vr_length(&below) ||
|
||||
!vr_unit_vector(&old_bend) || !vr_unit_vector(&bend) || !vr_unit_vector(&along))
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* (the hinge in the upper arm's axes, then where those axes are now) */
|
||||
c_forward = dot_product3d(&old_bend, &old_upper->forward);
|
||||
c_left = dot_product3d(&old_bend, &old_upper->left);
|
||||
c_up = dot_product3d(&old_bend, &old_upper->up);
|
||||
hinge.i = upper->forward.i * c_forward + upper->left.i * c_left + upper->up.i * c_up;
|
||||
hinge.j = upper->forward.j * c_forward + upper->left.j * c_left + upper->up.j * c_up;
|
||||
hinge.k = upper->forward.k * c_forward + upper->left.k * c_left + upper->up.k * c_up;
|
||||
/* (both across the arm: the turn about it between them) */
|
||||
vr_vector_scale_add_local(&hinge, &along, -dot_product3d(&hinge, &along));
|
||||
vr_vector_scale_add_local(&bend, &along, -dot_product3d(&bend, &along));
|
||||
if (!vr_unit_vector(&hinge) || !vr_unit_vector(&bend))
|
||||
return;
|
||||
{
|
||||
real_vector3d cross;
|
||||
|
||||
cross_product3d(&hinge, &bend, &cross);
|
||||
angle = (real)atan2(dot_product3d(&cross, &along), dot_product3d(&hinge, &bend));
|
||||
}
|
||||
if (!isfinite(angle))
|
||||
return;
|
||||
vr_axis_rotation(&along, angle, rotation);
|
||||
vr_rotate_vector(rotation, &upper->forward, &upper->forward);
|
||||
vr_rotate_vector(rotation, &upper->left, &upper->left);
|
||||
vr_rotate_vector(rotation, &upper->up, &upper->up);
|
||||
}
|
||||
|
||||
/* ---------- the fingers
|
||||
|
||||
Each finger's joints are found by name: "finger" and its number (0 the
|
||||
thumb to 4 the little finger), or "thumb", "index", "middle", "ring",
|
||||
"pinky", on the hand's side ("l ", "r "). They are posed by absolute
|
||||
directions in the controller's palm frame, each segment's subtree turned,
|
||||
its length kept (after HaloCE-Quest-VR, CC0, nearly verbatim; its touch of
|
||||
the world, a few rays a finger, is left out: it costs the Steam Frame's
|
||||
processor too much). */
|
||||
|
||||
/* the finger (0 the thumb .. 4 the little finger) a node's name says it is
|
||||
a joint of, on that side; NONE when not a finger's */
|
||||
static short vr_finger_of(
|
||||
char const *name,
|
||||
char const *side)
|
||||
{
|
||||
static char const *const names[] = { "thumb", "index", "middle", "ring", "pinky" };
|
||||
char const *finger;
|
||||
short index;
|
||||
|
||||
if (!name || !strstr(name, side))
|
||||
return NONE;
|
||||
finger = strstr(name, "finger");
|
||||
if (finger && finger[6] >= '0' && finger[6] <= '4')
|
||||
return (short)(finger[6] - '0');
|
||||
for (index = 0; index < 5; index++)
|
||||
{
|
||||
if (strstr(name, names[index]))
|
||||
return index;
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
void vr_finger_nodes(
|
||||
struct vr_skeleton const *skeleton,
|
||||
short hand,
|
||||
char const *side,
|
||||
short joints[5][3])
|
||||
{
|
||||
short node_index, finger, depth;
|
||||
|
||||
for (finger = 0; finger < 5; finger++)
|
||||
{
|
||||
for (depth = 0; depth < 3; depth++)
|
||||
joints[finger][depth] = NONE;
|
||||
}
|
||||
for (node_index = 0; node_index < skeleton->node_count; node_index++)
|
||||
{
|
||||
short at;
|
||||
|
||||
finger = vr_finger_of(animation_graph_node_name(skeleton->graph_index, node_index), side);
|
||||
if (finger == NONE || !vr_skeleton_under(skeleton, node_index, hand))
|
||||
continue;
|
||||
/* its depth: how many of the same finger's joints above it */
|
||||
depth = 0;
|
||||
for (at = skeleton->parents[node_index]; at != NONE; at = skeleton->parents[at])
|
||||
{
|
||||
if (vr_finger_of(animation_graph_node_name(skeleton->graph_index, at), side) == finger)
|
||||
depth++;
|
||||
}
|
||||
if (depth < 3)
|
||||
joints[finger][depth] = node_index;
|
||||
}
|
||||
}
|
||||
|
||||
/* a node and all under it turned about the node */
|
||||
void vr_turn_subtree(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short root,
|
||||
real rotation[3][3])
|
||||
{
|
||||
real_point3d pivot = matrices[root].position;
|
||||
unsigned long long under = skeleton->under[root];
|
||||
short node_index;
|
||||
|
||||
for (node_index = 0; node_index < skeleton->node_count; node_index++)
|
||||
{
|
||||
if (under & (1ULL << node_index))
|
||||
vr_carry(&matrices[node_index], &pivot, &pivot, rotation);
|
||||
}
|
||||
}
|
||||
|
||||
/* a finger's joints (low, middle, tip) curled 0 (straight, a little bent
|
||||
at rest) to 1 (a fist), in the palm's frame: forward along the fingers,
|
||||
up the knuckles towards the index, palm out of the palm */
|
||||
static boolean vr_pose_finger(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short hand,
|
||||
short const joints[3],
|
||||
int finger,
|
||||
real curl,
|
||||
real_vector3d const *palm,
|
||||
real_vector3d const *forward,
|
||||
real_vector3d const *up)
|
||||
{
|
||||
static real const rest[3] = { 0.12f, 0.15f, 0.10f };
|
||||
static real const bend[3] = { 1.25f, 1.6f, 1.1f };
|
||||
real_vector3d base, axis, direction, wanted;
|
||||
real angle = 0.0f, dot, rotation[3][3];
|
||||
int depth;
|
||||
|
||||
if (joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
|
||||
return FALSE;
|
||||
vr_point_minus(&matrices[joints[0]].position, &matrices[hand].position, &base);
|
||||
dot = dot_product3d(&base, palm);
|
||||
base.i -= dot * palm->i;
|
||||
base.j -= dot * palm->j;
|
||||
base.k -= dot * palm->k;
|
||||
if (finger == 0)
|
||||
{
|
||||
/* the thumb: up the handle, or across the palm */
|
||||
base.i = (up->i * 0.9f + forward->i * 0.3f) * (1.0f - curl) +
|
||||
(forward->i * 0.75f + palm->i * 0.55f + up->i * 0.1f) * curl;
|
||||
base.j = (up->j * 0.9f + forward->j * 0.3f) * (1.0f - curl) +
|
||||
(forward->j * 0.75f + palm->j * 0.55f + up->j * 0.1f) * curl;
|
||||
base.k = (up->k * 0.9f + forward->k * 0.3f) * (1.0f - curl) +
|
||||
(forward->k * 0.75f + palm->k * 0.55f + up->k * 0.1f) * curl;
|
||||
}
|
||||
if (!vr_unit_vector(&base))
|
||||
return FALSE;
|
||||
cross_product3d(&base, palm, &axis);
|
||||
if (!vr_unit_vector(&axis))
|
||||
return FALSE;
|
||||
for (depth = 0; depth < 3; depth++)
|
||||
{
|
||||
short joint = joints[depth];
|
||||
|
||||
if (depth < 2)
|
||||
{
|
||||
vr_point_minus(&matrices[joints[depth + 1]].position, &matrices[joint].position, &direction);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* the tip: its own axis that the middle joint's bone runs along */
|
||||
real_vector3d previous;
|
||||
real_vector3d *axes[3];
|
||||
real_vector3d *last[3];
|
||||
real best = 0.8f, sign = 1.0f;
|
||||
int a, found = -1;
|
||||
|
||||
axes[0] = &matrices[joints[1]].forward;
|
||||
axes[1] = &matrices[joints[1]].left;
|
||||
axes[2] = &matrices[joints[1]].up;
|
||||
last[0] = &matrices[joint].forward;
|
||||
last[1] = &matrices[joint].left;
|
||||
last[2] = &matrices[joint].up;
|
||||
vr_point_minus(&matrices[joint].position, &matrices[joints[1]].position, &previous);
|
||||
if (!vr_unit_vector(&previous))
|
||||
return FALSE;
|
||||
for (a = 0; a < 3; a++)
|
||||
{
|
||||
real_vector3d unit = *axes[a];
|
||||
|
||||
if (!vr_unit_vector(&unit))
|
||||
continue;
|
||||
dot = dot_product3d(&unit, &previous);
|
||||
if (fabs(dot) > best)
|
||||
{
|
||||
best = (real)fabs(dot);
|
||||
found = a;
|
||||
sign = dot < 0.0f ? -1.0f : 1.0f;
|
||||
}
|
||||
}
|
||||
if (found < 0)
|
||||
return TRUE;
|
||||
direction.i = last[found]->i * sign;
|
||||
direction.j = last[found]->j * sign;
|
||||
direction.k = last[found]->k * sign;
|
||||
}
|
||||
if (!vr_unit_vector(&direction))
|
||||
return FALSE;
|
||||
angle += finger == 0 ? (depth == 0 ? 0.0f : (depth == 1 ? 0.35f : 0.3f) * curl) :
|
||||
rest[depth] + bend[depth] * curl;
|
||||
vr_axis_rotation(&axis, angle, rotation);
|
||||
vr_rotate_vector(rotation, &base, &wanted);
|
||||
vr_rotation_between(&direction, &wanted, rotation);
|
||||
vr_turn_subtree(skeleton, matrices, joint, rotation);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void vr_pose_fingers(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short hand,
|
||||
short const joints[5][3],
|
||||
float const curls[4],
|
||||
real_vector3d const *palm,
|
||||
real_vector3d const *forward,
|
||||
real_vector3d const *up)
|
||||
{
|
||||
int finger;
|
||||
|
||||
for (finger = 0; finger < 5; finger++)
|
||||
vr_pose_finger(skeleton, matrices, hand, joints[finger], finger, curls[finger < 3 ? finger : 3], palm, forward, up);
|
||||
}
|
||||
|
||||
/* the finger's middle and tip turned this much (0..1) towards the line of
|
||||
its low joint: a finger lifted off the trigger (the weapon hand's index,
|
||||
whose low joint, and the rest of the hand, keep the weapon's grip) */
|
||||
void vr_straighten_finger(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short const joints[3],
|
||||
real amount)
|
||||
{
|
||||
real_vector3d low, middle, axis;
|
||||
real rotation[3][3];
|
||||
real angle;
|
||||
|
||||
if (amount <= 0.0f || joints[0] == NONE || joints[1] == NONE || joints[2] == NONE)
|
||||
return;
|
||||
vr_point_minus(&matrices[joints[1]].position, &matrices[joints[0]].position, &low);
|
||||
vr_point_minus(&matrices[joints[2]].position, &matrices[joints[1]].position, &middle);
|
||||
if (!vr_unit_vector(&low) || !vr_unit_vector(&middle))
|
||||
return;
|
||||
cross_product3d(&middle, &low, &axis);
|
||||
if (!vr_unit_vector(&axis))
|
||||
return;
|
||||
angle = (real)acos(PIN(dot_product3d(&middle, &low), -1.0f, 1.0f)) * PIN(amount, 0.0f, 1.0f);
|
||||
vr_axis_rotation(&axis, angle, rotation);
|
||||
vr_turn_subtree(skeleton, matrices, joints[1], rotation);
|
||||
}
|
||||
|
||||
/* ---------- hands alone (vr.body "hands")
|
||||
|
||||
The arm's bones (not the hand's, its fingers' or the weapon's) shrunk to
|
||||
nothing at a point 3.5 cm back from the wrist along the hand's line,
|
||||
knuckles to wrist: the cuff closes just behind the hand. Shrunk onto their
|
||||
own places, the forearm drew the wrist's skin to the elbow and the hand
|
||||
looked cut off (after HaloCE-Quest-VR, CC0, its vr_hide_forearms and
|
||||
vr_hand_back_axis). */
|
||||
void vr_hide_arm(
|
||||
struct vr_skeleton const *skeleton,
|
||||
real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
short const fingers[5][3],
|
||||
short gun)
|
||||
{
|
||||
real_point3d centre = matrices[bones[_vr_arm_hand]].position;
|
||||
real_point3d knuckles;
|
||||
real_vector3d back;
|
||||
boolean along = FALSE;
|
||||
short finger, node_index, found = 0;
|
||||
|
||||
knuckles.x = knuckles.y = knuckles.z = 0.0f;
|
||||
/* (the four fingers' bases: the thumb's sits off the line) */
|
||||
for (finger = 1; finger < 5; finger++)
|
||||
{
|
||||
if (fingers[finger][0] == NONE)
|
||||
continue;
|
||||
knuckles.x += matrices[fingers[finger][0]].position.x;
|
||||
knuckles.y += matrices[fingers[finger][0]].position.y;
|
||||
knuckles.z += matrices[fingers[finger][0]].position.z;
|
||||
found++;
|
||||
}
|
||||
if (found)
|
||||
{
|
||||
knuckles.x /= found;
|
||||
knuckles.y /= found;
|
||||
knuckles.z /= found;
|
||||
vr_point_minus(¢re, &knuckles, &back);
|
||||
along = vr_unit_vector(&back);
|
||||
}
|
||||
if (!along)
|
||||
{
|
||||
vr_point_minus(&matrices[bones[_vr_arm_fore]].position, ¢re, &back);
|
||||
along = vr_unit_vector(&back);
|
||||
}
|
||||
if (along)
|
||||
{
|
||||
centre.x += back.i * 0.035f * VR_UNITS_PER_METRE;
|
||||
centre.y += back.j * 0.035f * VR_UNITS_PER_METRE;
|
||||
centre.z += back.k * 0.035f * VR_UNITS_PER_METRE;
|
||||
}
|
||||
for (node_index = 0; node_index < skeleton->node_count; node_index++)
|
||||
{
|
||||
if (!vr_skeleton_under(skeleton, node_index, bones[_vr_arm_upper]) ||
|
||||
vr_skeleton_under(skeleton, node_index, bones[_vr_arm_hand]) ||
|
||||
(gun != NONE && vr_skeleton_under(skeleton, node_index, gun)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
matrices[node_index].scale = 0.0f;
|
||||
matrices[node_index].position = centre;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
VR_ARMS.H
|
||||
|
||||
The VR arms' bones (vr_arms.c), for the first-person hands
|
||||
(interface/first_person_weapons.c): the arms bent from the shoulders to the
|
||||
hands. Include after the game's math and animation headers.
|
||||
*/
|
||||
|
||||
#ifndef __VR_ARMS_H
|
||||
#define __VR_ARMS_H
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
enum
|
||||
{
|
||||
_vr_arm_upper,
|
||||
_vr_arm_fore,
|
||||
_vr_arm_hand,
|
||||
NUMBER_OF_VR_ARM_BONES
|
||||
};
|
||||
|
||||
/* a skeleton's parents, and the nodes under each node (it among them), from
|
||||
its animation graph: parents come before their children */
|
||||
struct vr_skeleton
|
||||
{
|
||||
long graph_index;
|
||||
short node_count;
|
||||
short parents[MAXIMUM_NODES_PER_ANIMATION];
|
||||
unsigned long long under[MAXIMUM_NODES_PER_ANIMATION];
|
||||
};
|
||||
|
||||
/* an arm's last frame, for its elbow's swing and its forearm's twist to run
|
||||
on smoothly (one each arm) */
|
||||
struct vr_limb_history
|
||||
{
|
||||
long graph_index;
|
||||
double time;
|
||||
real_point3d shoulder;
|
||||
real_vector3d bend;
|
||||
boolean valid;
|
||||
boolean roll_valid;
|
||||
real roll;
|
||||
};
|
||||
|
||||
boolean vr_skeleton_build(struct vr_skeleton *skeleton, long graph_index, short node_count);
|
||||
boolean vr_skeleton_under(struct vr_skeleton const *skeleton, short node_index, short root);
|
||||
/* the rotation (column vectors) taking direction a to direction b, the
|
||||
shortest way; a node and all under it turned by one about the node (the
|
||||
whole body, vr_body.c) */
|
||||
void vr_rotation_between(real_vector3d const *a, real_vector3d const *b, real rotation[3][3]);
|
||||
void vr_turn_subtree(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short root,
|
||||
real rotation[3][3]);
|
||||
|
||||
/* one arm (bones: upper arm, forearm, hand) bent from the shoulder for the
|
||||
hand to reach target, its elbow towards the pole; the hand (and what it
|
||||
holds) stays where it is, or turns with the forearm */
|
||||
void vr_solve_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES], real_point3d shoulder, real_point3d const *target,
|
||||
real_vector3d const *pole, boolean hand_stays, struct vr_limb_history *history);
|
||||
/* the hand's roll from the animation (old_fore, old_hand: the arm as
|
||||
animated) shared by the forearm and the upper arm */
|
||||
void vr_align_elbow(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
real_matrix4x3 const *old_upper, real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand);
|
||||
void vr_distribute_arm_twist(real_matrix4x3 *matrices, short const bones[NUMBER_OF_VR_ARM_BONES],
|
||||
real_matrix4x3 const *old_fore, real_matrix4x3 const *old_hand, struct vr_limb_history *history);
|
||||
|
||||
/* a hand's fingers' joints (low, middle, tip; NONE where not found), by
|
||||
their names on that side ("l ", "r "): the thumb, index, middle, ring and
|
||||
little finger */
|
||||
void vr_finger_nodes(struct vr_skeleton const *skeleton, short hand, char const *side, short joints[5][3]);
|
||||
/* the fingers curled (halo_vr_finger_curls: the thumb, index, middle, the
|
||||
last two) in the palm's frame: forward along the fingers, up the knuckles
|
||||
towards the index, palm out of the palm */
|
||||
void vr_pose_fingers(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short hand,
|
||||
short const joints[5][3], float const curls[4], real_vector3d const *palm, real_vector3d const *forward,
|
||||
real_vector3d const *up);
|
||||
/* a finger's middle and tip turned this much (0..1) into line with its low
|
||||
joint */
|
||||
void vr_straighten_finger(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short const joints[3],
|
||||
real amount);
|
||||
|
||||
/* the arm's bones shrunk to nothing just behind the wrist, the hand (with
|
||||
its fingers) and the weapon left: the hand alone */
|
||||
void vr_hide_arm(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices,
|
||||
short const bones[NUMBER_OF_VR_ARM_BONES], short const fingers[5][3], short gun);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
VR_BODY.H
|
||||
|
||||
The whole body in VR (vr.body "full", vr_body.c): the player's own biped
|
||||
drawn under the first-person arms and weapon. Include after the game's
|
||||
math, object and render headers.
|
||||
*/
|
||||
|
||||
#ifndef __VR_BODY_H
|
||||
#define __VR_BODY_H
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
struct render_camera;
|
||||
|
||||
/* render_objects.c: whether the object is the local player's own unit, drawn
|
||||
whole in first person this frame (its weapon and what it carries left
|
||||
out), and its node matrices as posed */
|
||||
boolean vr_body_drawn(short local_player_index, long object_index);
|
||||
real_matrix4x3 *vr_body_node_matrices(long object_index);
|
||||
|
||||
/* first_person_weapons.c: the drawn body's shoulders this frame (world
|
||||
units; the model's left, then right), from which the first-person arms hang;
|
||||
FALSE when no body is drawn */
|
||||
boolean vr_body_shoulders(short local_player_index, real_point3d shoulders[2]);
|
||||
|
||||
/* debug.vr_body, flat (the emulator): the camera of a player's view moved to
|
||||
look at the body (render.c), and whether the first-person weapon is left out
|
||||
of it (first_person_weapons.c) */
|
||||
boolean vr_body_debug_camera(short local_player_index, struct render_camera *render_camera,
|
||||
struct render_camera *rasterizer_camera);
|
||||
boolean vr_body_debug_hides_first_person(void);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
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.
|
||||
|
||||
- "auto": "high", less what the GPU's time asks to leave out (below);
|
||||
- "high": everything;
|
||||
- "medium", the default (2026-10-12): 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).
|
||||
|
||||
The Quest 3 held 90 Hz with everything in the autumn's first tests; by
|
||||
2026-10-12 (the whole body, upstream's shield colors and glow, 15 bots) a
|
||||
bot game on Blood Gulch had seconds at a time of 9-13 ms of GPU a frame,
|
||||
more than the 11.1 ms a frame less the compositor's share: stale frames,
|
||||
ASW, the picture doubled as the head turns. "medium" held. So "auto" reads
|
||||
the app's GPU time (XR_META_performance_metrics, host_xr.c) each frame: past
|
||||
four fifths of the frame for a quarter of a second, it leaves out the next
|
||||
effect of its steps (vr_graphics_auto_steps, the costliest for the least
|
||||
seen first); an effect comes back once the GPU time, plus what leaving it
|
||||
out saved when it went, stays under about seven tenths of the frame for
|
||||
three seconds (longer each time it had to go again soon after). Where the
|
||||
headset does not tell the GPU's time (SteamVR: the Steam Frame, whose time
|
||||
goes to its driver on the CPU) "auto" is "high".
|
||||
|
||||
debug.vr_graphics_measure leaves each effect out in turn (a second and a
|
||||
half on, as long off) and logs what the GPU time says it costs.
|
||||
|
||||
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); and the shield's glow
|
||||
(shield_glow.c, display.shield_glow: a light per flaring unit, each lighting
|
||||
the level once more). 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 onto the
|
||||
level: draws and fill; drawn once with both eyes at once, multiview, twice
|
||||
one eye at a time), 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"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.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, ...);
|
||||
/* port/linux/src/port_config.c */
|
||||
int config_boolean(char const *name);
|
||||
|
||||
/* 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 } },
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
/* vr_graphics_effects' order */
|
||||
_vr_effect_shadows,
|
||||
_vr_effect_specular,
|
||||
_vr_effect_reflections,
|
||||
_vr_effect_bump_maps,
|
||||
_vr_effect_detail_objects,
|
||||
_vr_effect_lens_flares,
|
||||
_vr_effect_screen_fog,
|
||||
_vr_effect_contrails,
|
||||
_vr_effect_weather,
|
||||
_vr_effect_camouflage_pass,
|
||||
/* the shield's glow (shield_glow.c), no switch of the table's */
|
||||
_vr_effect_shield_glow,
|
||||
NUMBER_OF_VR_EFFECTS
|
||||
};
|
||||
|
||||
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
|
||||
|
||||
static char const *const vr_effect_names[NUMBER_OF_VR_EFFECTS] =
|
||||
{
|
||||
"shadows", "specular", "reflections", "bump maps", "detail objects", "lens flares", "screen fog", "contrails",
|
||||
"weather", "camouflage pass", "shield glow"
|
||||
};
|
||||
|
||||
/* "auto"'s steps, each leaving out more: the shield's glow (a light per
|
||||
flaring unit, a pass of the level each), object shadows, reflections,
|
||||
specular light, lens flares (with the first three, "medium"), bump maps,
|
||||
then the rest of "low" */
|
||||
static unsigned long const vr_graphics_auto_steps[] =
|
||||
{
|
||||
1ul << _vr_effect_shield_glow,
|
||||
1ul << _vr_effect_shadows,
|
||||
1ul << _vr_effect_reflections,
|
||||
1ul << _vr_effect_specular,
|
||||
1ul << _vr_effect_lens_flares,
|
||||
1ul << _vr_effect_bump_maps,
|
||||
(1ul << _vr_effect_detail_objects) | (1ul << _vr_effect_screen_fog) | (1ul << _vr_effect_contrails) |
|
||||
(1ul << _vr_effect_weather) | (1ul << _vr_effect_camouflage_pass),
|
||||
};
|
||||
|
||||
#define NUMBER_OF_VR_AUTO_STEPS (sizeof(vr_graphics_auto_steps) / sizeof(vr_graphics_auto_steps[0]))
|
||||
/* of the frame's time: the GPU's past this leaves an effect out; under this
|
||||
with an effect's cost brings it back */
|
||||
#define VR_AUTO_HIGH 0.80f
|
||||
#define VR_AUTO_LOW 0.70f
|
||||
/* seconds: past the high mark this long (a share of the frames: the
|
||||
metric's frames vary) leaves out; settled after a change; under the low
|
||||
mark this long brings back (doubled, to a minute, while an effect brought
|
||||
back had to go again within ten seconds) */
|
||||
#define VR_AUTO_OVER_SECONDS 0.25f
|
||||
#define VR_AUTO_OVER_SHARE 0.75f
|
||||
#define VR_AUTO_SETTLE_SECONDS 0.4f
|
||||
#define VR_AUTO_UNDER_SECONDS 3.0f
|
||||
#define VR_AUTO_UNDER_MOST 60.0f
|
||||
#define VR_AUTO_AGAIN_SECONDS 10.0f
|
||||
/* an effect's cost (ms) until measured, and the least kept */
|
||||
#define VR_AUTO_COST_GUESS 1.5f
|
||||
#define VR_AUTO_COST_LEAST 0.3f
|
||||
/* debug.vr_graphics_measure: each half (on, off), seconds, of which the
|
||||
last part is measured */
|
||||
#define VR_MEASURE_HALF_SECONDS 1.5f
|
||||
#define VR_MEASURE_SKIP_SECONDS 0.5f
|
||||
#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];
|
||||
/* the effects left out now ("auto", debug.vr_graphics_measure), as
|
||||
bits of _vr_effect_*, and the shield's glow allowed */
|
||||
unsigned long left_out;
|
||||
boolean shield_glow_allowed;
|
||||
|
||||
/* "auto": the steps taken (0: everything drawn); the frames since the
|
||||
last change, over the high mark lately and under the low mark in a
|
||||
row; the GPU time's sums for the step's cost (before the last step, and
|
||||
after it settled); how long under the mark brings back (frames' seconds),
|
||||
and when (frames) an effect came back */
|
||||
boolean automatic, no_gpu_time_logged;
|
||||
unsigned long timed_frame;
|
||||
unsigned long steps;
|
||||
unsigned long frame, changed_frame, restored_frame, dropped_frame;
|
||||
float over_window[64];
|
||||
unsigned long over_count;
|
||||
unsigned long under_frames;
|
||||
float before_sum;
|
||||
unsigned long before_count;
|
||||
float after_sum;
|
||||
unsigned long after_count;
|
||||
unsigned long measured_step;
|
||||
float step_costs[NUMBER_OF_VR_AUTO_STEPS];
|
||||
float under_seconds;
|
||||
|
||||
/* debug.vr_graphics_measure: the effect measured, the frame its turn
|
||||
began, its sums on and off, and each effect's costs measured */
|
||||
boolean measuring;
|
||||
unsigned long measure_effect, measure_start;
|
||||
float measure_on_sum, measure_off_sum;
|
||||
unsigned long measure_on_count, measure_off_count;
|
||||
float measure_costs[NUMBER_OF_VR_EFFECTS];
|
||||
unsigned long measure_counts[NUMBER_OF_VR_EFFECTS];
|
||||
} vr_graphics;
|
||||
|
||||
static void vr_graphics_allow(
|
||||
void)
|
||||
{
|
||||
unsigned int index;
|
||||
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
|
||||
int preset = vr_graphics.preset;
|
||||
|
||||
vr_graphics.allowed[index] = (vr_graphics.client && effect->host_rules) ||
|
||||
((preset == HALO_VR_GRAPHICS_AUTO || preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset]) &&
|
||||
!(vr_graphics.left_out & (1ul << index)));
|
||||
}
|
||||
vr_graphics.shield_glow_allowed = !(vr_graphics.left_out & (1ul << _vr_effect_shield_glow));
|
||||
}
|
||||
|
||||
static void vr_graphics_choose(
|
||||
int preset,
|
||||
boolean client)
|
||||
{
|
||||
static char const *const names[] = { "low", "medium", "high", "max", "auto" };
|
||||
|
||||
vr_graphics.preset = preset;
|
||||
vr_graphics.client = client;
|
||||
vr_graphics.automatic = preset == HALO_VR_GRAPHICS_AUTO;
|
||||
if (!vr_graphics.automatic && !vr_graphics.measuring)
|
||||
vr_graphics.left_out = 0;
|
||||
vr_graphics_allow();
|
||||
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
|
||||
}
|
||||
|
||||
/* the steps' effects left out, as bits */
|
||||
static unsigned long vr_auto_left_out(
|
||||
unsigned long steps)
|
||||
{
|
||||
unsigned long left_out = 0, step;
|
||||
|
||||
for (step = 0; step < steps && step < NUMBER_OF_VR_AUTO_STEPS; step++)
|
||||
left_out |= vr_graphics_auto_steps[step];
|
||||
return left_out;
|
||||
}
|
||||
|
||||
static void vr_auto_effects_names(
|
||||
unsigned long effects,
|
||||
char *text,
|
||||
size_t size)
|
||||
{
|
||||
unsigned long effect;
|
||||
|
||||
text[0] = 0;
|
||||
for (effect = 0; effect < NUMBER_OF_VR_EFFECTS; effect++)
|
||||
{
|
||||
if (effects & (1ul << effect))
|
||||
{
|
||||
size_t length = strlen(text);
|
||||
|
||||
snprintf(text + length, size - length, "%s%s", length ? ", " : "", vr_effect_names[effect]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* whether a step leaves out anything drawn now: the shield's glow only
|
||||
with display.shield_glow, an effect only while the game wants it */
|
||||
static boolean vr_auto_step_matters(
|
||||
unsigned long step)
|
||||
{
|
||||
unsigned long effects = vr_graphics_auto_steps[step];
|
||||
unsigned long effect;
|
||||
|
||||
if ((effects & (1ul << _vr_effect_shield_glow)) && config_boolean("display.shield_glow"))
|
||||
return TRUE;
|
||||
for (effect = 0; effect < NUMBER_OF_VR_GRAPHICS_EFFECTS; effect++)
|
||||
{
|
||||
if ((effects & (1ul << effect)) && (vr_graphics.wanted[effect][0] || vr_graphics.wanted[effect][1]))
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* "auto": steps more left out, enough of them for what is over the mark by
|
||||
their costs as known (more: TRUE), or one brought back */
|
||||
static void vr_auto_step(
|
||||
boolean more,
|
||||
float gpu_ms,
|
||||
float frame_ms,
|
||||
float excess_ms)
|
||||
{
|
||||
char names[160];
|
||||
unsigned long first = vr_graphics.steps;
|
||||
|
||||
if (more)
|
||||
{
|
||||
float saved = 0.f;
|
||||
|
||||
while (vr_graphics.steps < NUMBER_OF_VR_AUTO_STEPS && (saved < excess_ms || saved == 0.f))
|
||||
{
|
||||
if (vr_auto_step_matters(vr_graphics.steps))
|
||||
saved += vr_graphics.step_costs[vr_graphics.steps];
|
||||
vr_graphics.steps++;
|
||||
}
|
||||
{
|
||||
unsigned long step, effects = 0;
|
||||
|
||||
for (step = first; step < vr_graphics.steps; step++)
|
||||
effects |= vr_auto_step_matters(step) ? vr_graphics_auto_steps[step] : 0;
|
||||
vr_auto_effects_names(effects, names, sizeof(names));
|
||||
}
|
||||
platform_log("VR graphics auto: %s left out (step %lu): the GPU %.1f ms a frame of %.1f", names,
|
||||
vr_graphics.steps, gpu_ms, frame_ms);
|
||||
/* (their cost: the GPU's time before, against after it settles; one
|
||||
step's alone) */
|
||||
vr_graphics.measured_step = vr_graphics.steps - first == 1 ? vr_graphics.steps : 0;
|
||||
vr_graphics.after_sum = 0.f;
|
||||
vr_graphics.after_count = 0;
|
||||
/* (it had to go again soon after coming back: wait longer, once for
|
||||
each coming back) */
|
||||
if (vr_graphics.restored_frame && vr_graphics.frame - vr_graphics.restored_frame <
|
||||
(unsigned long)(VR_AUTO_AGAIN_SECONDS * (1000.f / frame_ms)))
|
||||
{
|
||||
vr_graphics.under_seconds = MIN(vr_graphics.under_seconds * 2.f, VR_AUTO_UNDER_MOST);
|
||||
}
|
||||
vr_graphics.restored_frame = 0;
|
||||
vr_graphics.dropped_frame = vr_graphics.frame;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* (one that matters back, and those that do not below it with it) */
|
||||
vr_graphics.steps--;
|
||||
while (vr_graphics.steps && !vr_auto_step_matters(vr_graphics.steps - 1))
|
||||
vr_graphics.steps--;
|
||||
vr_auto_effects_names(vr_auto_left_out(first) & ~vr_auto_left_out(vr_graphics.steps), names, sizeof(names));
|
||||
platform_log("VR graphics auto: %s back (step %lu): the GPU %.1f ms a frame of %.1f, %.1f ms for it",
|
||||
names, vr_graphics.steps, gpu_ms, frame_ms, vr_graphics.step_costs[vr_graphics.steps]);
|
||||
vr_graphics.restored_frame = vr_graphics.frame;
|
||||
vr_graphics.measured_step = 0;
|
||||
}
|
||||
vr_graphics.changed_frame = vr_graphics.frame;
|
||||
vr_graphics.over_count = 0;
|
||||
vr_graphics.under_frames = 0;
|
||||
vr_graphics.left_out = vr_auto_left_out(vr_graphics.steps);
|
||||
vr_graphics_allow();
|
||||
}
|
||||
|
||||
/* "auto", each frame: the GPU's time against the frame's */
|
||||
static void vr_auto_update(
|
||||
float gpu_ms,
|
||||
float frame_ms,
|
||||
float frames_a_second)
|
||||
{
|
||||
unsigned long settle = (unsigned long)(VR_AUTO_SETTLE_SECONDS * frames_a_second);
|
||||
unsigned long over_frames = (unsigned long)(VR_AUTO_OVER_SECONDS * frames_a_second);
|
||||
unsigned long since = vr_graphics.frame - vr_graphics.changed_frame;
|
||||
unsigned long index, over = 0;
|
||||
|
||||
if (over_frames < 4)
|
||||
over_frames = 4;
|
||||
if (over_frames > NUMBEROF(vr_graphics.over_window))
|
||||
over_frames = NUMBEROF(vr_graphics.over_window);
|
||||
/* (what the last step saved: the GPU time before it, against the next
|
||||
second's after it settled) */
|
||||
if (vr_graphics.measured_step && since > settle && since <= settle + (unsigned long)frames_a_second)
|
||||
{
|
||||
vr_graphics.after_sum += gpu_ms;
|
||||
vr_graphics.after_count++;
|
||||
if (since == settle + (unsigned long)frames_a_second && vr_graphics.before_count && vr_graphics.after_count)
|
||||
{
|
||||
float cost = vr_graphics.before_sum / vr_graphics.before_count - vr_graphics.after_sum / vr_graphics.after_count;
|
||||
unsigned long step = vr_graphics.measured_step - 1;
|
||||
|
||||
vr_graphics.step_costs[step] = MAX(VR_AUTO_COST_LEAST, 0.5f * (vr_graphics.step_costs[step] + cost));
|
||||
platform_log("VR graphics auto: leaving out step %lu saved %.1f ms (taken as %.1f)", step + 1, cost,
|
||||
vr_graphics.step_costs[step]);
|
||||
vr_graphics.measured_step = 0;
|
||||
}
|
||||
}
|
||||
if (since < settle)
|
||||
return;
|
||||
vr_graphics.over_window[vr_graphics.over_count++ % over_frames] = gpu_ms;
|
||||
for (index = 0; index < over_frames && index < vr_graphics.over_count; index++)
|
||||
over += vr_graphics.over_window[index] > VR_AUTO_HIGH * frame_ms;
|
||||
if (vr_graphics.over_count >= over_frames && over >= (unsigned long)(VR_AUTO_OVER_SHARE * over_frames) &&
|
||||
vr_graphics.steps < NUMBER_OF_VR_AUTO_STEPS)
|
||||
{
|
||||
float sum = 0.f;
|
||||
|
||||
for (index = 0; index < over_frames; index++)
|
||||
sum += vr_graphics.over_window[index];
|
||||
vr_graphics.before_sum = sum;
|
||||
vr_graphics.before_count = over_frames;
|
||||
/* (far over: as many steps as its costs say it takes, at once) */
|
||||
vr_auto_step(TRUE, sum / over_frames, frame_ms, sum / over_frames - VR_AUTO_HIGH * frame_ms);
|
||||
return;
|
||||
}
|
||||
/* (back: the time, with the effect's own, under the low mark) */
|
||||
if (vr_graphics.steps && gpu_ms + vr_graphics.step_costs[vr_graphics.steps - 1] < VR_AUTO_LOW * frame_ms)
|
||||
{
|
||||
vr_graphics.under_frames++;
|
||||
}
|
||||
else
|
||||
{
|
||||
vr_graphics.under_frames = 0;
|
||||
}
|
||||
if (vr_graphics.steps && vr_graphics.under_frames > (unsigned long)(vr_graphics.under_seconds * frames_a_second))
|
||||
vr_auto_step(FALSE, gpu_ms, frame_ms, 0.f);
|
||||
/* (a minute without having to leave anything out: as quick as at first) */
|
||||
if (vr_graphics.frame - vr_graphics.dropped_frame > (unsigned long)(60.f * frames_a_second))
|
||||
vr_graphics.under_seconds = VR_AUTO_UNDER_SECONDS;
|
||||
}
|
||||
|
||||
/* debug.vr_graphics_measure, each frame: each effect left out in turn */
|
||||
static void vr_measure_update(
|
||||
float gpu_ms,
|
||||
float frames_a_second)
|
||||
{
|
||||
unsigned long half = (unsigned long)(VR_MEASURE_HALF_SECONDS * frames_a_second);
|
||||
unsigned long skip = (unsigned long)(VR_MEASURE_SKIP_SECONDS * frames_a_second);
|
||||
unsigned long since = vr_graphics.frame - vr_graphics.measure_start;
|
||||
|
||||
if (since >= skip && since < half)
|
||||
{
|
||||
vr_graphics.measure_on_sum += gpu_ms;
|
||||
vr_graphics.measure_on_count++;
|
||||
}
|
||||
else if (since >= half + skip && since < 2 * half)
|
||||
{
|
||||
vr_graphics.measure_off_sum += gpu_ms;
|
||||
vr_graphics.measure_off_count++;
|
||||
}
|
||||
if (since == half)
|
||||
{
|
||||
vr_graphics.left_out = 1ul << vr_graphics.measure_effect;
|
||||
vr_graphics_allow();
|
||||
}
|
||||
if (since < 2 * half)
|
||||
return;
|
||||
if (vr_graphics.measure_on_count && vr_graphics.measure_off_count)
|
||||
{
|
||||
unsigned long effect = vr_graphics.measure_effect;
|
||||
float on = vr_graphics.measure_on_sum / vr_graphics.measure_on_count;
|
||||
float off = vr_graphics.measure_off_sum / vr_graphics.measure_off_count;
|
||||
|
||||
vr_graphics.measure_costs[effect] += on - off;
|
||||
vr_graphics.measure_counts[effect]++;
|
||||
platform_log("VR graphics measure: %s %.2f ms (on %.2f, off %.2f; mean of %lu: %.2f)", vr_effect_names[effect],
|
||||
on - off, on, off, vr_graphics.measure_counts[effect],
|
||||
vr_graphics.measure_costs[effect] / vr_graphics.measure_counts[effect]);
|
||||
}
|
||||
vr_graphics.measure_effect = (vr_graphics.measure_effect + 1) % NUMBER_OF_VR_EFFECTS;
|
||||
vr_graphics.measure_start = vr_graphics.frame;
|
||||
vr_graphics.measure_on_sum = vr_graphics.measure_off_sum = 0.f;
|
||||
vr_graphics.measure_on_count = vr_graphics.measure_off_count = 0;
|
||||
vr_graphics.left_out = 0;
|
||||
vr_graphics_allow();
|
||||
if (!vr_graphics.measure_effect)
|
||||
{
|
||||
char line[512] = "";
|
||||
unsigned long effect;
|
||||
|
||||
for (effect = 0; effect < NUMBER_OF_VR_EFFECTS; effect++)
|
||||
{
|
||||
size_t length = strlen(line);
|
||||
|
||||
if (vr_graphics.measure_counts[effect])
|
||||
snprintf(line + length, sizeof(line) - length, " %s %.2f;", vr_effect_names[effect],
|
||||
vr_graphics.measure_costs[effect] / vr_graphics.measure_counts[effect]);
|
||||
}
|
||||
platform_log("VR graphics measure: means (ms of GPU a frame):%s", line);
|
||||
}
|
||||
}
|
||||
|
||||
/* the GPU's time against the frame's, each frame: "auto" and
|
||||
debug.vr_graphics_measure */
|
||||
static void vr_graphics_govern(
|
||||
void)
|
||||
{
|
||||
float gpu_ms = halo_vr_gpu_milliseconds();
|
||||
float rate = halo_vr_refresh_rate();
|
||||
float frames_a_second = rate > 1.f ? rate : 90.f;
|
||||
|
||||
vr_graphics.frame++;
|
||||
if (!vr_graphics.automatic && !vr_graphics.measuring)
|
||||
return;
|
||||
/* (none for five seconds: the headset does not tell it) */
|
||||
if (gpu_ms > 0.f)
|
||||
vr_graphics.timed_frame = vr_graphics.frame;
|
||||
if (gpu_ms <= 0.f && vr_graphics.frame - vr_graphics.timed_frame < (unsigned long)(5.f * frames_a_second))
|
||||
return;
|
||||
if (gpu_ms <= 0.f)
|
||||
{
|
||||
if (!vr_graphics.no_gpu_time_logged)
|
||||
platform_log("VR graphics %s: the headset does not tell the GPU's time: everything drawn (\"high\")",
|
||||
vr_graphics.measuring ? "measure" : "auto");
|
||||
vr_graphics.no_gpu_time_logged = TRUE;
|
||||
if (vr_graphics.left_out)
|
||||
{
|
||||
vr_graphics.left_out = 0;
|
||||
vr_graphics.steps = 0;
|
||||
vr_graphics_allow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (vr_graphics.measuring)
|
||||
vr_measure_update(gpu_ms, frames_a_second);
|
||||
else
|
||||
vr_auto_update(gpu_ms, 1000.f / frames_a_second, frames_a_second);
|
||||
}
|
||||
|
||||
/* 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.measuring = config_boolean("debug.vr_graphics_measure") != 0;
|
||||
vr_graphics.under_seconds = VR_AUTO_UNDER_SECONDS;
|
||||
for (index = 0; index < NUMBER_OF_VR_AUTO_STEPS; index++)
|
||||
vr_graphics.step_costs[index] = VR_AUTO_COST_GUESS;
|
||||
vr_graphics.shield_glow_allowed = TRUE;
|
||||
vr_graphics_choose(preset, client);
|
||||
if (vr_graphics.measuring)
|
||||
platform_log("VR graphics measure: each effect left out in turn, %.1f s on and %.1f s off",
|
||||
VR_MEASURE_HALF_SECONDS, VR_MEASURE_HALF_SECONDS);
|
||||
}
|
||||
else if (preset != vr_graphics.preset || client != vr_graphics.client)
|
||||
{
|
||||
vr_graphics.steps = 0;
|
||||
vr_graphics_choose(preset, client);
|
||||
}
|
||||
vr_graphics_govern();
|
||||
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 shield's glow may be drawn (shield_glow.c): "auto" leaves it
|
||||
out first */
|
||||
boolean vr_graphics_allows_shield_glow(
|
||||
void)
|
||||
{
|
||||
return !vr_graphics.started || vr_graphics.shield_glow_allowed;
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -85,6 +85,11 @@ void halo_vertex_shader_lighting(unsigned long handle);
|
||||
/* display.anti_aliasing's pass over a window's 3D view, before the HUD and
|
||||
menus (source/render/render.c): the window's bounds on the screen */
|
||||
void halo_screen_anti_alias(short x0, short y0, short x1, short y1);
|
||||
/* a map loaded (game_initialize_for_new_map): the shader programs it drew
|
||||
with last time made ready; and as it goes, its list of them kept
|
||||
(port/linux/src/xgpu_program_cache.c, the Android app's) */
|
||||
void halo_shader_programs_map_loaded(char const *map_name);
|
||||
void halo_shader_programs_map_disposed(void);
|
||||
/* display.hdr's tone mapping of a window's 3D view, before its
|
||||
anti-aliasing (source/render/render.c) */
|
||||
void halo_screen_tone_map(short x0, short y0, short x1, short y1);
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
HALO_VR.H
|
||||
|
||||
VR for the native ports (port/linux/src/vr.c; implemented on Android, where
|
||||
the host runs an OpenXR session, port/android/host/host_xr.c). The game's
|
||||
sources call these under #ifdef HALO_ANDROID; the types are plain C so that
|
||||
both the game (MSVC-era C) and the platform layer can include this.
|
||||
|
||||
Coordinates here are the game's: world units (10 feet, 3.048 metres), +z up,
|
||||
yaw counter-clockwise from +x.
|
||||
|
||||
A frame in VR:
|
||||
- halo_vr_frame_begin (the platform layer, before input is read) waits for
|
||||
the headset's next frame and takes its predicted poses;
|
||||
- the player's facing follows the head (halo_vr_aim, player_control.c);
|
||||
- render.c draws the player's view once per eye (halo_vr_eye_camera), each
|
||||
copied to its eye image (halo_vr_capture_eye), then clears the back buffer
|
||||
(halo_vr_begin_overlay) and draws the HUD, menus and overlays over nothing;
|
||||
- Present (d3d8_gl.c) copies the back buffer to the overlay layer and
|
||||
submits the frame (halo_vr_present): the overlay follows the head over the
|
||||
eyes' images, or, when no eyes were drawn (menus, loading, cinematics),
|
||||
becomes a screen fixed in the room.
|
||||
*/
|
||||
|
||||
#ifndef __HALO_VR_H
|
||||
#define __HALO_VR_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* the game runs in VR (a headset was found and its session created) */
|
||||
int halo_vr_active(void);
|
||||
|
||||
/* this frame can draw the eyes: the session is shown and the views known */
|
||||
int halo_vr_stereo_frame(void);
|
||||
|
||||
/* whether the given local player's view is drawn once per eye this frame:
|
||||
cinematics and the like are shown on the flat screen instead */
|
||||
int halo_vr_stereo_player(short local_player_index, int cinematic);
|
||||
|
||||
/* the camera of an eye, from the game's camera position: the eye's position
|
||||
and orientation, the vertical field of view (radians) and the frustum
|
||||
bounds (x0, x1, y0, y1; render_camera_build_frustum) for the game's
|
||||
640x480 viewport */
|
||||
void halo_vr_eye_camera(int eye, const float game_position[3],
|
||||
float position[3], float forward[3], float up[3],
|
||||
float *vertical_field_of_view, float frustum_bounds[4]);
|
||||
|
||||
/* copies the back buffer, just drawn for an eye, to that eye's image */
|
||||
void halo_vr_capture_eye(int eye);
|
||||
|
||||
/* Both eyes in one draw (debug.multiview, where GL_OVR_multiview2 is): whether
|
||||
this frame draws so; the camera between the eyes whose view takes in both
|
||||
(as halo_vr_eye_camera), which also starts drawing into both eyes' layers;
|
||||
and after the draw, the layers taken for the eyes' images. */
|
||||
int halo_vr_multiview(void);
|
||||
void halo_vr_center_camera(const float game_position[3], float position[3], float forward[3], float up[3],
|
||||
float *vertical_field_of_view, float frustum_bounds[4]);
|
||||
void halo_vr_capture_multiview(void);
|
||||
/* the sky, drawn about the camera, is being drawn: infinitely far */
|
||||
void halo_vr_multiview_sky(int sky);
|
||||
/* the shared camera's projection's [2][2] and [3][2], for what the game
|
||||
projects itself (sprites, lens flare tests) */
|
||||
void halo_vr_multiview_projection(float projection_22, float projection_32);
|
||||
/* lens flares are being drawn: placed by their depth, though not depth
|
||||
tested */
|
||||
void halo_vr_multiview_lens_flares(int drawing);
|
||||
|
||||
/* clears the back buffer to transparent black for the overlay, after the
|
||||
eyes; menus_active chooses where the overlay is shown */
|
||||
void halo_vr_begin_overlay(int menus_active);
|
||||
|
||||
/* the player's facing from the headset. Given the game's current desired
|
||||
facing, returns nonzero with the facing to use instead. */
|
||||
int halo_vr_aim(short local_player_index, float current_yaw, float current_pitch,
|
||||
float *yaw, float *pitch);
|
||||
|
||||
/* where the head looks, in the game's axes; 0 when not in VR play */
|
||||
int halo_vr_look_vector(short local_player_index, float vector[3]);
|
||||
|
||||
/* vr.log_performance: logs the unit's looking vector against the head's
|
||||
every second (players.c) */
|
||||
void halo_vr_debug_look(short local_player_index, const float unit_looking[3], int in_vehicle);
|
||||
|
||||
/* how far the view has turned since this was last asked, once past a few
|
||||
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, 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 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]);
|
||||
/* - and the same as a point in the world, where the crosshair's line starts
|
||||
(as the drawn frame had it): shots start there too, not from an offset to
|
||||
the unit's own eye point, which need not be the drawn camera's */
|
||||
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
|
||||
/* The first-person model's camera: where the model's weapon hand lands on
|
||||
the weapon hand's controller, as the free hand does (graph_index: the
|
||||
weapon's first-person animations, whose hand halo_vr_weapon_wrist says
|
||||
where it is, in the camera's axes: forward, right, up, world units, from
|
||||
the weapon held ready; any other, the weapon as one held at the old fixed
|
||||
place) */
|
||||
int halo_vr_weapon_camera(short local_player_index, long graph_index, float position[3], float forward[3],
|
||||
float up[3]);
|
||||
void halo_vr_weapon_wrist(long graph_index, const float wrist[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; 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):
|
||||
- the body: the eyes' position (world units), the neck they turn about,
|
||||
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 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 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_free_hand(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
/* - for the whole body: the eyes (world units), the head's forward and up,
|
||||
and the game's camera: where the eyes are standing straight (lower
|
||||
crouched by the button), from which the physical crouch is measured; */
|
||||
int halo_vr_head_pose(short local_player_index, float eye[3], float forward[3], float up[3], float standing[3]);
|
||||
int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]);
|
||||
/* - 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
|
||||
trigger reads as one on it, unpressed); */
|
||||
enum
|
||||
{
|
||||
HALO_VR_FINGERS_CURLED = 1,
|
||||
HALO_VR_FINGERS_TOUCH = 2,
|
||||
};
|
||||
int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]);
|
||||
/* - whether the free hand's arm bends at the elbow to reach it from the
|
||||
shoulder (vr.arm_ik); else it keeps the shape it has on the foregrip,
|
||||
pointed at the shoulder;
|
||||
- the time of the headset's frame (seconds, steady through the frame), by
|
||||
which the arms' bending is eased */
|
||||
int halo_vr_free_arm_bends(void);
|
||||
double halo_vr_pose_time(void);
|
||||
/* - what of the body is drawn (vr.body): the arms and hands, the hands
|
||||
alone, or neither (the weapon alone); or the whole body, its torso, hips
|
||||
and legs the player's own biped's (port/linux/game/vr_body.c), the arms
|
||||
and hands as with "arms" */
|
||||
enum
|
||||
{
|
||||
HALO_VR_BODY_ARMS = 0,
|
||||
HALO_VR_BODY_HANDS,
|
||||
HALO_VR_BODY_OFF,
|
||||
HALO_VR_BODY_FULL,
|
||||
};
|
||||
int halo_vr_body_mode(void);
|
||||
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
|
||||
int halo_vr_melee(void);
|
||||
/* voice chat's push to talk held: its button (vr.button_talk; the free
|
||||
hand's grip by default) */
|
||||
int halo_vr_push_to_talk_held(void);
|
||||
/* the weight of the melee attack under way, from the swing's speed (0..1;
|
||||
0 with none): the game scales melee damage by the player's run instead,
|
||||
and in VR a player punches standing (units.c) */
|
||||
float halo_vr_melee_strength(void);
|
||||
/* 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 (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
|
||||
view on a disc over the weapon, in the game's axes; then that view, drawn
|
||||
into the given part of the back buffer (left, top, right, bottom, 0..1),
|
||||
is taken for the disc. Between the two, the first-person weapon is left
|
||||
out (halo_vr_scope_pass). */
|
||||
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
|
||||
float *vertical_field_of_view);
|
||||
int halo_vr_scope_pass(void);
|
||||
/* The menus' pointer (d3d8_gl.c's halo_ui_pointer_update): where a
|
||||
controller points on the menus' screen, in their 640x480, whether that
|
||||
moved since asked, and the trigger's clicks since (nonzero while it points
|
||||
at the screen); and where it points this frame, to draw it (render.c) */
|
||||
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks);
|
||||
int halo_vr_menu_pointer_at(int *x, int *y);
|
||||
/* Players' names over their heads (hud.c, render.c): whether they are drawn
|
||||
apart this frame; the slot a name is drawn into (left, top, right, bottom
|
||||
of the game's 640x480); a name's place (world units, over the head),
|
||||
giving its slot (-1: no more); and the slots, drawn on the cleared back
|
||||
buffer, taken (it is cleared again) */
|
||||
int halo_vr_names_apart(void);
|
||||
void halo_vr_name_slot(int slot, short bounds[4]);
|
||||
int halo_vr_add_name(const float point[3]);
|
||||
void halo_vr_capture_names(void);
|
||||
/* how much higher than a rifle's the weapon in hand has its scope's disc
|
||||
(metres; render.c, before halo_vr_scope) */
|
||||
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; 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 */
|
||||
void halo_vr_set_aim_distance(float world_units);
|
||||
/* - and whether the HUD's crosshair, which marks where the head looks, is
|
||||
left out */
|
||||
int halo_vr_hide_crosshair(void);
|
||||
/* whether the crosshair is drawn apart this frame (render.c), and taking
|
||||
it, drawn alone on the cleared back buffer, which is cleared again */
|
||||
int halo_vr_crosshair_apart(void);
|
||||
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
|
||||
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
|
||||
{
|
||||
HALO_VR_SEAT_NONE = 0,
|
||||
HALO_VR_SEAT_STEER = 1,
|
||||
HALO_VR_SEAT_AIM = 2,
|
||||
};
|
||||
/* (armed: HALO_VR_SEAT_ARMED if the seat fires the vehicle's gun, as a
|
||||
Ghost's driver does, and HALO_VR_SEAT_TURRET too if all it does is man the
|
||||
gun, as the Warthog's gunner) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_SEAT_ARMED = 1,
|
||||
HALO_VR_SEAT_TURRET = 2,
|
||||
};
|
||||
void halo_vr_set_seat(short local_player_index, int seat, int armed, float vehicle_yaw);
|
||||
void halo_vr_set_seat_frame(short local_player_index, float vehicle_yaw);
|
||||
/* At an open vehicle's wheel (a Warthog's), each frame (render.c): the
|
||||
driver's hands on it as drawn (world units, from the game's camera), or
|
||||
NULL; the player's own hands turn it. And whether they hold it. */
|
||||
void halo_vr_set_wheel(short local_player_index, const float left[3], const float right[3]);
|
||||
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 eight 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, the free hand's); left-handed with vr.mirror_controls, their mirror
|
||||
images. The actions are numbered by the button each has by default. The
|
||||
triggers, 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,
|
||||
/* (pressed held a moment, if it took neither the weapon's foregrip nor
|
||||
the Warthog's wheel: vr.c's free_grip_down) */
|
||||
HALO_VR_INPUT_FREE_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_TALK = HALO_VR_INPUT_FREE_GRIP, /* voice chat's push to talk */
|
||||
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); "auto" is "high"
|
||||
less what the GPU's time asks to leave out */
|
||||
enum
|
||||
{
|
||||
HALO_VR_GRAPHICS_LOW = 0,
|
||||
HALO_VR_GRAPHICS_MEDIUM,
|
||||
HALO_VR_GRAPHICS_HIGH,
|
||||
HALO_VR_GRAPHICS_MAX,
|
||||
HALO_VR_GRAPHICS_AUTO,
|
||||
};
|
||||
int halo_vr_graphics(void);
|
||||
/* the app's GPU time of a recent frame (ms; 0: the headset does not tell
|
||||
it, as SteamVR), and the refresh rate (Hz; 0: unknown) */
|
||||
float halo_vr_gpu_milliseconds(void);
|
||||
float halo_vr_refresh_rate(void);
|
||||
/* whether vr.foveation has an effect: the headset's GPU foveates (the
|
||||
Quest's; vr.c, d3d8_gl.c) */
|
||||
int halo_vr_foveation_supported(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
|
||||
for a vehicle; nonzero once per ask (player_control.c spawns it) */
|
||||
int halo_vr_take_vehicle_spawn(short local_player_index);
|
||||
/* how far a gunner's view is moved back from the gun (world units) */
|
||||
float halo_vr_gunner_camera_back(void);
|
||||
/* how far forward an open vehicle's driver's view is moved (world units) */
|
||||
float halo_vr_driver_camera_forward(void);
|
||||
/* how far the eyes of a rider who sits down in the hull rise above his
|
||||
head (world units), by the kind of hull */
|
||||
enum
|
||||
{
|
||||
HALO_VR_HULL_OPEN = 0,
|
||||
HALO_VR_HULL_SCOUT, /* a Ghost: its hood arches over the rider */
|
||||
HALO_VR_HULL_FIGHTER, /* a Banshee: the pilot lies in it */
|
||||
HALO_VR_HULL_TURRET, /* a Shade: its shield is at the eyes */
|
||||
HALO_VR_HULL_TANK, /* a Scorpion: its driver sits in the hull */
|
||||
};
|
||||
float halo_vr_enclosed_seat_raise(int hull);
|
||||
/* how strongly glass tints and reflects (1 as the game has it) */
|
||||
float halo_vr_glass_strength(void);
|
||||
|
||||
/* VR players of other machines in a network game (port/linux/game/
|
||||
network_vr.c): their hands, as their machines send them, while they keep
|
||||
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 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]);
|
||||
int halo_vr_network_weapon_origin(long player_index, long unit_index, float origin[3]);
|
||||
int halo_vr_network_grenade_held(long player_index);
|
||||
int halo_vr_network_grenade_throw(long player_index, long unit_index, float origin[3], float direction[3],
|
||||
float *strength);
|
||||
void halo_vr_network_grenade_thrown(short local_player_index, long unit_index, const float origin[3],
|
||||
const float direction[3], float strength);
|
||||
|
||||
/* the platform layer's side (vr.c) */
|
||||
void halo_vr_initialize(void);
|
||||
void halo_vr_frame_begin(void);
|
||||
/* vr.log_performance: the end of a part of the frame, for the report: the
|
||||
input and network, the game's ticks (how many), the camera and director,
|
||||
the drawing; the rest is presenting */
|
||||
enum
|
||||
{
|
||||
HALO_VR_PHASE_INPUT,
|
||||
HALO_VR_PHASE_TICKS,
|
||||
HALO_VR_PHASE_CAMERA,
|
||||
HALO_VR_PHASE_DRAW,
|
||||
HALO_VR_PHASE_COUNT
|
||||
};
|
||||
void halo_vr_phase(int phase, int ticks);
|
||||
/* vr.log_performance: time taken by a part of the drawing, for the report
|
||||
(first_person_weapons.c): its start (0 when not logged), and its end. The
|
||||
first-person pose (its animation and the arms), the arms alone, the
|
||||
pose taken as built already this frame (counted), and the whole body's pose
|
||||
(vr_body.c; flat with debug.vr_body too, reported alone) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_TIMING_FIRST_PERSON,
|
||||
HALO_VR_TIMING_ARMS,
|
||||
HALO_VR_TIMING_FIRST_PERSON_KEPT,
|
||||
HALO_VR_TIMING_BODY,
|
||||
HALO_VR_TIMING_COUNT
|
||||
};
|
||||
unsigned long long halo_vr_timing_start(void);
|
||||
void halo_vr_timing_end(int part, unsigned long long start);
|
||||
/* the headset's frames, counted (0 before the first): what is drawn the same
|
||||
for every eye, as the first-person pose posed from the controller, is built
|
||||
once a frame */
|
||||
unsigned long halo_vr_frame_serial(void);
|
||||
void halo_vr_present(unsigned int back_buffer_texture);
|
||||
/* the eye image size: the back buffer's size in pixels in VR */
|
||||
int halo_vr_eye_size(int *width, int *height);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1540,6 +1540,34 @@ static void mix(float *output, unsigned long frames)
|
||||
|
||||
static SDL_AudioStream *audio_stream;
|
||||
static BOOL audio_started = FALSE;
|
||||
static BOOL audio_log = FALSE;
|
||||
|
||||
/* debug.audio_log: the level of what is mixed, every five seconds */
|
||||
static void audio_log_level(const float *samples, unsigned long count)
|
||||
{
|
||||
static double sum;
|
||||
static float peak;
|
||||
static unsigned long total;
|
||||
unsigned long index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
float value = samples[index] < 0.0f ? -samples[index] : samples[index];
|
||||
|
||||
sum += (double)value * value;
|
||||
if (value > peak)
|
||||
peak = value;
|
||||
}
|
||||
total += count;
|
||||
if (total >= (unsigned long)OUTPUT_RATE * OUTPUT_CHANNELS * 5)
|
||||
{
|
||||
platform_log("audio: mixed peak %.4f, rms %.5f over %lu samples", peak, sqrt(sum / total), total);
|
||||
sum = 0.0;
|
||||
peak = 0.0f;
|
||||
total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* the device it plays on (audio.output_device), and when it was looked at */
|
||||
static char audio_device_name[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
|
||||
static unsigned long audio_device_read_at = (unsigned long)-1;
|
||||
@@ -1567,6 +1595,8 @@ static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int
|
||||
if (!frames)
|
||||
frames = 1;
|
||||
mix(buffer, frames);
|
||||
if (audio_log)
|
||||
audio_log_level(buffer, frames * output_channels);
|
||||
SDL_PutAudioStreamData(stream, buffer, (int)(frames * output_channels * sizeof(float)));
|
||||
additional_amount -= (int)(frames * output_channels * sizeof(float));
|
||||
}
|
||||
@@ -1628,6 +1658,7 @@ static void audio_start(void)
|
||||
return;
|
||||
audio_started = TRUE;
|
||||
master_volume = (float)config_real("audio.volume");
|
||||
audio_log = config_boolean("debug.audio_log");
|
||||
reverb_enabled = config_boolean("audio.reverb");
|
||||
resampler_mode = resampler_mode_read();
|
||||
resampler_initialize();
|
||||
@@ -1672,6 +1703,8 @@ static void audio_start(void)
|
||||
if (audio_stream)
|
||||
{
|
||||
SDL_ResumeAudioStreamDevice(audio_stream);
|
||||
platform_log("audio: device stream open, %d Hz, %d channels, volume %.2f", OUTPUT_RATE, OUTPUT_CHANNELS,
|
||||
master_volume);
|
||||
return;
|
||||
}
|
||||
platform_log("cannot open an audio device (%s); sound is silent", SDL_GetError());
|
||||
|
||||
@@ -135,6 +135,10 @@ this list to generate the guest's entry points */
|
||||
X(glGenQueries) \
|
||||
X(glBeginQuery) \
|
||||
X(glEndQuery) \
|
||||
X(glTexStorage3D) \
|
||||
X(glFramebufferTextureLayer) \
|
||||
X(glFramebufferTextureMultiviewOVR) \
|
||||
X(glIsEnabled) \
|
||||
X(glGetQueryObjectuiv) \
|
||||
X(glGetInternalformativ)
|
||||
/* entry points of extensions, which a driver may lack: loaded where the
|
||||
@@ -143,6 +147,21 @@ context has the extension, never required (gl_functions.c). On ES 3.0 and
|
||||
#define GL_OPTIONAL_FUNCTIONS(X) \
|
||||
X(glDrawElementsBaseVertexEXT) \
|
||||
X(glDrawElementsBaseVertexOES)
|
||||
#ifdef HALO_ANDROID
|
||||
/* the app's alone (not the web's, whose WebGL 2 has none of them): the
|
||||
program binary cache (xgpu_program_cache.c) and the eyes' foveation
|
||||
(d3d8_gl.c); the optional ones, of GL_QCOM_texture_foveated and
|
||||
GL_KHR_parallel_shader_compile, as above */
|
||||
#define GL_ANDROID_FUNCTIONS(X) \
|
||||
X(glDeleteProgram) \
|
||||
X(glProgramParameteri) \
|
||||
X(glGetProgramBinary) \
|
||||
X(glProgramBinary) \
|
||||
X(glGetTexParameteriv)
|
||||
#define GL_ANDROID_OPTIONAL_FUNCTIONS(X) \
|
||||
X(glTextureFoveationParametersQCOM) \
|
||||
X(glMaxShaderCompilerThreadsKHR)
|
||||
#endif
|
||||
/* ANDROID_GL_FUNCTIONS_END */
|
||||
#else
|
||||
#define GL_FUNCTIONS(X) \
|
||||
@@ -274,6 +293,10 @@ GL_FUNCTIONS(GL_DECLARE_FUNCTION)
|
||||
#ifdef HALO_GLES
|
||||
GL_OPTIONAL_FUNCTIONS(GL_DECLARE_FUNCTION)
|
||||
#endif
|
||||
#ifdef HALO_ANDROID
|
||||
GL_ANDROID_FUNCTIONS(GL_DECLARE_FUNCTION)
|
||||
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_DECLARE_FUNCTION)
|
||||
#endif
|
||||
#undef GL_DECLARE_FUNCTION
|
||||
|
||||
/* call sites use the ordinary names; gl_functions.c, which defines the
|
||||
@@ -385,7 +408,18 @@ pointers, sees the declarations without these aliases */
|
||||
#define glGenQueries halo_glGenQueries
|
||||
#define glBeginQuery halo_glBeginQuery
|
||||
#define glEndQuery halo_glEndQuery
|
||||
#define glTexStorage3D halo_glTexStorage3D
|
||||
#define glFramebufferTextureLayer halo_glFramebufferTextureLayer
|
||||
#define glFramebufferTextureMultiviewOVR halo_glFramebufferTextureMultiviewOVR
|
||||
#define glIsEnabled halo_glIsEnabled
|
||||
#define glGetQueryObjectuiv halo_glGetQueryObjectuiv
|
||||
#ifdef HALO_ANDROID
|
||||
#define glDeleteProgram halo_glDeleteProgram
|
||||
#define glProgramParameteri halo_glProgramParameteri
|
||||
#define glGetProgramBinary halo_glGetProgramBinary
|
||||
#define glProgramBinary halo_glProgramBinary
|
||||
#define glGetTexParameteriv halo_glGetTexParameteriv
|
||||
#endif
|
||||
#define glGetInternalformativ halo_glGetInternalformativ
|
||||
#else
|
||||
#define glGetString halo_glGetString
|
||||
|
||||
@@ -15,6 +15,10 @@ GL_FUNCTIONS(GL_DEFINE_FUNCTION)
|
||||
#ifdef HALO_GLES
|
||||
GL_OPTIONAL_FUNCTIONS(GL_DEFINE_FUNCTION)
|
||||
#endif
|
||||
#ifdef HALO_ANDROID
|
||||
GL_ANDROID_FUNCTIONS(GL_DEFINE_FUNCTION)
|
||||
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_DEFINE_FUNCTION)
|
||||
#endif
|
||||
|
||||
int gl_functions_load(void)
|
||||
{
|
||||
@@ -38,6 +42,9 @@ has them (xgpu_capabilities, d3d8_gl.c) */
|
||||
success = success && !GL_MISSING_FAILS; \
|
||||
}
|
||||
GL_FUNCTIONS(GL_LOAD_FUNCTION)
|
||||
#ifdef HALO_ANDROID
|
||||
GL_ANDROID_FUNCTIONS(GL_LOAD_FUNCTION)
|
||||
#endif
|
||||
#undef GL_LOAD_FUNCTION
|
||||
#if defined(HALO_GLES) && !defined(HALO_WEB)
|
||||
/* a missing one is no failure: the renderer uses each only where the
|
||||
@@ -46,6 +53,9 @@ has them (xgpu_capabilities, d3d8_gl.c) */
|
||||
#define GL_LOAD_OPTIONAL_FUNCTION(name) \
|
||||
halo_##name = (__typeof__(halo_##name))SDL_GL_GetProcAddress(#name);
|
||||
GL_OPTIONAL_FUNCTIONS(GL_LOAD_OPTIONAL_FUNCTION)
|
||||
#ifdef HALO_ANDROID
|
||||
GL_ANDROID_OPTIONAL_FUNCTIONS(GL_LOAD_OPTIONAL_FUNCTION)
|
||||
#endif
|
||||
#undef GL_LOAD_OPTIONAL_FUNCTION
|
||||
#endif
|
||||
return success;
|
||||
|
||||
@@ -616,6 +616,19 @@ unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned
|
||||
return png_pixels_texture(pixels, width, height, levels);
|
||||
}
|
||||
|
||||
/* (port, Mjolnir VR: the same in two steps, the decode on any thread:
|
||||
menu_files.c's menu pictures) */
|
||||
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height)
|
||||
{
|
||||
return png_decode(png, size, width, height);
|
||||
}
|
||||
|
||||
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
|
||||
unsigned long *levels)
|
||||
{
|
||||
return pixels ? png_pixels_texture(pixels, width, height, levels) : 0;
|
||||
}
|
||||
|
||||
unsigned int hud_hires_override_texture(long asset, unsigned long *levels)
|
||||
{
|
||||
const struct hud_hires_embedded *embedded;
|
||||
|
||||
@@ -85,6 +85,11 @@ unsigned int hud_hires_override_texture(long asset, unsigned long *levels);
|
||||
/* a GL texture drawn from an 8-bit RGBA PNG (as the tools write them), with
|
||||
all its mip levels, and their number; 0 if it could not be */
|
||||
unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels);
|
||||
/* the same in two steps: the PNG's texels (RGBA, rows top first; any thread
|
||||
may decode), and a texture of them (which frees them) */
|
||||
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height);
|
||||
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
|
||||
unsigned long *levels);
|
||||
/* the HUD's textures for a map's bitmaps (port/linux/game/hud_hires_tags.c),
|
||||
decoded from their PNGs as it loads, on a thread of their own, so that they
|
||||
are only uploaded when first drawn; and, as it is unloaded, that stopped and
|
||||
|
||||
@@ -1034,9 +1034,190 @@ static struct
|
||||
unsigned int texture;
|
||||
unsigned long levels;
|
||||
int failed;
|
||||
#ifdef HALO_ANDROID
|
||||
/* being decoded on the pictures' thread (art_decode) */
|
||||
int decoding;
|
||||
#endif
|
||||
} art[MAXIMUM_ART];
|
||||
static long art_count;
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* The menus' pictures decoded on a thread of their own: a screen's first
|
||||
frame decoded its PNGs, 15 ms each, 100 ms and more for a screen (in VR a
|
||||
frame shown late, the picture doubled). A picture not decoded yet is drawn
|
||||
transparent for the frames it takes; the game's thread only uploads it. The
|
||||
requests and results carry the art's generation: forgotten art's results
|
||||
are dropped. */
|
||||
#include <pthread.h>
|
||||
|
||||
#define ART_DECODE_QUEUE 64
|
||||
|
||||
static struct
|
||||
{
|
||||
int started;
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t wake;
|
||||
/* asked (the art's index, its PNG and generation), and decoded */
|
||||
struct
|
||||
{
|
||||
long index;
|
||||
unsigned long generation;
|
||||
const unsigned char *png;
|
||||
unsigned long size;
|
||||
unsigned char *pixels;
|
||||
unsigned long width, height;
|
||||
int state; /* 0 free, 1 asked, 2 decoding, 3 decoded */
|
||||
} slots[ART_DECODE_QUEUE];
|
||||
unsigned long generation;
|
||||
/* drawn while a picture is decoded */
|
||||
unsigned int transparent;
|
||||
} art_decode;
|
||||
|
||||
static void *art_decode_main(void *unused)
|
||||
{
|
||||
(void)unused;
|
||||
pthread_mutex_lock(&art_decode.lock);
|
||||
for (;;)
|
||||
{
|
||||
int slot;
|
||||
|
||||
for (slot = 0; slot < ART_DECODE_QUEUE && art_decode.slots[slot].state != 1; slot++)
|
||||
{
|
||||
}
|
||||
if (slot == ART_DECODE_QUEUE)
|
||||
{
|
||||
pthread_cond_wait(&art_decode.wake, &art_decode.lock);
|
||||
continue;
|
||||
}
|
||||
art_decode.slots[slot].state = 2;
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
{
|
||||
unsigned long width = 0, height = 0;
|
||||
unsigned char *pixels = hud_hires_png_pixels(art_decode.slots[slot].png, art_decode.slots[slot].size,
|
||||
&width, &height);
|
||||
|
||||
pthread_mutex_lock(&art_decode.lock);
|
||||
art_decode.slots[slot].pixels = pixels;
|
||||
art_decode.slots[slot].width = width;
|
||||
art_decode.slots[slot].height = height;
|
||||
art_decode.slots[slot].state = 3;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* the art's texture if its picture is decoded (uploaded now), else asked
|
||||
for and 0 (*waiting: draw it transparent for now) */
|
||||
static unsigned int art_decoded_texture(long index, int *waiting)
|
||||
{
|
||||
unsigned int texture = 0;
|
||||
int slot, free_slot = -1;
|
||||
|
||||
*waiting = 0;
|
||||
if (!art_decode.started)
|
||||
{
|
||||
pthread_t thread;
|
||||
|
||||
art_decode.started = -1;
|
||||
pthread_mutex_init(&art_decode.lock, NULL);
|
||||
pthread_cond_init(&art_decode.wake, NULL);
|
||||
if (pthread_create(&thread, NULL, art_decode_main, NULL) == 0)
|
||||
{
|
||||
pthread_detach(thread);
|
||||
art_decode.started = 1;
|
||||
}
|
||||
}
|
||||
if (art_decode.started < 0)
|
||||
return 0;
|
||||
pthread_mutex_lock(&art_decode.lock);
|
||||
for (slot = 0; slot < ART_DECODE_QUEUE; slot++)
|
||||
{
|
||||
if (art_decode.slots[slot].state == 3 && art_decode.slots[slot].generation != art_decode.generation)
|
||||
{
|
||||
/* (forgotten art's) */
|
||||
free(art_decode.slots[slot].pixels);
|
||||
art_decode.slots[slot].pixels = NULL;
|
||||
art_decode.slots[slot].state = 0;
|
||||
}
|
||||
if (art_decode.slots[slot].state == 0 && free_slot < 0)
|
||||
free_slot = slot;
|
||||
if (art_decode.slots[slot].state && art_decode.slots[slot].index == index &&
|
||||
art_decode.slots[slot].generation == art_decode.generation)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < ART_DECODE_QUEUE)
|
||||
{
|
||||
if (art_decode.slots[slot].state == 3)
|
||||
{
|
||||
unsigned char *pixels = art_decode.slots[slot].pixels;
|
||||
unsigned long width = art_decode.slots[slot].width, height = art_decode.slots[slot].height;
|
||||
|
||||
art_decode.slots[slot].pixels = NULL;
|
||||
art_decode.slots[slot].state = 0;
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
art[index].decoding = 0;
|
||||
if (!pixels)
|
||||
return 0;
|
||||
{
|
||||
unsigned long long start = xgpu_shader_work_now_us();
|
||||
|
||||
texture = hud_hires_pixels_texture(pixels, width, height, &art[index].levels);
|
||||
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, width * height, start);
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
*waiting = 1;
|
||||
return 0;
|
||||
}
|
||||
if (free_slot >= 0 && !art[index].decoding)
|
||||
{
|
||||
unsigned long size = 0;
|
||||
const unsigned char *png;
|
||||
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
png = file_data(art[index].png, &size);
|
||||
if (!png)
|
||||
return 0;
|
||||
pthread_mutex_lock(&art_decode.lock);
|
||||
art_decode.slots[free_slot].index = index;
|
||||
art_decode.slots[free_slot].generation = art_decode.generation;
|
||||
art_decode.slots[free_slot].png = png;
|
||||
art_decode.slots[free_slot].size = size;
|
||||
art_decode.slots[free_slot].state = 1;
|
||||
art[index].decoding = 1;
|
||||
pthread_cond_signal(&art_decode.wake);
|
||||
}
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
/* (the queue full: asked again at its next draw) */
|
||||
*waiting = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static unsigned int art_transparent_texture(void)
|
||||
{
|
||||
if (!art_decode.transparent)
|
||||
{
|
||||
static const unsigned char clear[4] = { 0, 0, 0, 0 };
|
||||
|
||||
glGenTextures(1, &art_decode.transparent);
|
||||
glBindTexture(GL_TEXTURE_2D, art_decode.transparent);
|
||||
xgpu_gl_state_invalidate();
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, clear);
|
||||
xgpu_gl_state_invalidate();
|
||||
}
|
||||
return art_decode.transparent;
|
||||
}
|
||||
#endif
|
||||
/* the lowest and highest texture the art stands for: every texture the
|
||||
renderer binds asks (xbox_textures.c), every frame of a game too, and the
|
||||
menus' lie together, made as the map loaded, mostly apart from the map's
|
||||
(in its texture cache) */
|
||||
static unsigned long art_lowest, art_highest;
|
||||
|
||||
void halo_menus_art_register(void const *texture, char const *png)
|
||||
{
|
||||
unsigned long data = ((const unsigned long *)texture)[1];
|
||||
@@ -1072,6 +1253,10 @@ void halo_menus_art_register(void const *texture, char const *png)
|
||||
memset(&art[index], 0, sizeof(art[index]));
|
||||
art[index].data = data;
|
||||
art[index].png = strdup(png);
|
||||
if (art_count == 1 || data < art_lowest)
|
||||
art_lowest = data;
|
||||
if (art_count == 1 || data > art_highest)
|
||||
art_highest = data;
|
||||
}
|
||||
|
||||
void halo_menus_art_forget(void)
|
||||
@@ -1088,12 +1273,23 @@ void halo_menus_art_forget(void)
|
||||
}
|
||||
}
|
||||
art_count = 0;
|
||||
art_lowest = art_highest = 0;
|
||||
#ifdef HALO_ANDROID
|
||||
if (art_decode.started > 0)
|
||||
{
|
||||
pthread_mutex_lock(&art_decode.lock);
|
||||
art_decode.generation++;
|
||||
pthread_mutex_unlock(&art_decode.lock);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
|
||||
{
|
||||
long index;
|
||||
|
||||
if (!art_count || data < art_lowest || data > art_highest)
|
||||
return 0;
|
||||
for (index = 0; index < art_count; index++)
|
||||
{
|
||||
if (art[index].data == data)
|
||||
@@ -1104,10 +1300,25 @@ unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
|
||||
return 0;
|
||||
if (!art[index].texture)
|
||||
{
|
||||
unsigned long size = 0;
|
||||
const unsigned char *png = file_data(art[index].png, &size);
|
||||
#ifdef HALO_ANDROID
|
||||
int waiting = 0;
|
||||
|
||||
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
|
||||
art[index].texture = art_decoded_texture(index, &waiting);
|
||||
if (waiting)
|
||||
{
|
||||
*levels = 1;
|
||||
return art_transparent_texture();
|
||||
}
|
||||
if (!art[index].texture && art_decode.started < 0)
|
||||
#endif
|
||||
{
|
||||
unsigned long size = 0;
|
||||
unsigned long long start = xgpu_shader_work_now_us();
|
||||
const unsigned char *png = file_data(art[index].png, &size);
|
||||
|
||||
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
|
||||
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, 0, start);
|
||||
}
|
||||
if (!art[index].texture)
|
||||
{
|
||||
platform_log("menus: could not draw %s", art[index].png);
|
||||
|
||||
@@ -387,13 +387,30 @@ static const char shader_prologue[] =
|
||||
"}\n";
|
||||
|
||||
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting)
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview)
|
||||
{
|
||||
struct xgpu_text text = { 0 };
|
||||
unsigned long index;
|
||||
|
||||
#ifdef HALO_GLES
|
||||
xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language);
|
||||
if (multiview)
|
||||
{
|
||||
/* VR: one draw into both eyes' layers. The scene is posed for a
|
||||
camera between the eyes whose view covers both; each view moves the
|
||||
position to its eye (half the eyes' distance aside) and onto its
|
||||
own view's edges. As x' = a x + b w + c and y' likewise, with w the
|
||||
depth along the view: mv_rows[2 * view] and [2 * view + 1];
|
||||
what is drawn in screen space goes without c, as though far. */
|
||||
xgpu_text_append(&text,
|
||||
"#extension GL_OVR_multiview2 : require\n"
|
||||
"layout(num_views = 2) in;\n"
|
||||
"uniform vec4 mv_rows[4];\n"
|
||||
/* the projection's depth: z = x + y / depth; z: depth tested */
|
||||
"uniform vec4 mv_depth;\n");
|
||||
}
|
||||
#else
|
||||
(void)multiview;
|
||||
#endif
|
||||
xgpu_text_append(&text, "%s", shader_prologue);
|
||||
/* (the pixel shader's model_lighting; the normal's length in w) */
|
||||
@@ -580,6 +597,7 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
GL: rows from the top, depth 0..1 */
|
||||
"\tgl_Position.y = -gl_Position.y;\n"
|
||||
"\tgl_Position.z = 2.0 * gl_Position.z - gl_Position.w;\n"
|
||||
"%s"
|
||||
#endif
|
||||
"\tgl_PointSize = oPts.x;\n"
|
||||
"\txD0 = clamp(oD0, 0.0, 1.0);\n"
|
||||
@@ -593,6 +611,29 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
"\txFog = oFog.x;\n"
|
||||
"}\n",
|
||||
XGPU_VERTEX_CONSTANT_BIAS - 38, XGPU_VERTEX_CONSTANT_BIAS - 37
|
||||
#ifdef HALO_ANDROID
|
||||
/* (what is drawn in screen space, which kept no clip position, is
|
||||
taken to each view as though infinitely far: a lens flare's sun
|
||||
lands where the sky's does) */
|
||||
/* (what the game projects itself, a sprite or a lens flare's
|
||||
test, comes as a screen position with w 1 and its depth in z: its
|
||||
eye's offset is by that depth, where it is depth tested) */
|
||||
, multiview ?
|
||||
"\t{\n"
|
||||
"\t\tint row = int(gl_ViewID_OVR) * 2;\n"
|
||||
"\t\tfloat near = 0.0;\n"
|
||||
"\t\tif (clip_captured && abs(clip_position.w - 1.0) > 1.0e-5)\n"
|
||||
"\t\t\tnear = 1.0;\n"
|
||||
"\t\telse if (clip_captured && mv_depth.z > 0.5)\n"
|
||||
"\t\t{\n"
|
||||
"\t\t\tfloat depth = mv_depth.y / (clip_position.z - mv_depth.x);\n"
|
||||
"\t\t\tnear = depth > 0.0 ? 1.0 / depth : 0.0;\n"
|
||||
"\t\t}\n"
|
||||
"\t\tgl_Position.xy = vec2(dot(vec3(gl_Position.xw, near), mv_rows[row].xyz),\n"
|
||||
"\t\t\tdot(vec3(gl_Position.yw, near), mv_rows[row + 1].xyz));\n"
|
||||
"\t}\n" :
|
||||
""
|
||||
#endif
|
||||
);
|
||||
return text.buffer;
|
||||
}
|
||||
|
||||
@@ -421,6 +421,8 @@ static struct
|
||||
unsigned char join_host_hash[P2P_KEY_HASH_SIZE];
|
||||
unsigned char join_token[P2P_TOKEN_SIZE];
|
||||
unsigned long join_time;
|
||||
/* how the latest join goes, for the menus (p2p_join_state) */
|
||||
int join_state;
|
||||
/* the join's progress (p2p_join_progress): one was asked for this run;
|
||||
its host answered; it was given up (JOIN_TIMEOUT); the host's game
|
||||
refused it (its rejection code, -1 if not) */
|
||||
@@ -976,6 +978,12 @@ static void drop_peer(struct peer *peer, const char *reason)
|
||||
{
|
||||
int ask_again = p2p.joining && peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE);
|
||||
|
||||
if (peer->is_host && !memcmp(peer->identifier, p2p.join_host, P2P_IDENTIFIER_SIZE) &&
|
||||
p2p.join_state != P2P_JOIN_STATE_NONE)
|
||||
{
|
||||
p2p.join_state = peer->connected ? P2P_JOIN_STATE_LOST : P2P_JOIN_STATE_BLOCKED;
|
||||
}
|
||||
|
||||
if (!peer->probe)
|
||||
{
|
||||
platform_log("Internet play: %s %s: %s", peer->is_host && !peer->is_player ? "host" : "player", peer->name,
|
||||
@@ -1225,6 +1233,8 @@ static void peer_heard(struct peer *peer, unsigned long address, unsigned short
|
||||
peer->name, log_address_ipv4(address, port, text, sizeof(text)));
|
||||
if (peer->is_host && !peer->is_player)
|
||||
{
|
||||
if (!memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE) && p2p.join_state != P2P_JOIN_STATE_NONE)
|
||||
p2p.join_state = P2P_JOIN_STATE_CONNECTED;
|
||||
if (p2p.joining && !memcmp(p2p.join_host, peer->identifier, P2P_IDENTIFIER_SIZE))
|
||||
{
|
||||
p2p.joining = 0;
|
||||
@@ -2839,6 +2849,7 @@ static int join_invite(const char *text)
|
||||
memcpy(p2p.join_host_hash, hash, sizeof(hash));
|
||||
memcpy(p2p.join_token, token, sizeof(token));
|
||||
p2p.join_requested = 1;
|
||||
p2p.join_state = P2P_JOIN_STATE_REACHING;
|
||||
join_progress_begin(0);
|
||||
return 1;
|
||||
}
|
||||
@@ -2883,6 +2894,130 @@ int p2p_join_invite(const char *text)
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
int p2p_join_code(char *text, int size)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
result = p2p.running && p2p_code_hosted(text, size);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int p2p_join_with_code(const char *typed)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
result = p2p.running && p2p_code_look_up(typed);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int p2p_join_code_status(void)
|
||||
{
|
||||
int status;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
status = p2p.running ? p2p_code_status() : P2P_CODE_OFFLINE;
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
void p2p_set_public_game(int public_game)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
p2p_list_set_public(public_game);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
/* (and in the server browser of every build: p2p_lobby.c) */
|
||||
p2p_set_hosting_public(public_game);
|
||||
}
|
||||
|
||||
int p2p_public_game(void)
|
||||
{
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
result = p2p_list_public();
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
|
||||
int maximum_players)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
p2p_list_set_description(name, map, mode, players, maximum_players);
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
}
|
||||
|
||||
/* the server browser's games first (every build lists its public games
|
||||
there: PC, phones, VR), then the VR port's own list's that it has not */
|
||||
int p2p_public_games(struct p2p_public_game *games, int maximum)
|
||||
{
|
||||
struct p2p_listing listings[32];
|
||||
struct p2p_public_game own[32];
|
||||
int listing_count;
|
||||
int own_count;
|
||||
int count = 0;
|
||||
int index;
|
||||
|
||||
p2p_lobby_browse(1);
|
||||
listing_count = p2p_lobby_games(listings, 32);
|
||||
for (index = 0; index < listing_count && count < maximum; index++)
|
||||
{
|
||||
struct p2p_public_game *game;
|
||||
|
||||
/* (one with a password: joined from the server browser, which asks
|
||||
for it, not from this list, which has no way to) */
|
||||
if (listings[index].locked)
|
||||
continue;
|
||||
game = &games[count++];
|
||||
|
||||
memset(game, 0, sizeof(*game));
|
||||
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
|
||||
snprintf(game->map, sizeof(game->map), "%s", listings[index].map);
|
||||
snprintf(game->mode, sizeof(game->mode), "%s", listings[index].gametype);
|
||||
snprintf(game->invite, sizeof(game->invite), "%s", listings[index].invite);
|
||||
game->players = listings[index].player_count;
|
||||
game->maximum_players = listings[index].maximum_player_count;
|
||||
}
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
own_count = p2p.running ? p2p_list_games(own, 32) : 0;
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
for (index = 0; index < own_count && count < maximum; index++)
|
||||
{
|
||||
int other;
|
||||
int listed = 0;
|
||||
|
||||
for (other = 0; other < listing_count; other++)
|
||||
{
|
||||
if (!strcmp(own[index].name, listings[other].name) && !strcmp(own[index].map, listings[other].map))
|
||||
listed = 1;
|
||||
}
|
||||
if (!listed)
|
||||
games[count++] = own[index];
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int p2p_join_state(void)
|
||||
{
|
||||
int state;
|
||||
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
state = p2p.running ? p2p.join_state : P2P_JOIN_STATE_NONE;
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
return state;
|
||||
}
|
||||
|
||||
void p2p_join_code_clear_status(void)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
p2p_code_clear_status();
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
}
|
||||
|
||||
int p2p_join_listing(const struct p2p_listing *listing)
|
||||
{
|
||||
unsigned char hash[P2P_KEY_HASH_SIZE], token[P2P_TOKEN_SIZE];
|
||||
@@ -2970,8 +3105,9 @@ void p2p_join_refused(int rejection_code)
|
||||
|
||||
void p2p_invite_received(const char *text)
|
||||
{
|
||||
/* (an older version's is logged as such) */
|
||||
if (!join_invite(text))
|
||||
/* (an older version's is logged as such; a join code is looked up,
|
||||
p2p_code.c) */
|
||||
if (!join_invite(text) && !p2p_code_look_up(text))
|
||||
platform_log("Internet play: that is not an invite");
|
||||
}
|
||||
|
||||
@@ -3001,6 +3137,8 @@ static void update_joining(void)
|
||||
p2p.joining = 0;
|
||||
p2p.join_gave_up = 1;
|
||||
p2p_signal_stop_joining();
|
||||
if (p2p.join_state == P2P_JOIN_STATE_REACHING)
|
||||
p2p.join_state = P2P_JOIN_STATE_NO_ANSWER;
|
||||
platform_log("Internet play: no answer from the invite's host; it may have stopped hosting or quit");
|
||||
}
|
||||
}
|
||||
@@ -3144,8 +3282,13 @@ static void update_hosting(void)
|
||||
p2p_signal_start();
|
||||
p2p_signal_host(p2p.token);
|
||||
{
|
||||
/* (and a join code for it, p2p_code.c) */
|
||||
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
|
||||
char shown[128];
|
||||
|
||||
p2p_key_hash(p2p_public_key(), bytes);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, p2p.token, P2P_TOKEN_SIZE);
|
||||
p2p_code_host(bytes);
|
||||
/* (whole on the clipboard and in the menus) */
|
||||
platform_log("Internet play: hosting. Invite players with this link (it only works while this "
|
||||
"copy of the game runs; it is on the clipboard): %s",
|
||||
@@ -3163,6 +3306,7 @@ static void update_hosting(void)
|
||||
{
|
||||
p2p.hosting = 0;
|
||||
p2p_signal_stop_hosting();
|
||||
p2p_code_stop_hosting();
|
||||
p2p_discord_set_hosting(NULL, 0, 0);
|
||||
}
|
||||
if (p2p.hosting)
|
||||
|
||||
@@ -26,6 +26,63 @@ int p2p_hand_off_invite(void);
|
||||
words around it. Returns nonzero if it held an invite */
|
||||
int p2p_join_invite(const char *text);
|
||||
|
||||
/* Join codes (p2p_code.c): the hosted game's code as XXXX-XXXX (0 if
|
||||
none); looking a typed one up, which then joins as its invite does (0 if
|
||||
it is not a code, or internet play is off); and how the look up goes */
|
||||
enum
|
||||
{
|
||||
P2P_CODE_IDLE,
|
||||
P2P_CODE_LOOKING,
|
||||
P2P_CODE_FOUND,
|
||||
P2P_CODE_NOT_FOUND,
|
||||
P2P_CODE_INVALID,
|
||||
P2P_CODE_OWN,
|
||||
P2P_CODE_OFFLINE,
|
||||
};
|
||||
int p2p_join_code(char *text, int size);
|
||||
int p2p_join_with_code(const char *typed);
|
||||
int p2p_join_code_status(void);
|
||||
void p2p_join_code_clear_status(void);
|
||||
/* how the latest join of an invite or code goes (the VR port's, for the
|
||||
menus): reaching the host, connected (its game is in the system link list),
|
||||
or why not */
|
||||
enum
|
||||
{
|
||||
P2P_JOIN_STATE_NONE,
|
||||
P2P_JOIN_STATE_REACHING,
|
||||
P2P_JOIN_STATE_CONNECTED,
|
||||
P2P_JOIN_STATE_NO_ANSWER,
|
||||
P2P_JOIN_STATE_BLOCKED,
|
||||
P2P_JOIN_STATE_LOST,
|
||||
};
|
||||
int p2p_join_state(void);
|
||||
|
||||
/* Public games (p2p_list.c): the hosted game listed for anyone or not, and
|
||||
what it is (the game's server, as it changes); and the games others list,
|
||||
joined by their codes (p2p_join_with_code) */
|
||||
enum
|
||||
{
|
||||
P2P_PUBLIC_NAME_SIZE = 16,
|
||||
P2P_PUBLIC_MAP_SIZE = 32,
|
||||
P2P_PUBLIC_MODE_SIZE = 24,
|
||||
};
|
||||
struct p2p_public_game
|
||||
{
|
||||
char name[P2P_PUBLIC_NAME_SIZE];
|
||||
char map[P2P_PUBLIC_MAP_SIZE];
|
||||
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||
char code[12];
|
||||
int players;
|
||||
int maximum_players;
|
||||
/* (a game of the server browser's: no code, its invite) */
|
||||
char invite[77];
|
||||
};
|
||||
void p2p_set_public_game(int public_game);
|
||||
int p2p_public_game(void);
|
||||
void p2p_set_game_description(const char *name, const char *map, const char *mode, int players,
|
||||
int maximum_players);
|
||||
int p2p_public_games(struct p2p_public_game *games, int maximum);
|
||||
|
||||
/* this machine's identifier, which its XNADDR carries (6 bytes) */
|
||||
const unsigned char *p2p_identifier(void);
|
||||
/* the address the game reaches the machine with this identifier at, if it
|
||||
@@ -126,6 +183,7 @@ enum
|
||||
P2P_LISTING_GAMETYPE_SIZE = 24,
|
||||
/* an invite link's text (P2P_LINK_SIZE) */
|
||||
P2P_LISTING_INVITE_SIZE = 77,
|
||||
/* (struct p2p_public_game's invite is as large) */
|
||||
/* a password's game's: its host's key and its key's hash, and its token
|
||||
sealed (P2P_KEY_SIZE, P2P_KEY_HASH_SIZE, P2P_SEALED_TOKEN_SIZE) */
|
||||
P2P_LISTING_SIGNING_KEY_SIZE = 32,
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
P2P_CODE.C
|
||||
|
||||
Join codes (the VR port's): a short code that stands for a hosted game's
|
||||
invite (p2p.c), for a player who cannot pass a link on, as in a headset,
|
||||
where a link is not copied out and 64 hex digits are not typed in.
|
||||
|
||||
- A host makes a code when it starts hosting: 8 characters of Crockford's
|
||||
base 32 (40 bits), shown as XXXX-XXXX (ui_join_code.c draws it in the
|
||||
lobby). It lasts while the host hosts.
|
||||
- From the code come a topic and a key (PBKDF2-HMAC-SHA256, slow on
|
||||
purpose: CODE_KDF_ITERATIONS). The host listens on the topic through the
|
||||
signalling brokers (p2p_signal.c). A player who types the code asks
|
||||
there (ASK: a nonce), and the host answers with its invite, sealed with
|
||||
the key, with the nonce (INVITE); the asker opens it and joins as an
|
||||
invite link would.
|
||||
- The brokers, and anyone who watches them, see the topic, but cannot tell
|
||||
the code from it, or open the answer, short of working the slow
|
||||
derivation out for codes until one fits; a code is guessed at one in 2^40
|
||||
of them. An asker who has the code has the invite, as one given the link
|
||||
has: it joins as anyone invited does.
|
||||
|
||||
A build without this (another of the same network version) has no codes,
|
||||
and its players join with the link (which the host still logs and copies).
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
#include "posix.h"
|
||||
#include "p2p_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
enum
|
||||
{
|
||||
CODE_SYMBOLS = 8,
|
||||
CODE_KDF_ITERATIONS = 20000,
|
||||
CODE_NONCE_SIZE = 8,
|
||||
CODE_INVITE_SIZE = P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE,
|
||||
CODE_MESSAGE_VERSION = 1,
|
||||
/* milliseconds */
|
||||
ASK_INTERVAL = 1000,
|
||||
LOOK_TIMEOUT = 20000,
|
||||
/* the host's answers: this many at once, and one more each this often */
|
||||
MAXIMUM_ANSWERS = 8,
|
||||
ANSWER_INTERVAL = 250,
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
_code_message_ask = 'Q',
|
||||
_code_message_invite = 'I',
|
||||
};
|
||||
|
||||
/* Crockford's base 32: no I, L, O or U */
|
||||
static const char code_alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
|
||||
static struct
|
||||
{
|
||||
/* hosting: the code, its topic and key, and the invite it stands for */
|
||||
int hosting;
|
||||
char host_code[CODE_SYMBOLS + 1];
|
||||
char host_topic[P2P_CODE_TOPIC_SIZE];
|
||||
unsigned char host_key[P2P_SHA256_SIZE];
|
||||
unsigned char invite[CODE_INVITE_SIZE];
|
||||
int answers_left;
|
||||
unsigned long answers_time;
|
||||
|
||||
/* looking a code up */
|
||||
int status;
|
||||
char look_topic[P2P_CODE_TOPIC_SIZE];
|
||||
unsigned char look_key[P2P_SHA256_SIZE];
|
||||
unsigned char look_nonce[CODE_NONCE_SIZE];
|
||||
unsigned long look_started_time;
|
||||
unsigned long asked_time;
|
||||
int asked;
|
||||
} code;
|
||||
|
||||
static int code_elapsed(unsigned long since, unsigned long time)
|
||||
{
|
||||
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
|
||||
}
|
||||
|
||||
/* the code's symbols from what was typed: letters and digits, read as
|
||||
Crockford reads them (O as 0, I and L as 1), the rest (dashes, spaces)
|
||||
left out; 0 unless they are exactly a code's */
|
||||
static int code_normalize(const char *text, char *symbols)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
for (; *text; text++)
|
||||
{
|
||||
char character = *text;
|
||||
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = (char)(character - 'a' + 'A');
|
||||
if (character == 'O')
|
||||
character = '0';
|
||||
else if (character == 'I' || character == 'L')
|
||||
character = '1';
|
||||
if (!strchr(code_alphabet, character) || !character)
|
||||
{
|
||||
if (character == '-' || character == ' ' || character == '\t' || character == '\r' ||
|
||||
character == '\n')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (count == CODE_SYMBOLS)
|
||||
return 0;
|
||||
symbols[count++] = character;
|
||||
}
|
||||
symbols[count] = 0;
|
||||
return count == CODE_SYMBOLS;
|
||||
}
|
||||
|
||||
/* the topic and key of a code (PBKDF2-HMAC-SHA256, one block) */
|
||||
static void code_derive(const char *symbols, char *topic, unsigned char *key)
|
||||
{
|
||||
static const char salt[] = "halo-ce-vr join code 1";
|
||||
unsigned char block[sizeof(salt) - 1 + 4];
|
||||
unsigned char u[P2P_SHA256_SIZE];
|
||||
unsigned char derived[P2P_SHA256_SIZE];
|
||||
unsigned char digest[P2P_SHA256_SIZE];
|
||||
char hex[2 * P2P_SHA256_SIZE + 1];
|
||||
int iteration;
|
||||
int index;
|
||||
|
||||
memcpy(block, salt, sizeof(salt) - 1);
|
||||
block[sizeof(salt) - 1] = 0;
|
||||
block[sizeof(salt)] = 0;
|
||||
block[sizeof(salt) + 1] = 0;
|
||||
block[sizeof(salt) + 2] = 1;
|
||||
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, block, sizeof(block), u);
|
||||
memcpy(derived, u, sizeof(derived));
|
||||
for (iteration = 1; iteration < CODE_KDF_ITERATIONS; iteration++)
|
||||
{
|
||||
p2p_hmac_sha256((const unsigned char *)symbols, CODE_SYMBOLS, u, sizeof(u), u);
|
||||
for (index = 0; index < P2P_SHA256_SIZE; index++)
|
||||
derived[index] ^= u[index];
|
||||
}
|
||||
p2p_hmac_sha256(derived, sizeof(derived), "topic", 5, digest);
|
||||
p2p_hex(digest, 16, hex);
|
||||
snprintf(topic, P2P_CODE_TOPIC_SIZE, "hcvr/1/%s", hex);
|
||||
p2p_hmac_sha256(derived, sizeof(derived), "seal", 4, key);
|
||||
}
|
||||
|
||||
/* ---------- p2p.c's side (under p2p_lock) */
|
||||
|
||||
void p2p_code_host(const unsigned char *invite)
|
||||
{
|
||||
unsigned char random[CODE_SYMBOLS];
|
||||
int index;
|
||||
|
||||
posix_random_bytes(random, sizeof(random));
|
||||
for (index = 0; index < CODE_SYMBOLS; index++)
|
||||
code.host_code[index] = code_alphabet[random[index] & 31];
|
||||
code.host_code[CODE_SYMBOLS] = 0;
|
||||
memcpy(code.invite, invite, CODE_INVITE_SIZE);
|
||||
code_derive(code.host_code, code.host_topic, code.host_key);
|
||||
code.answers_left = MAXIMUM_ANSWERS;
|
||||
code.answers_time = p2p_now();
|
||||
code.hosting = 1;
|
||||
platform_log("Internet play: join code %.4s-%.4s", code.host_code, code.host_code + 4);
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
|
||||
void p2p_code_stop_hosting(void)
|
||||
{
|
||||
code.hosting = 0;
|
||||
code.host_code[0] = 0;
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
|
||||
int p2p_code_hosted(char *text, int size)
|
||||
{
|
||||
if (!code.hosting || size < CODE_SYMBOLS + 2)
|
||||
return 0;
|
||||
snprintf(text, (size_t)size, "%.4s-%.4s", code.host_code, code.host_code + 4);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int p2p_code_look_up(const char *typed)
|
||||
{
|
||||
char symbols[CODE_SYMBOLS + 1];
|
||||
|
||||
if (!code_normalize(typed, symbols))
|
||||
{
|
||||
code.status = P2P_CODE_INVALID;
|
||||
return 0;
|
||||
}
|
||||
if (code.hosting && !strcmp(symbols, code.host_code))
|
||||
{
|
||||
code.status = P2P_CODE_OWN;
|
||||
return 0;
|
||||
}
|
||||
code_derive(symbols, code.look_topic, code.look_key);
|
||||
posix_random_bytes(code.look_nonce, sizeof(code.look_nonce));
|
||||
code.look_started_time = p2p_now();
|
||||
code.asked = 0;
|
||||
code.status = P2P_CODE_LOOKING;
|
||||
p2p_signal_sync_topics();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int p2p_code_status(void)
|
||||
{
|
||||
return code.status;
|
||||
}
|
||||
|
||||
void p2p_code_clear_status(void)
|
||||
{
|
||||
if (code.status != P2P_CODE_LOOKING)
|
||||
code.status = P2P_CODE_IDLE;
|
||||
}
|
||||
|
||||
/* ---------- p2p_signal.c's side (under p2p_lock) */
|
||||
|
||||
void p2p_code_topics(const char **host_topic, const char **look_topic)
|
||||
{
|
||||
*host_topic = code.hosting ? code.host_topic : "";
|
||||
*look_topic = code.status == P2P_CODE_LOOKING ? code.look_topic : "";
|
||||
}
|
||||
|
||||
static void code_answer(const unsigned char *nonce)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE];
|
||||
unsigned char sealed[2 + sizeof(message) + P2P_SEAL_OVERHEAD];
|
||||
int sealed_size;
|
||||
|
||||
/* (anyone watching the brokers can ask, as often as they like) */
|
||||
while (code.answers_left < MAXIMUM_ANSWERS && code_elapsed(code.answers_time, ANSWER_INTERVAL))
|
||||
{
|
||||
code.answers_left++;
|
||||
code.answers_time += ANSWER_INTERVAL;
|
||||
}
|
||||
if (code.answers_left == MAXIMUM_ANSWERS)
|
||||
code.answers_time = p2p_now();
|
||||
if (code.answers_left <= 0)
|
||||
return;
|
||||
code.answers_left--;
|
||||
message[0] = _code_message_invite;
|
||||
message[1] = CODE_MESSAGE_VERSION;
|
||||
memcpy(message + 2, nonce, CODE_NONCE_SIZE);
|
||||
memcpy(message + 2 + CODE_NONCE_SIZE, code.invite, CODE_INVITE_SIZE);
|
||||
sealed[0] = _code_message_invite;
|
||||
sealed[1] = CODE_MESSAGE_VERSION;
|
||||
sealed_size = p2p_seal(code.host_key, message, sizeof(message), sealed + 2);
|
||||
p2p_signal_publish(code.host_topic, sealed, 2 + sealed_size);
|
||||
}
|
||||
|
||||
static void code_invite_received(const unsigned char *payload, int size)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE + 16];
|
||||
int message_size;
|
||||
char hex[2 * CODE_INVITE_SIZE + 1];
|
||||
char link[P2P_LINK_SIZE];
|
||||
|
||||
if (size > (int)sizeof(message) + P2P_SEAL_OVERHEAD)
|
||||
return;
|
||||
message_size = p2p_open(code.look_key, payload, size, message);
|
||||
if (message_size != 2 + CODE_NONCE_SIZE + CODE_INVITE_SIZE || message[0] != _code_message_invite ||
|
||||
message[1] != CODE_MESSAGE_VERSION || !p2p_equal(message + 2, code.look_nonce, CODE_NONCE_SIZE))
|
||||
{
|
||||
return;
|
||||
}
|
||||
code.status = P2P_CODE_FOUND;
|
||||
p2p_signal_sync_topics();
|
||||
p2p_hex(message + 2 + CODE_NONCE_SIZE, CODE_INVITE_SIZE, hex);
|
||||
snprintf(link, sizeof(link), "halo://join/%s", hex);
|
||||
platform_log("Internet play: the join code's host answered");
|
||||
p2p_invite_received(link);
|
||||
}
|
||||
|
||||
int p2p_code_received(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
if (size < 2 || payload[1] != CODE_MESSAGE_VERSION)
|
||||
return 0;
|
||||
if (code.hosting && !strcmp(topic, code.host_topic))
|
||||
{
|
||||
if (payload[0] == _code_message_ask && size == 2 + CODE_NONCE_SIZE)
|
||||
code_answer(payload + 2);
|
||||
return 1;
|
||||
}
|
||||
if (code.status == P2P_CODE_LOOKING && !strcmp(topic, code.look_topic))
|
||||
{
|
||||
if (payload[0] == _code_message_invite)
|
||||
code_invite_received(payload + 2, size - 2);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void p2p_code_update(void)
|
||||
{
|
||||
unsigned char message[2 + CODE_NONCE_SIZE];
|
||||
|
||||
if (code.status != P2P_CODE_LOOKING)
|
||||
return;
|
||||
p2p_signal_start();
|
||||
if (code_elapsed(code.look_started_time, LOOK_TIMEOUT))
|
||||
{
|
||||
code.status = P2P_CODE_NOT_FOUND;
|
||||
p2p_signal_sync_topics();
|
||||
platform_log("Internet play: no game answered the join code");
|
||||
return;
|
||||
}
|
||||
if (!p2p_signal_connected() || (code.asked && !code_elapsed(code.asked_time, ASK_INTERVAL)))
|
||||
return;
|
||||
message[0] = _code_message_ask;
|
||||
message[1] = CODE_MESSAGE_VERSION;
|
||||
memcpy(message + 2, code.look_nonce, CODE_NONCE_SIZE);
|
||||
p2p_signal_publish(code.look_topic, message, sizeof(message));
|
||||
code.asked = 1;
|
||||
code.asked_time = p2p_now();
|
||||
}
|
||||
@@ -169,6 +169,53 @@ int p2p_signal_probe(const unsigned char *host_hash, const unsigned char *token)
|
||||
void p2p_signal_stop_probe(const unsigned char *identifier);
|
||||
/* whether any broker is connected */
|
||||
int p2p_signal_connected(void);
|
||||
/* (for p2p_code.c) the topics every broker listens on, as they are now; and
|
||||
a message to a topic through every broker */
|
||||
void p2p_signal_sync_topics(void);
|
||||
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size);
|
||||
/* the same, kept by the brokers for whoever listens later (empty: no
|
||||
longer) */
|
||||
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size);
|
||||
|
||||
/* ---------- p2p_code.c: join codes (under p2p_lock) */
|
||||
|
||||
enum
|
||||
{
|
||||
P2P_CODE_TOPIC_SIZE = 7 + 32 + 1,
|
||||
};
|
||||
|
||||
/* hosting: a new code for the invite (its key hash and token), and none */
|
||||
void p2p_code_host(const unsigned char *invite);
|
||||
void p2p_code_stop_hosting(void);
|
||||
/* the code hosted, as XXXX-XXXX; 0 if none */
|
||||
int p2p_code_hosted(char *text, int size);
|
||||
/* starts looking up a typed code (0: not a code, or this machine's own;
|
||||
p2p_code_status says which) */
|
||||
int p2p_code_look_up(const char *typed);
|
||||
int p2p_code_status(void);
|
||||
/* forgets a finished look up's result */
|
||||
void p2p_code_clear_status(void);
|
||||
/* (p2p_signal.c) the topics to listen on (empty: none); a message on a
|
||||
topic, nonzero if it was a code's; and each pass of the p2p thread */
|
||||
void p2p_code_topics(const char **host_topic, const char **look_topic);
|
||||
int p2p_code_received(const char *topic, const unsigned char *payload, int size);
|
||||
void p2p_code_update(void);
|
||||
|
||||
/* ---------- p2p_list.c: public games (under p2p_lock) */
|
||||
|
||||
/* hosting: whether the game is listed, and what it is */
|
||||
void p2p_list_set_public(int public_game);
|
||||
int p2p_list_public(void);
|
||||
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
|
||||
int maximum_players);
|
||||
/* the games listed lately; asking keeps this machine listening a while */
|
||||
int p2p_list_games(struct p2p_public_game *games, int maximum);
|
||||
/* (p2p_signal.c) the topic to listen on (empty: none); a message on one
|
||||
(nonzero if it was a listing's); and each pass, whether hosting */
|
||||
const char *p2p_list_topic(void);
|
||||
int p2p_list_received(const char *topic, const unsigned char *payload, int size);
|
||||
void p2p_list_update(int hosting);
|
||||
|
||||
/* every broker's connection made anew at once: after the p2p thread was
|
||||
stopped (the process was suspended, or the machine slept), the old ones
|
||||
may carry nothing, and would be noticed only at their silence timeout */
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
P2P_LIST.C
|
||||
|
||||
Public games (the VR port's): a host may list its game for anyone, who
|
||||
finds it in a list of online games and joins it by its join code
|
||||
(p2p_code.c), without a server of this project's.
|
||||
|
||||
- A public game's host announces it through the signalling brokers
|
||||
(p2p_signal.c) every ANNOUNCE_INTERVAL, retained, on a topic of its own
|
||||
under hcvr/1/g<network version>/: its name, map, game type, players, its
|
||||
join code, and the time. Retained, a machine that starts listening gets
|
||||
every game's latest at once. A host that stops (it closes its game, or
|
||||
makes it private) clears its topic; one that quits without that leaves
|
||||
its last announcement, which listeners drop once it is older than
|
||||
STALE_SECONDS (by the machines' clocks).
|
||||
- A machine that shows the list listens on the topics of its network
|
||||
version (a wildcard), and keeps the games heard of lately.
|
||||
- An announcement carries no address: a machine reaches a public game's
|
||||
host only by its code, as anyone given the code does. Anyone can announce
|
||||
anything, so what is shown is checked to be printable and in bounds, and
|
||||
a game that is not what it says is only a code that does not answer.
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
#include "posix.h"
|
||||
#include "port_config.h"
|
||||
#include "p2p_internal.h"
|
||||
#include "halo_port_limits.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
enum
|
||||
{
|
||||
ANNOUNCEMENT_VERSION = 1,
|
||||
/* milliseconds */
|
||||
ANNOUNCE_INTERVAL = 10000,
|
||||
/* a listener's interest lapses this long after the menu last asked */
|
||||
WATCH_TIME = 3000,
|
||||
STALE_SECONDS = 45,
|
||||
MAXIMUM_LISTED_GAMES = 32,
|
||||
};
|
||||
|
||||
/* what goes to the brokers (byte for byte) */
|
||||
struct announcement
|
||||
{
|
||||
char magic[4];
|
||||
unsigned char version;
|
||||
unsigned char players;
|
||||
unsigned char maximum_players;
|
||||
unsigned char flags;
|
||||
unsigned char time[8];
|
||||
char code[8];
|
||||
char name[P2P_PUBLIC_NAME_SIZE];
|
||||
char map[P2P_PUBLIC_MAP_SIZE];
|
||||
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||
};
|
||||
|
||||
struct listed_game
|
||||
{
|
||||
int used;
|
||||
char topic[P2P_CODE_TOPIC_SIZE];
|
||||
unsigned long heard_time;
|
||||
long long announced_time;
|
||||
struct p2p_public_game game;
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
/* hosting (whether public: network.public_game, until Y changes it) */
|
||||
int initialized;
|
||||
int public_game;
|
||||
char topic[P2P_CODE_TOPIC_SIZE];
|
||||
int announced;
|
||||
unsigned long announced_time;
|
||||
char name[P2P_PUBLIC_NAME_SIZE];
|
||||
char map[P2P_PUBLIC_MAP_SIZE];
|
||||
char mode[P2P_PUBLIC_MODE_SIZE];
|
||||
int players;
|
||||
int maximum_players;
|
||||
|
||||
/* listening */
|
||||
unsigned long watched_time;
|
||||
int watching;
|
||||
char watch_topic[P2P_CODE_TOPIC_SIZE];
|
||||
struct listed_game games[MAXIMUM_LISTED_GAMES];
|
||||
} list;
|
||||
|
||||
static int list_elapsed(unsigned long since, unsigned long time)
|
||||
{
|
||||
return (unsigned int)(p2p_now() - since) >= (unsigned int)time;
|
||||
}
|
||||
|
||||
/* text as shown: printable ASCII only, ended */
|
||||
static void list_text(char *destination, int size, const char *source, int source_size)
|
||||
{
|
||||
int index;
|
||||
|
||||
for (index = 0; index < size - 1 && index < source_size && source[index]; index++)
|
||||
destination[index] = source[index] >= 32 && source[index] < 127 ? source[index] : '?';
|
||||
destination[index] = 0;
|
||||
}
|
||||
|
||||
static long long list_time_now(void)
|
||||
{
|
||||
return (long long)time(NULL);
|
||||
}
|
||||
|
||||
static void list_topics_make(void)
|
||||
{
|
||||
if (!list.topic[0])
|
||||
{
|
||||
unsigned char random[8];
|
||||
char hex[17];
|
||||
|
||||
posix_random_bytes(random, sizeof(random));
|
||||
p2p_hex(random, sizeof(random), hex);
|
||||
snprintf(list.topic, sizeof(list.topic), "hcvr/1/g%d/%s", HALO_PORT_NETWORK_VERSION, hex);
|
||||
}
|
||||
if (!list.watch_topic[0])
|
||||
snprintf(list.watch_topic, sizeof(list.watch_topic), "hcvr/1/g%d/+", HALO_PORT_NETWORK_VERSION);
|
||||
}
|
||||
|
||||
static void list_announce(void)
|
||||
{
|
||||
struct announcement announcement;
|
||||
char code[16];
|
||||
long long now = list_time_now();
|
||||
int index;
|
||||
|
||||
if (!p2p_code_hosted(code, sizeof(code)))
|
||||
return;
|
||||
memset(&announcement, 0, sizeof(announcement));
|
||||
memcpy(announcement.magic, "HCVR", 4);
|
||||
announcement.version = ANNOUNCEMENT_VERSION;
|
||||
announcement.players = (unsigned char)(list.players < 0 ? 0 : list.players > 255 ? 255 : list.players);
|
||||
announcement.maximum_players =
|
||||
(unsigned char)(list.maximum_players < 0 ? 0 : list.maximum_players > 255 ? 255 : list.maximum_players);
|
||||
for (index = 0; index < 8; index++)
|
||||
announcement.time[index] = (unsigned char)(now >> (8 * index));
|
||||
/* (the code without its dash) */
|
||||
memcpy(announcement.code, code, 4);
|
||||
memcpy(announcement.code + 4, code + 5, 4);
|
||||
memcpy(announcement.name, list.name, sizeof(announcement.name));
|
||||
memcpy(announcement.map, list.map, sizeof(announcement.map));
|
||||
memcpy(announcement.mode, list.mode, sizeof(announcement.mode));
|
||||
p2p_signal_publish_retained(list.topic, (const unsigned char *)&announcement, sizeof(announcement));
|
||||
if (!list.announced)
|
||||
platform_log("Internet play: the game is listed as public (%s, %s, %s)", list.name, list.map, list.mode);
|
||||
list.announced = 1;
|
||||
list.announced_time = p2p_now();
|
||||
}
|
||||
|
||||
static void list_withdraw(void)
|
||||
{
|
||||
if (!list.announced)
|
||||
return;
|
||||
p2p_signal_publish_retained(list.topic, NULL, 0);
|
||||
platform_log("Internet play: the game is no longer listed as public");
|
||||
list.announced = 0;
|
||||
}
|
||||
|
||||
/* ---------- p2p.c's side (under p2p_lock) */
|
||||
|
||||
void p2p_list_set_public(int public_game)
|
||||
{
|
||||
if (public_game == list.public_game)
|
||||
return;
|
||||
list.public_game = public_game;
|
||||
list_topics_make();
|
||||
if (!public_game)
|
||||
list_withdraw();
|
||||
else
|
||||
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
|
||||
}
|
||||
|
||||
int p2p_list_public(void)
|
||||
{
|
||||
return list.public_game;
|
||||
}
|
||||
|
||||
void p2p_list_set_description(const char *name, const char *map, const char *mode, int players,
|
||||
int maximum_players)
|
||||
{
|
||||
int changed = strncmp(list.name, name, sizeof(list.name) - 1) || strncmp(list.map, map, sizeof(list.map) - 1) ||
|
||||
strncmp(list.mode, mode, sizeof(list.mode) - 1) || players != list.players ||
|
||||
maximum_players != list.maximum_players;
|
||||
|
||||
list_text(list.name, sizeof(list.name), name, P2P_PUBLIC_NAME_SIZE);
|
||||
list_text(list.map, sizeof(list.map), map, P2P_PUBLIC_MAP_SIZE);
|
||||
list_text(list.mode, sizeof(list.mode), mode, P2P_PUBLIC_MODE_SIZE);
|
||||
list.players = players;
|
||||
list.maximum_players = maximum_players;
|
||||
/* (a change goes out soon, not at the next interval) */
|
||||
if (changed && list.announced && list_elapsed(list.announced_time, 1000))
|
||||
list.announced_time = p2p_now() - ANNOUNCE_INTERVAL;
|
||||
}
|
||||
|
||||
int p2p_list_games(struct p2p_public_game *games, int maximum)
|
||||
{
|
||||
int count = 0;
|
||||
int index;
|
||||
|
||||
list.watched_time = p2p_now();
|
||||
list.watching = 1;
|
||||
for (index = 0; index < MAXIMUM_LISTED_GAMES && count < maximum; index++)
|
||||
{
|
||||
if (list.games[index].used)
|
||||
games[count++] = list.games[index].game;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ---------- p2p_signal.c's side (under p2p_lock) */
|
||||
|
||||
const char *p2p_list_topic(void)
|
||||
{
|
||||
return list.watching ? list.watch_topic : "";
|
||||
}
|
||||
|
||||
int p2p_list_received(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
struct announcement announcement;
|
||||
struct listed_game *slot = NULL;
|
||||
long long announced = 0;
|
||||
char code[16];
|
||||
int index;
|
||||
|
||||
if (!list.watch_topic[0] || strncmp(topic, list.watch_topic, strlen(list.watch_topic) - 1))
|
||||
return 0;
|
||||
/* (this machine's own, as the broker echoes it) */
|
||||
if (!strcmp(topic, list.topic))
|
||||
return 1;
|
||||
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
|
||||
{
|
||||
if (list.games[index].used && !strcmp(list.games[index].topic, topic))
|
||||
slot = &list.games[index];
|
||||
}
|
||||
if (size != (int)sizeof(announcement))
|
||||
{
|
||||
/* (empty: withdrawn) */
|
||||
if (slot && size == 0)
|
||||
slot->used = 0;
|
||||
return 1;
|
||||
}
|
||||
memcpy(&announcement, payload, sizeof(announcement));
|
||||
if (memcmp(announcement.magic, "HCVR", 4) || announcement.version != ANNOUNCEMENT_VERSION ||
|
||||
!announcement.maximum_players || announcement.players > announcement.maximum_players)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
for (index = 0; index < 8; index++)
|
||||
announced |= (long long)announcement.time[index] << (8 * index);
|
||||
if (announced < list_time_now() - STALE_SECONDS || announced > list_time_now() + STALE_SECONDS)
|
||||
{
|
||||
if (slot)
|
||||
slot->used = 0;
|
||||
return 1;
|
||||
}
|
||||
snprintf(code, sizeof(code), "%.4s-%.4s", announcement.code, announcement.code + 4);
|
||||
for (index = 0; code[index]; index++)
|
||||
{
|
||||
if (index != 4 && !((code[index] >= '0' && code[index] <= '9') || (code[index] >= 'A' && code[index] <= 'Z')))
|
||||
return 1;
|
||||
}
|
||||
if (!slot)
|
||||
{
|
||||
int oldest = -1;
|
||||
|
||||
for (index = 0; index < MAXIMUM_LISTED_GAMES && !slot; index++)
|
||||
{
|
||||
if (!list.games[index].used)
|
||||
slot = &list.games[index];
|
||||
else if (oldest < 0 || list.games[index].announced_time < list.games[oldest].announced_time)
|
||||
oldest = index;
|
||||
}
|
||||
if (!slot)
|
||||
slot = &list.games[oldest];
|
||||
memset(slot, 0, sizeof(*slot));
|
||||
snprintf(slot->topic, sizeof(slot->topic), "%s", topic);
|
||||
}
|
||||
slot->used = 1;
|
||||
slot->heard_time = p2p_now();
|
||||
slot->announced_time = announced;
|
||||
list_text(slot->game.name, sizeof(slot->game.name), announcement.name, sizeof(announcement.name));
|
||||
list_text(slot->game.map, sizeof(slot->game.map), announcement.map, sizeof(announcement.map));
|
||||
list_text(slot->game.mode, sizeof(slot->game.mode), announcement.mode, sizeof(announcement.mode));
|
||||
snprintf(slot->game.code, sizeof(slot->game.code), "%s", code);
|
||||
slot->game.players = announcement.players;
|
||||
slot->game.maximum_players = announcement.maximum_players;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void p2p_list_update(int hosting)
|
||||
{
|
||||
int index;
|
||||
|
||||
if (!list.initialized)
|
||||
{
|
||||
list.initialized = 1;
|
||||
if (!list.public_game)
|
||||
list.public_game = config_boolean("network.public_game");
|
||||
}
|
||||
list_topics_make();
|
||||
/* hosting a public game: announced every so often; withdrawn when it
|
||||
is not */
|
||||
if (hosting && list.public_game)
|
||||
{
|
||||
p2p_signal_start();
|
||||
if (p2p_signal_connected() && list_elapsed(list.announced_time, ANNOUNCE_INTERVAL))
|
||||
list_announce();
|
||||
}
|
||||
else if (list.announced)
|
||||
{
|
||||
list_withdraw();
|
||||
}
|
||||
/* listening while the menu asks */
|
||||
if (list.watching && list_elapsed(list.watched_time, WATCH_TIME))
|
||||
{
|
||||
list.watching = 0;
|
||||
memset(list.games, 0, sizeof(list.games));
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
else if (list.watching)
|
||||
{
|
||||
p2p_signal_start();
|
||||
p2p_signal_sync_topics();
|
||||
}
|
||||
for (index = 0; index < MAXIMUM_LISTED_GAMES; index++)
|
||||
{
|
||||
if (list.games[index].used && list.games[index].announced_time < list_time_now() - STALE_SECONDS)
|
||||
list.games[index].used = 0;
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,11 @@ enum
|
||||
_topic_own_slot,
|
||||
_topic_query,
|
||||
_topic_slots,
|
||||
/* the VR port's: a hosted join code's, a looked up one's, the public
|
||||
games' (p2p_code.c, p2p_list.c) */
|
||||
_topic_code_host,
|
||||
_topic_code_look,
|
||||
_topic_list,
|
||||
_topic_probe,
|
||||
/* the topics a broker is subscribed to */
|
||||
NUMBER_OF_TOPICS = _topic_probe + P2P_MAXIMUM_PROBES,
|
||||
@@ -700,6 +705,9 @@ static void broker_sync_topics(struct broker *broker)
|
||||
wanted[_topic_own_slot] = lobby && signalling.lobby_listed ? signalling.own_slot : "";
|
||||
wanted[_topic_query] = lobby && signalling.lobby_listed ? P2P_LOBBY_QUERY_TOPIC : "";
|
||||
wanted[_topic_slots] = lobby && signalling.lobby_browsing ? P2P_LOBBY_SLOT_PREFIX "+" : "";
|
||||
/* (the VR port's join codes and public games: p2p_code.c, p2p_list.c) */
|
||||
p2p_code_topics(&wanted[_topic_code_host], &wanted[_topic_code_look]);
|
||||
wanted[_topic_list] = p2p_list_topic();
|
||||
for (index = 0; index < P2P_MAXIMUM_PROBES; index++)
|
||||
wanted[_topic_probe + index] = signalling.probes[index].join.active ? signalling.probes[index].topic : "";
|
||||
for (index = 0; index < NUMBER_OF_TOPICS; index++)
|
||||
@@ -1468,6 +1476,8 @@ static void publish_received(struct broker *broker, const char *topic, const uns
|
||||
|
||||
if (size > MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
|
||||
return;
|
||||
if (p2p_code_received(topic, payload, size) || p2p_list_received(topic, payload, size))
|
||||
return;
|
||||
if (!strncmp(topic, P2P_LOBBY_SLOT_PREFIX, sizeof(P2P_LOBBY_SLOT_PREFIX) - 1))
|
||||
{
|
||||
p2p_lobby_slot_heard(topic + sizeof(P2P_LOBBY_SLOT_PREFIX) - 1, payload, size, retained);
|
||||
@@ -1969,6 +1979,8 @@ void p2p_signal_update(const int *read, int read_count, const int *write, int wr
|
||||
if (elapsed(join->sent_time, JOIN_INTERVAL))
|
||||
send_join(join);
|
||||
}
|
||||
p2p_code_update();
|
||||
p2p_list_update(signalling.hosting);
|
||||
}
|
||||
|
||||
int p2p_signal_connected(void)
|
||||
@@ -2008,6 +2020,42 @@ static void sync_all_topics(void)
|
||||
broker_sync_topics(&signalling.brokers[index]);
|
||||
}
|
||||
|
||||
void p2p_signal_sync_topics(void)
|
||||
{
|
||||
sync_all_topics();
|
||||
}
|
||||
|
||||
void p2p_signal_publish(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
if (size <= MAXIMUM_MESSAGE_SIZE + P2P_SEAL_OVERHEAD)
|
||||
publish_everywhere(topic, payload, size);
|
||||
}
|
||||
|
||||
void p2p_signal_publish_retained(const char *topic, const unsigned char *payload, int size)
|
||||
{
|
||||
unsigned char body[3 + TOPIC_SIZE + MAXIMUM_MESSAGE_SIZE];
|
||||
int index;
|
||||
|
||||
if (size > MAXIMUM_MESSAGE_SIZE)
|
||||
return;
|
||||
for (index = 0; index < signalling.broker_count; index++)
|
||||
{
|
||||
struct broker *broker = &signalling.brokers[index];
|
||||
int body_size;
|
||||
|
||||
if (broker->state != _broker_ready)
|
||||
continue;
|
||||
body_size = put_string(body, topic);
|
||||
/* (MQTT 5: no properties) */
|
||||
if (broker->protocol == 5)
|
||||
body[body_size++] = 0;
|
||||
if (size)
|
||||
memcpy(body + body_size, payload, (size_t)size);
|
||||
/* (PUBLISH, at most once, retained: an empty one clears it) */
|
||||
broker_send(broker, 0x31, body, body_size + size);
|
||||
}
|
||||
}
|
||||
|
||||
void p2p_signal_host(const unsigned char *token)
|
||||
{
|
||||
if (!signalling.has_nonce_key)
|
||||
|
||||
@@ -21,6 +21,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>
|
||||
@@ -215,6 +216,258 @@ static const struct config_setting config_settings[] =
|
||||
"pixel by the lights the game gives them, without the facets the light\n"
|
||||
"of each vertex shows across curved surfaces; false lights each vertex,\n"
|
||||
"as the Xbox does." },
|
||||
{ "display.shader_cache", _config_boolean, "true", "HALO_SHADER_CACHE", _environment_value, _platform_android,
|
||||
"Keep the compiled shader programs in the shader_cache folder beside this\n"
|
||||
"file, and make ready those a map drew with while it loads: fewer\n"
|
||||
"stalls the first time something is drawn. false compiles each the first\n"
|
||||
"time it is drawn. The folder may be deleted at any time." },
|
||||
{ "display.shader_worker", _config_boolean, "true", "HALO_SHADER_WORKER", _environment_value, _platform_android,
|
||||
"Make the shader programs on a thread of their own (with a GL context\n"
|
||||
"sharing the game's), from the shader cache or compiled: a draw whose\n"
|
||||
"program is not made yet is left out for the frames it takes, and the\n"
|
||||
"frame does not wait. false makes each on the game's thread when it is\n"
|
||||
"first drawn with. Read when the app starts." },
|
||||
|
||||
{ "vr.enabled", _config_boolean, "true", "HALO_VR_ENABLED", _environment_value, _platform_android,
|
||||
"On a VR headset (Quest, Steam Frame), play in VR; false shows the game\n"
|
||||
"on a flat panel instead. Read when the app starts." },
|
||||
{ "vr.menus", _config_string, "\"pc\"", "HALO_VR_MENUS", _environment_value, _platform_android,
|
||||
"The menus in VR: \"pc\" for the PC version's (as display.menus), \"xbox\"\n"
|
||||
"for the Xbox's. Read when the menus' map loads." },
|
||||
{ "vr.resolution_scale", _config_real, "1.0", "HALO_VR_RESOLUTION_SCALE", _environment_value, _platform_android,
|
||||
"The eye images' size as a multiple of what the headset recommends;\n"
|
||||
"lower is faster. Read when the app starts." },
|
||||
{ "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.refresh_rate_auto", _config_boolean, "true", "HALO_VR_REFRESH_RATE_AUTO", _environment_value,
|
||||
_platform_android,
|
||||
"While frames come late at that rate (big online games), step down to the\n"
|
||||
"next the headset has (90 to 72 Hz), and back up once they would keep up\n"
|
||||
"again; the headset shows a late frame twice, a doubled picture when\n"
|
||||
"turning. Only where the headset lets the game choose (not SteamVR)." },
|
||||
{ "vr.graphics", _config_string, "\"medium\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
|
||||
"Which of the costlier effects are drawn (\"medium\" by default): \"auto\" all of them while the\n"
|
||||
"GPU keeps up, leaving out one after another (the shield's glow, object\n"
|
||||
"shadows, reflections, specular light, lens flares, bump maps, the rest\n"
|
||||
"of \"low\") while it does not, and bringing them back when it does again\n"
|
||||
"(the Quest; on the Steam Frame, which does not tell the GPU's time, as\n"
|
||||
"\"high\"); \"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.foveation", _config_string, "\"off\"", "HALO_VR_FOVEATION", _environment_value, _platform_android,
|
||||
"Foveated rendering on the Quest (GL_QCOM_texture_foveated): the eyes'\n"
|
||||
"pictures drawn at full detail around the lenses' centres and coarser\n"
|
||||
"towards their edges, for less GPU time. \"off\", \"low\" or \"medium\"\n"
|
||||
"(coarser edges, nearer the centre). The HUD and menus are not\n"
|
||||
"foveated; a headset without it ignores it." },
|
||||
{ "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"
|
||||
"when you recentre the headset (hold the Meta button)." },
|
||||
{ "vr.lean_limit", _config_real, "0.35", "HALO_VR_LEAN_LIMIT", _environment_value, _platform_android,
|
||||
"How far, in metres, the view follows your head sideways or forwards\n"
|
||||
"away from the player's body (walls are not checked)." },
|
||||
{ "vr.crouch_limit", _config_real, "0.9", "HALO_VR_CROUCH_LIMIT", _environment_value, _platform_android,
|
||||
"How far, in metres, the view follows your head down." },
|
||||
{ "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android,
|
||||
"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 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), \"free_grip\" (the free hand's, held a\n"
|
||||
"moment, when it took neither the weapon's foregrip nor the Warthog's\n"
|
||||
"wheel), 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,
|
||||
"melee" },
|
||||
{ "vr.button_talk", _config_string, "\"free_grip\"", "HALO_VR_BUTTON_TALK", _environment_value,
|
||||
_platform_android,
|
||||
"and voice chat's push to talk (audio.voice_chat). The triggers and the\n"
|
||||
"menu button keep 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,
|
||||
"The weapon's crosshair where it aims, at the distance of what it aims at\n"
|
||||
"(with aim_with = \"controller\"); false leaves it out." },
|
||||
{ "vr.weapon_hand_forward", _config_real, "0.0", "HALO_VR_WEAPON_HAND_FORWARD", _environment_value,
|
||||
_platform_android,
|
||||
"The weapon in your hand: each one's model is held so that its hand is\n"
|
||||
"where yours is, as the free hand's; this moves your hand on it, in\n"
|
||||
"metres (weapon_hand_right: to its right, weapon_hand_down: down)." },
|
||||
{ "vr.weapon_hand_right", _config_real, "0.0", "HALO_VR_WEAPON_HAND_RIGHT", _environment_value,
|
||||
_platform_android,
|
||||
"See weapon_hand_forward." },
|
||||
{ "vr.weapon_hand_down", _config_real, "0.0", "HALO_VR_WEAPON_HAND_DOWN", _environment_value,
|
||||
_platform_android,
|
||||
"See weapon_hand_forward." },
|
||||
{ "vr.two_hand_reach", _config_real, "0.2", "HALO_VR_TWO_HAND_REACH", _environment_value, _platform_android,
|
||||
"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"
|
||||
"reaching from the shoulders to the controllers; \"full\", the whole\n"
|
||||
"body, its torso, hips and legs too, under the head and following the\n"
|
||||
"physical crouch and lean, the feet stepping after it; \"hands\", the\n"
|
||||
"hands alone; \"off\", neither (the weapon alone)." },
|
||||
{ "vr.fingers", _config_boolean, "true", "HALO_VR_FINGERS", _environment_value, _platform_android,
|
||||
"The free hand's fingers follow the controller: the index finger the\n"
|
||||
"trigger, the others the grip, the thumb where it rests (on the Quest's\n"
|
||||
"and the Steam Frame's controllers); false leaves the hand as the\n"
|
||||
"weapon's animation has it." },
|
||||
{ "vr.arm_ik", _config_boolean, "true", "HALO_VR_ARM_IK", _environment_value, _platform_android,
|
||||
"The free hand's arm bends at the elbow to reach it from the shoulder;\n"
|
||||
"false keeps the arm's shape as on the weapon, pointed at the shoulder." },
|
||||
{ "vr.melee_swing_speed", _config_real, "2.0", "HALO_VR_MELEE_SWING", _environment_value, _platform_android,
|
||||
"Thrusting a hand forward faster than this, in metres per second, is a\n"
|
||||
"melee attack (the weapon hand's grip is too); 0 turns it off." },
|
||||
{ "vr.grenade_throw_speed", _config_real, "1.5", "HALO_VR_GRENADE_THROW", _environment_value, _platform_android,
|
||||
"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 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 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,
|
||||
"down the drawn hand's knuckles, from the index finger to the little one,", },
|
||||
{ "vr.grenade_back", _config_real, "0.02", "HALO_VR_GRENADE_BACK", _environment_value, _platform_android,
|
||||
"and back along its fingers, towards the wrist." },
|
||||
{ "vr.vehicle_first_person", _config_boolean, "true", "HALO_VR_VEHICLE_FIRST_PERSON", _environment_value,
|
||||
_platform_android,
|
||||
"In a vehicle, the view stays behind the player's own eyes, and turns\n"
|
||||
"with the vehicle (false: the game's camera behind it, as on a screen)." },
|
||||
{ "vr.turret_handles", _config_boolean, "true", "HALO_VR_TURRET_HANDLES", _environment_value,
|
||||
_platform_android,
|
||||
"At a vehicle's gun, the right controller works it as by its handles: tipped\n"
|
||||
"down, the barrel rises (false: the barrel points where the controller does)." },
|
||||
{ "vr.ghost_eye_raise", _config_real, "0.45", "HALO_VR_GHOST_EYE_RAISE", _environment_value, _platform_android,
|
||||
"In a Ghost, whose hood arches over its rider, the eyes this many metres\n"
|
||||
"above his head," },
|
||||
{ "vr.banshee_eye_raise", _config_real, "1.0", "HALO_VR_BANSHEE_EYE_RAISE", _environment_value,
|
||||
_platform_android,
|
||||
"in a Banshee, whose pilot lies in it, this many," },
|
||||
{ "vr.turret_eye_raise", _config_real, "0.5", "HALO_VR_TURRET_EYE_RAISE", _environment_value,
|
||||
_platform_android,
|
||||
"at a Covenant gun, whose shield is at the eyes, this many," },
|
||||
{ "vr.tank_eye_raise", _config_real, "0.5", "HALO_VR_TANK_EYE_RAISE", _environment_value, _platform_android,
|
||||
"and driving a Scorpion, whose driver sits in the hull, this many." },
|
||||
{ "vr.driver_camera_forward", _config_real, "0.15", "HALO_VR_DRIVER_CAMERA_FORWARD", _environment_value,
|
||||
_platform_android,
|
||||
"At an open vehicle's wheel (a Warthog's), the view this many metres forward\n"
|
||||
"of the driver's head, nearer the windscreen." },
|
||||
{ "vr.gunner_camera_back", _config_real, "0.15", "HALO_VR_GUNNER_CAMERA_BACK", _environment_value,
|
||||
_platform_android,
|
||||
"At a vehicle's gun, the view this many metres back from where the\n"
|
||||
"gunner's head leans into its sights." },
|
||||
{ "vr.glass_strength", _config_real, "0.5", "HALO_VR_GLASS_STRENGTH", _environment_value, _platform_android,
|
||||
"How strongly glass tints and reflects (1 as the game has it): up close, as\n"
|
||||
"through a vehicle's windscreen, it hides the view." },
|
||||
{ "vr.vehicle_steer_degrees", _config_real, "60", "HALO_VR_VEHICLE_STEER", _environment_value, _platform_android,
|
||||
"Driving, the right stick pushed fully aside steers this many degrees off\n"
|
||||
"the vehicle's heading (the game turns the vehicle towards it)." },
|
||||
{ "vr.vehicle_gun_steer_degrees", _config_real, "40", "HALO_VR_VEHICLE_GUN_STEER", _environment_value,
|
||||
_platform_android,
|
||||
"The same for a vehicle whose steering aims its guns too (a Ghost, a Banshee)." },
|
||||
{ "vr.vehicle_steer_curve", _config_real, "2.0", "HALO_VR_VEHICLE_STEER_CURVE", _environment_value,
|
||||
_platform_android,
|
||||
"Driving, the right stick's deflection to this power steers (1 evenly;\n"
|
||||
"above it, finely near the middle, fully at the end)," },
|
||||
{ "vr.vehicle_steer_smoothing", _config_real, "0.12", "HALO_VR_VEHICLE_STEER_SMOOTHING", _environment_value,
|
||||
_platform_android,
|
||||
"eased in over about this many seconds (0: at once)." },
|
||||
{ "vr.vehicle_pitch_degrees", _config_real, "45", "HALO_VR_VEHICLE_PITCH", _environment_value, _platform_android,
|
||||
"Driving, the right stick pushed fully up or down aims this many degrees\n"
|
||||
"above or below the horizon (a Banshee climbs or dives)." },
|
||||
{ "vr.vehicle_wheel", _config_boolean, "true", "HALO_VR_VEHICLE_WHEEL", _environment_value, _platform_android,
|
||||
"At an open vehicle's wheel (a Warthog's), grip it with one hand or both and\n"
|
||||
"turn it to steer, the right trigger the throttle and the left the brake;\n"
|
||||
"let go, and the right stick steers." },
|
||||
{ "vr.vehicle_wheel_degrees", _config_real, "90", "HALO_VR_VEHICLE_WHEEL_DEGREES", _environment_value,
|
||||
_platform_android,
|
||||
"The wheel turned this many degrees either way steers fully." },
|
||||
{ "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"
|
||||
"it towards you to read it), instead of in the HUD." },
|
||||
{ "vr.motion_tracker_size", _config_real, "0.11", "HALO_VR_MOTION_TRACKER_SIZE", _environment_value, _platform_android,
|
||||
"How wide the motion tracker on the wrist is, in metres." },
|
||||
{ "vr.motion_tracker_turn", _config_real, "32.0", "HALO_VR_MOTION_TRACKER_TURN", _environment_value, _platform_android,
|
||||
"Turns the motion tracker on the wrist, in degrees clockwise as you read it." },
|
||||
{ "vr.motion_tracker_right", _config_real, "0.015", "HALO_VR_MOTION_TRACKER_RIGHT", _environment_value,
|
||||
_platform_android,
|
||||
"Moves the motion tracker on the wrist right as you read it, in metres." },
|
||||
{ "vr.motion_tracker_down", _config_real, "0.055", "HALO_VR_MOTION_TRACKER_DOWN", _environment_value,
|
||||
_platform_android,
|
||||
"Moves the motion tracker on the wrist down as you read it, in metres." },
|
||||
{ "vr.scope_size", _config_real, "0.10", "HALO_VR_SCOPE_SIZE", _environment_value, _platform_android,
|
||||
"Zoomed in, the scope's view shows on a disc over the weapon this wide, in\n"
|
||||
"metres; 0 turns it off." },
|
||||
{ "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, 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\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)." },
|
||||
{ "vr.hud_distance", _config_real, "1.5", "HALO_VR_HUD_DISTANCE", _environment_value, _platform_android,
|
||||
"How far in front of the eyes the HUD floats, in metres." },
|
||||
{ "vr.hud_field_of_view", _config_real, "62.0", "HALO_VR_HUD_FOV", _environment_value, _platform_android,
|
||||
"How wide the HUD is, in degrees across." },
|
||||
{ "vr.screen_distance", _config_real, "2.5", "HALO_VR_SCREEN_DISTANCE", _environment_value, _platform_android,
|
||||
"How far away the screen for menus, loading and cinematics stands, in metres." },
|
||||
{ "vr.screen_width", _config_real, "3.2", "HALO_VR_SCREEN_WIDTH", _environment_value, _platform_android,
|
||||
"That screen's width in metres (its height is three quarters of it)." },
|
||||
{ "display.structure_culling", _config_integer, "31", "HALO_STRUCTURE_CULLING", _environment_value,
|
||||
_platform_all,
|
||||
"Which tests skip the level's triangles the camera cannot see, as bits\n"
|
||||
@@ -420,14 +673,16 @@ static const struct config_setting config_settings[] =
|
||||
"as CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER. Their tags are checked\n"
|
||||
"as the game's own maps' are before they run; false refuses them\n"
|
||||
"(docs/custom_edition_caches.md)." },
|
||||
{ "bots.count", _config_integer, "0", "HALO_BOTS", _environment_value, _platform_all,
|
||||
/* (the desktop's local bots, bots.c; Mjolnir VR's host bots have their own
|
||||
bots.count and bots.skill, Android's, below: host_bots.c) */
|
||||
{ "bots.count", _config_integer, "0", "HALO_BOTS", _environment_value, _platform_desktop,
|
||||
"Computer players (bots) in a local (split screen) Slayer game: 0 to 15\n"
|
||||
"join its lobby with the players, as far as the game has room. Not in\n"
|
||||
"system link or internet games. 0: none, as in the stock game." },
|
||||
{ "bots.skill", _config_string, "\"normal\"", "HALO_BOT_SKILL", _environment_value, _platform_all,
|
||||
{ "bots.skill", _config_string, "\"normal\"", "HALO_BOT_SKILL", _environment_value, _platform_desktop,
|
||||
"How well the bots play: \"easy\", \"normal\", \"heroic\" or \"legendary\"\n"
|
||||
"(how soon they react, how well they aim, how far they see)." },
|
||||
{ "bots.teams", _config_string, "\"even\"", "HALO_BOT_TEAMS", _environment_value, _platform_all,
|
||||
{ "bots.teams", _config_string, "\"even\"", "HALO_BOT_TEAMS", _environment_value, _platform_desktop,
|
||||
"In team games: \"even\" puts the bots on both teams, evening them with\n"
|
||||
"the players; \"against\" puts them all on the team the players are not on." },
|
||||
{ "display.viewmodel_shield", _config_boolean, "false", "HALO_VIEWMODEL_SHIELD", _environment_value, _platform_all,
|
||||
@@ -505,6 +760,9 @@ static const struct config_setting config_settings[] =
|
||||
"clipboard) that lets whoever has it join over the internet; opening a\n"
|
||||
"link (or copying one before switching to the game) joins. Only people\n"
|
||||
"with the invite can join. Off keeps system link to the local network." },
|
||||
{ "network.public_game", _config_boolean, "false", "HALO_NET_PUBLIC_GAME", _environment_value, _platform_all,
|
||||
"Host games as public: listed for anyone among the online games (the\n"
|
||||
"system link screen's X), as Y in a hosted game's lobby makes them." },
|
||||
{ "network.join_from_clipboard", _config_boolean, "true", "HALO_NET_JOIN_FROM_CLIPBOARD", _environment_value,
|
||||
_platform_all,
|
||||
"Join the game of an invite link found on the clipboard when the game\n"
|
||||
@@ -705,6 +963,12 @@ static const struct config_setting config_settings[] =
|
||||
"The Discord application internet play invites go through while the\n"
|
||||
"Discord desktop client runs; empty for none." },
|
||||
|
||||
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
|
||||
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
|
||||
"players it drives itself, which every machine sees as players. Set in\n"
|
||||
"the lobby's host options (Y)." },
|
||||
{ "bots.skill", _config_integer, "1", "HALO_BOTS_SKILL", _environment_value, _platform_android,
|
||||
"How well the bots play: 0 easy, 1 normal, 2 hard." },
|
||||
{ "update.auto", _config_boolean, "true", "HALO_UPDATE_AUTO", _environment_value, _platform_all,
|
||||
"Look for a new version when the game starts, and offer to update to it;\n"
|
||||
"false never looks (the game's \"Do not ask again\" writes false here)." },
|
||||
@@ -967,6 +1231,47 @@ static const struct config_setting config_settings[] =
|
||||
"machines of twice it), to test the netcode as over the internet; 0 none." },
|
||||
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
|
||||
"Percent of datagrams received that are dropped, for the same; 0 none." },
|
||||
{ "debug.multiview", _config_boolean, "false", "HALO_MULTIVIEW", _environment_value, _platform_android,
|
||||
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
|
||||
"half the drawing's cost on the processor, but the eyes' pictures did not\n"
|
||||
"always fuse (doubled as the head turned: on the Steam Frame now and then,\n"
|
||||
"on the Quest 3 in big online games). Off, one eye after the other; on,\n"
|
||||
"for finding why." },
|
||||
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
|
||||
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
|
||||
"driver chooses. Read when the app starts." },
|
||||
{ "debug.xr_deferred_copies", _config_integer, "-1", "HALO_XR_DEFERRED", _environment_value, _platform_android,
|
||||
"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_graphics_measure", _config_boolean, "false", "HALO_VR_GRAPHICS_MEASURE", _environment_value,
|
||||
_platform_android,
|
||||
"VR: leave each costlier effect out in turn (a second and a half drawn,\n"
|
||||
"as long left out) and log what the GPU's time says it costs\n"
|
||||
"(\"VR graphics measure\"); vr.graphics is ignored meanwhile." },
|
||||
{ "debug.shader_warm_up", _config_string, "\"worker\"", "HALO_SHADER_WARM_UP", _environment_value,
|
||||
_platform_android,
|
||||
"Each shader program made on the worker (display.shader_worker) is drawn\n"
|
||||
"with once there, into a small target of its own, so that the driver\n"
|
||||
"makes what it makes at a first draw then and not in a frame: \"worker\";\n"
|
||||
"\"load\" draws with a map's programs on the game's thread as the map\n"
|
||||
"loads instead; \"off\" draws with none. 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.vr_body", _config_string, "\"off\"", "HALO_DEBUG_VR_BODY", _environment_value, _platform_android,
|
||||
"Off the headset (flat), the player's own body drawn as vr.body \"full\"\n"
|
||||
"draws it, posed from the flat camera, a test aid: \"eye\" (seen from the\n"
|
||||
"eyes), \"down\" (looking down, as in the headset), \"front\", \"side\" or\n"
|
||||
"\"back\" (the camera moved to look at it); after a colon how the head\n"
|
||||
"moves: \"cycle\" (turning, crouching, leaning, five seconds each; the\n"
|
||||
"default), \"still\", \"turn\", \"crouch\", \"lean\", or \"raw\" (the game's\n"
|
||||
"own pose, only the head and arms shrunk). \"off\" (default): none. Read\n"
|
||||
"once." },
|
||||
{ "debug.network_corrupt", _config_real, "0.0", "HALO_NETWORK_CORRUPT", _environment_value, _platform_all,
|
||||
"Percent of the datagrams received that are damaged at random, to test\n"
|
||||
"that nothing a machine sends can crash the game; 0 none." },
|
||||
@@ -1039,6 +1344,11 @@ static const struct config_setting config_settings[] =
|
||||
"Log texture uploads." },
|
||||
{ "debug.texture_no_cache", _config_boolean, "false", "HALO_TEXTURE_NO_CACHE", _environment_set_is_true, _platform_all,
|
||||
"Upload textures again every time they are used." },
|
||||
{ "debug.vehicle_spawn", _config_boolean, "true", "HALO_VEHICLE_SPAWN", _environment_value, _platform_android,
|
||||
"A test aid in VR: both thumbsticks held in for a second and a half put\n"
|
||||
"the map's next vehicle a few metres ahead." },
|
||||
{ "debug.audio_log", _config_boolean, "false", "HALO_AUDIO_LOG", _environment_set_is_true, _platform_all,
|
||||
"Log the level of the mixed sound every five seconds." },
|
||||
{ "debug.sample_seconds", _config_real, "0.0", "HALO_SAMPLE", _environment_value, _platform_android,
|
||||
"Log where every game thread is this often, in seconds (read by the\n"
|
||||
"app, port/android/host/host_debug.c); 0 never." },
|
||||
@@ -1198,13 +1508,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_ARM64_GUEST
|
||||
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;
|
||||
#elif !defined(HALO_ANDROID) && !defined(HALO_WEB)
|
||||
/* (a dedicated server's holds its passwords: its user's to read alone) */
|
||||
if (!SDL_SaveFile(path, text, strlen(text)))
|
||||
@@ -1611,13 +1932,21 @@ static volatile unsigned long config_change_count;
|
||||
static long config_setting_index(const char *name)
|
||||
{
|
||||
size_t index;
|
||||
long other = -1;
|
||||
|
||||
/* (port: this platform's, where two of a name are each another's: the
|
||||
bots') */
|
||||
for (index = 0; index < NUMBER_OF_CONFIG_SETTINGS; index++)
|
||||
{
|
||||
if (!strcmp(config_settings[index].name, name))
|
||||
return (long)index;
|
||||
{
|
||||
if (config_settings[index].platforms & CONFIG_PLATFORM)
|
||||
return (long)index;
|
||||
if (other < 0)
|
||||
other = (long)index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return other;
|
||||
}
|
||||
|
||||
/* a key in the file that is no setting, likely misspelt or in the wrong
|
||||
@@ -2461,6 +2790,15 @@ void config_folder(char *path, size_t size)
|
||||
snprintf(path, size, "%s", file);
|
||||
}
|
||||
|
||||
int config_write_integer(const char *name, long value)
|
||||
{
|
||||
long index = config_setting_index(name);
|
||||
char text[32];
|
||||
|
||||
snprintf(text, sizeof(text), "%ld", value);
|
||||
return index >= 0 && config_settings[index].type == _config_integer && config_write(name, text);
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
char *config_file_read(const char *path, size_t *size)
|
||||
|
||||
@@ -24,6 +24,7 @@ const char *config_string(const char *name);
|
||||
writes it into config.toml (only its line changes); 1 on success */
|
||||
int config_write(const char *name, const char *value);
|
||||
int config_write_boolean(const char *name, int value);
|
||||
int config_write_integer(const char *name, long value);
|
||||
/* a setting's value as text ("true", "60", "1.5", "all"); 0 if there is no
|
||||
such setting */
|
||||
int config_text(const char *name, char *text, size_t size);
|
||||
|
||||
@@ -11,6 +11,7 @@ and the debug keyboard that the game's console reads.
|
||||
|
||||
#include "platform.h"
|
||||
#include "sdl_platform.h"
|
||||
#include "halo_vr.h"
|
||||
#include "gl.h"
|
||||
#include "port_config.h"
|
||||
#include "p2p.h"
|
||||
@@ -1287,6 +1288,11 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
|
||||
(void)version;
|
||||
platform_event_thread = SDL_GetCurrentThreadID();
|
||||
platform_log("OpenGL %s on %s", (const char *)glGetString(GL_VERSION), (const char *)glGetString(GL_RENDERER));
|
||||
/* on a headset, its session on this context (vr.c); the headset then
|
||||
paces the frames, and the window is not shown */
|
||||
halo_vr_initialize();
|
||||
if (halo_vr_active())
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
#if defined(__linux__) && !defined(HALO_ANDROID)
|
||||
{
|
||||
const char *renderer = (const char *)glGetString(GL_RENDERER);
|
||||
@@ -1659,6 +1665,37 @@ static BOOL platform_text_has_invite_link(const char *text)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the last clipboard text looked at, kept beside the settings from run to
|
||||
run (the VR port's): a run's own invite, still on the clipboard when the
|
||||
game starts again, is not joined then (its host, the run before, is gone,
|
||||
and the attempt held up a join for a minute and a half) */
|
||||
static void platform_clipboard_seen(char *seen, int size, BOOL save)
|
||||
{
|
||||
char path[512];
|
||||
FILE *file;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/clipboard_seen.txt", platform_data_root());
|
||||
if (save)
|
||||
{
|
||||
file = fopen(path, "wb");
|
||||
if (file)
|
||||
{
|
||||
fputs(seen, file);
|
||||
fclose(file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
file = fopen(path, "rb");
|
||||
if (!file)
|
||||
return;
|
||||
{
|
||||
size_t length = fread(seen, 1, (size_t)size - 1, file);
|
||||
|
||||
seen[length] = 0;
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
/* the clipboard's text (the menus' text fields' Ctrl+V), and text put on it
|
||||
(the server settings' invite link); 0 if there is none. The main thread's */
|
||||
int platform_clipboard_get(char *text, int size)
|
||||
@@ -1682,8 +1719,14 @@ static void platform_invite_clipboard(BOOL look)
|
||||
{
|
||||
/* the last clipboard text looked at, so each invite is joined once */
|
||||
static char seen[256];
|
||||
static BOOL seen_loaded;
|
||||
const char *invite = p2p_take_clipboard_text();
|
||||
|
||||
if (!seen_loaded)
|
||||
{
|
||||
seen_loaded = TRUE;
|
||||
platform_clipboard_seen(seen, sizeof(seen), FALSE);
|
||||
}
|
||||
if (invite)
|
||||
{
|
||||
#ifdef HALO_WEB
|
||||
@@ -1693,6 +1736,7 @@ static void platform_invite_clipboard(BOOL look)
|
||||
#else
|
||||
SDL_SetClipboardText(invite);
|
||||
snprintf(seen, sizeof(seen), "%s", invite);
|
||||
platform_clipboard_seen(seen, sizeof(seen), TRUE);
|
||||
platform_log("Internet play: the invite link is on the clipboard");
|
||||
#endif
|
||||
#ifdef HALO_ANDROID
|
||||
@@ -1706,6 +1750,7 @@ static void platform_invite_clipboard(BOOL look)
|
||||
if (text && strcmp(text, seen) && strlen(text) < sizeof(seen))
|
||||
{
|
||||
snprintf(seen, sizeof(seen), "%s", text);
|
||||
platform_clipboard_seen(seen, sizeof(seen), TRUE);
|
||||
/* (a link, not a bare code: 64 hex digits alone are as often a
|
||||
checksum copied for something else) */
|
||||
if (platform_text_has_invite_link(text) && p2p_join_invite(text))
|
||||
@@ -1852,18 +1897,33 @@ static void platform_show_pending_message(void)
|
||||
/* ---------- events */
|
||||
|
||||
/* quits as closing the window does, when the events are next read (the
|
||||
menus' Quit: port/linux/game/menu_functions.c); on Android at once */
|
||||
menus' Quit: port/linux/game/menu_functions.c); on Android at once, as
|
||||
the system closes its apps: the VR options' Quit (ui_widget.c) too, as on
|
||||
the Steam Frame Steam cannot end the game, which runs in Lepton's container
|
||||
(port/android/STEAM_FRAME.md) */
|
||||
void platform_request_quit(void)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
/* (the guest has no SDL_PushEvent: exit ends the process, host_exit) */
|
||||
exit(EXIT_SUCCESS);
|
||||
#else
|
||||
#ifndef HALO_ANDROID
|
||||
SDL_Event event;
|
||||
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
#else
|
||||
/* (the guest has no SDL_PushEvent: exit ends the process, host_exit) */
|
||||
{
|
||||
/* (and says so beside the game's data: the Steam Frame's launcher
|
||||
does not then take it for a start that failed and start it again) */
|
||||
char path[512];
|
||||
FILE *file;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
|
||||
file = fopen(path, "wb");
|
||||
if (file)
|
||||
fclose(file);
|
||||
}
|
||||
platform_log("quit: the player asked");
|
||||
exit(EXIT_SUCCESS);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2028,6 +2088,9 @@ void platform_pump_events(void)
|
||||
}
|
||||
if (SDL_GetCurrentThreadID() != platform_event_thread)
|
||||
return;
|
||||
/* VR: the next frame starts here, after the last was presented and
|
||||
before the game reads its input (vr.c) */
|
||||
halo_vr_frame_begin();
|
||||
exit_after_check();
|
||||
platform_show_pending_message();
|
||||
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
|
||||
|
||||
@@ -46,7 +46,8 @@ update.h's: posix_update.c on Linux, win32_update.c on Windows.
|
||||
#define HALO_BUILD_FLAVOR "release"
|
||||
#endif
|
||||
|
||||
#define UPDATE_REPOSITORY "OpenCommunityEdition/OpenCE"
|
||||
/* the VR port's own releases (the original's must not replace it) */
|
||||
#define UPDATE_REPOSITORY "Cesarus85/mjolnir-vr"
|
||||
#ifdef _WIN32
|
||||
#define UPDATE_PLATFORM "windows"
|
||||
#define PATH_SEPARATOR "\\"
|
||||
|
||||
@@ -927,9 +927,57 @@ void halo_custom_edition_texels_forget(void)
|
||||
custom_edition_texel_capacity = 0;
|
||||
}
|
||||
|
||||
/* the textures uploaded, the menus' pictures decoded and uploaded and the
|
||||
render targets made since xgpu_texture_work_take last asked, and in how
|
||||
long (vr.c's long frames: a menu screen's first frame uploads its pictures) */
|
||||
static struct
|
||||
{
|
||||
unsigned long counts[XGPU_TEXTURE_WORK_KINDS];
|
||||
unsigned long texels;
|
||||
unsigned long long microseconds;
|
||||
} texture_work;
|
||||
|
||||
void xgpu_texture_work_add(int kind, unsigned long texels, unsigned long long start_us)
|
||||
{
|
||||
if (kind < 0 || kind >= XGPU_TEXTURE_WORK_KINDS)
|
||||
return;
|
||||
texture_work.counts[kind]++;
|
||||
texture_work.texels += texels;
|
||||
texture_work.microseconds += xgpu_shader_work_now_us() - start_us;
|
||||
}
|
||||
|
||||
void xgpu_texture_work_take(unsigned long *uploads, unsigned long *kilotexels, unsigned long *decoded,
|
||||
unsigned long *pictures, unsigned long *targets, float *milliseconds)
|
||||
{
|
||||
*uploads = texture_work.counts[XGPU_TEXTURE_WORK_UPLOAD] + texture_work.counts[XGPU_TEXTURE_WORK_DECODED];
|
||||
*kilotexels = texture_work.texels / 1000;
|
||||
*decoded = texture_work.counts[XGPU_TEXTURE_WORK_DECODED];
|
||||
*pictures = texture_work.counts[XGPU_TEXTURE_WORK_PICTURE];
|
||||
*targets = texture_work.counts[XGPU_TEXTURE_WORK_TARGET];
|
||||
*milliseconds = texture_work.microseconds * 1e-3f;
|
||||
memset(&texture_work, 0, sizeof(texture_work));
|
||||
}
|
||||
|
||||
static void upload_texels(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
|
||||
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order);
|
||||
|
||||
static void upload(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
|
||||
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order)
|
||||
{
|
||||
unsigned long long start = xgpu_shader_work_now_us();
|
||||
int kind = XGPU_TEXTURE_WORK_UPLOAD;
|
||||
|
||||
#ifdef HALO_GLES
|
||||
if (description->compressed && !xgpu_capabilities.s3tc)
|
||||
kind = XGPU_TEXTURE_WORK_DECODED;
|
||||
#endif
|
||||
upload_texels(texture, target, description, base, palette, channel_order);
|
||||
xgpu_texture_work_add(kind, description->width * description->height * description->depth *
|
||||
(description->cube_map ? 6 : 1), start);
|
||||
}
|
||||
|
||||
static void upload_texels(GLuint texture, GLenum target, const struct xgpu_texture_description *description,
|
||||
const unsigned char *base, const D3DCOLOR *palette, unsigned char channel_order)
|
||||
{
|
||||
struct format_information information = format_information(description->format);
|
||||
unsigned long face_count = description->cube_map ? 6 : 1;
|
||||
|
||||
@@ -42,10 +42,40 @@ DWORD WINAPI XLaunchNewImageA(LPCSTR image_path, PLAUNCH_DATA launch_data)
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* port: a European disc in two languages has a maps folder for each, and
|
||||
the Android and Steam Frame disc image setups (XisoExtractor.java,
|
||||
steam_frame_xiso.py) write the language of the one they took into
|
||||
maps/language.txt: the Xbox's language, unless game.language says another */
|
||||
static const char *maps_language(void)
|
||||
{
|
||||
static char language[4];
|
||||
static BOOL read;
|
||||
|
||||
if (!read)
|
||||
{
|
||||
char path[MAX_PATH];
|
||||
FILE *file;
|
||||
|
||||
read = TRUE;
|
||||
snprintf(path, sizeof(path), "%s/maps/language.txt", platform_data_root());
|
||||
if ((file = fopen(path, "r")) != NULL)
|
||||
{
|
||||
if (!fgets(language, sizeof(language), file))
|
||||
language[0] = '\0';
|
||||
language[2] = '\0';
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
DWORD WINAPI XGetLanguage(void)
|
||||
{
|
||||
const char *language = config_string("game.language");
|
||||
|
||||
if (!*language)
|
||||
language = maps_language();
|
||||
|
||||
if (*language)
|
||||
{
|
||||
if (!strncmp(language, "ja", 2)) return XC_LANGUAGE_JAPANESE;
|
||||
|
||||
@@ -33,14 +33,27 @@ struct xgpu_capabilities
|
||||
BOOL color_buffer_half_float;
|
||||
/* "300 es" or "310 es" */
|
||||
const char *shading_language;
|
||||
/* GL_OVR_multiview2: both eyes in one draw (VR, d3d8_multiview_begin) */
|
||||
BOOL multiview;
|
||||
/* the app's (HALO_ANDROID): GL_QCOM_texture_foveated, for the eyes'
|
||||
targets (d3d8_foveation_parameters); GL_KHR_parallel_shader_compile,
|
||||
and the program binary formats the driver has (xgpu_program_cache.c) */
|
||||
BOOL foveation;
|
||||
BOOL parallel_compile;
|
||||
int program_binary_formats;
|
||||
};
|
||||
|
||||
extern struct xgpu_capabilities xgpu_capabilities;
|
||||
|
||||
/* port/android/guest/runtime/guest_host.h */
|
||||
int host_gl_has_extension(const char *name);
|
||||
unsigned int host_gl_read_buffer_word(unsigned int buffer, unsigned int offset);
|
||||
void host_gl_counters_snapshot(unsigned int buffer, unsigned int size, unsigned int slot);
|
||||
int host_gl_counters_read(unsigned int slot, void *out, unsigned int size);
|
||||
void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data);
|
||||
void host_gl_buffer_write(unsigned int target, unsigned int offset, unsigned int size, const void *data);
|
||||
int host_gl_buffer_storage(unsigned int target, unsigned int size);
|
||||
int host_gl_persistent_write(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
|
||||
void host_gl_fence_frame(unsigned int slot);
|
||||
void host_gl_wait_frame(unsigned int slot);
|
||||
#endif
|
||||
@@ -63,6 +76,61 @@ void xgpu_gl_texture_deleted(unsigned int texture);
|
||||
written */
|
||||
GLuint xgpu_compile_shader(GLenum type, const char *code, const char *what);
|
||||
GLuint xgpu_link_program(GLuint vertex_shader, GLuint fragment_shader, const char *what);
|
||||
/* the time a shader's compile, a program's link or its load from the
|
||||
cache took, added to the frame's (xgpu_shader_work_take: vr.c's long
|
||||
frames) */
|
||||
void xgpu_shader_work_add(BOOL loaded, unsigned long long start_us);
|
||||
unsigned long long xgpu_shader_work_now_us(void);
|
||||
/* the same for a texture's upload (with its texels), one decoded from DXT
|
||||
first (GPUs without S3TC), a menu picture's (a PNG decoded) or a render
|
||||
target's making (xbox_textures.c: xgpu_texture_work_take) */
|
||||
enum
|
||||
{
|
||||
XGPU_TEXTURE_WORK_UPLOAD,
|
||||
XGPU_TEXTURE_WORK_DECODED,
|
||||
XGPU_TEXTURE_WORK_PICTURE,
|
||||
XGPU_TEXTURE_WORK_TARGET,
|
||||
XGPU_TEXTURE_WORK_KINDS
|
||||
};
|
||||
void xgpu_texture_work_add(int kind, unsigned long texels, unsigned long long start_us);
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* ---------- the program binary cache (xgpu_program_cache.c)
|
||||
|
||||
Linked programs kept in the data folder's shader_cache, by their sources'
|
||||
hash (xgpu_program_key), with the driver they were linked by; and for each
|
||||
map the programs it used, loaded or compiled again as it loads. */
|
||||
|
||||
void xgpu_program_cache_initialize(void);
|
||||
/* whether programs go through the cache (display.shader_cache, and a driver
|
||||
with binary formats) */
|
||||
BOOL xgpu_program_cache_enabled(void);
|
||||
unsigned long long xgpu_program_key(unsigned long long vertex_hash, unsigned long long fragment_hash);
|
||||
unsigned long long xgpu_source_hash(const char *source);
|
||||
/* the program of these sources: made (*program), being made (a draw that
|
||||
needs it is left out for now), or to make here (XGPU_PROGRAM_NONE) */
|
||||
enum
|
||||
{
|
||||
XGPU_PROGRAM_NONE,
|
||||
XGPU_PROGRAM_READY,
|
||||
XGPU_PROGRAM_WAITING,
|
||||
};
|
||||
int xgpu_program_cache_take(unsigned long long key, const char *vertex_source, const char *fragment_source,
|
||||
GLuint *program);
|
||||
/* a program just linked from these sources, written to the cache */
|
||||
void xgpu_program_cache_store(unsigned long long key, GLuint program, const char *vertex_source,
|
||||
const char *fragment_source);
|
||||
/* a program the map drew with (the map's list); asked again of each
|
||||
program as xgpu_program_map_serial changes (a new map) */
|
||||
void xgpu_program_cache_used(unsigned long long key);
|
||||
extern unsigned long xgpu_program_map_serial;
|
||||
/* each frame: other maps' programs asked for, the worker's results, the
|
||||
lists */
|
||||
void xgpu_program_cache_frame(void);
|
||||
#endif
|
||||
/* the headset's copy, or anything else, left GL as it drew: all set again
|
||||
(d3d8_gl.c) */
|
||||
void d3d8_host_state_changed(void);
|
||||
|
||||
/* ---------- generated source text */
|
||||
|
||||
@@ -106,8 +174,10 @@ NORMPACKED3 32-bit integers and unpacked in the shader. With lighting (else
|
||||
NULL), the normal and world position go to the pixel shader too, which
|
||||
lights the diffuse color for each pixel (nv2a_pixel_shader_key
|
||||
per_pixel_lighting). Returns a malloc'd string. */
|
||||
/* multiview: for a two-view (GL_OVR_multiview2) target, each view's
|
||||
position corrected from the shared one by mv_rows (d3d8_gl.c, VR) */
|
||||
char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instruction_count,
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting);
|
||||
unsigned long packed_attribute_mask, const struct nv2a_vertex_lighting *lighting, int multiview);
|
||||
/* the fixed-function pipeline's vertex shader (the loading screen's draws):
|
||||
positions already in clip space. Returns a malloc'd string. */
|
||||
char *nv2a_fixed_function_glsl(void);
|
||||
|
||||
@@ -40,6 +40,7 @@ drive the controller.
|
||||
#include "platform.h"
|
||||
#include "sdl_platform.h"
|
||||
#include "port_config.h"
|
||||
#include "halo_vr.h"
|
||||
#include "touch_input.h"
|
||||
/* port/linux/game/network_aim_assist.c */
|
||||
extern unsigned char network_aim_assist_block_mouse(void);
|
||||
@@ -219,7 +220,8 @@ int halo_chat_taps(float *x, float *y)
|
||||
int halo_chat_touch_layout(void)
|
||||
{
|
||||
#if defined(HALO_ANDROID)
|
||||
return TRUE;
|
||||
/* (port: not in VR, which has no touch controls in the way) */
|
||||
return !halo_vr_active();
|
||||
#elif defined(HALO_WEB)
|
||||
int used;
|
||||
|
||||
@@ -279,7 +281,8 @@ int halo_linux_touch_move(short controller_index, float *forward, float *strafe)
|
||||
if (controller_index != 0)
|
||||
return FALSE;
|
||||
#if defined(HALO_ANDROID) || defined(HALO_WEB)
|
||||
if (!touch_input_move(forward, strafe) || network_chat_keyboard_open())
|
||||
/* (port: not in VR, where the touch controls are off) */
|
||||
if (halo_vr_active() || !touch_input_move(forward, strafe) || network_chat_keyboard_open())
|
||||
{
|
||||
*forward = 0.0f;
|
||||
*strafe = 0.0f;
|
||||
@@ -306,6 +309,9 @@ int halo_linux_touch_look(short gamepad_index, float *yaw, float *pitch, float *
|
||||
if (gamepad_index != 0)
|
||||
return FALSE;
|
||||
#if defined(HALO_ANDROID) || defined(HALO_WEB)
|
||||
/* (port: not in VR, where the touch controls are off) */
|
||||
if (halo_vr_active())
|
||||
return FALSE;
|
||||
touch_input_look(0.0022f, yaw, pitch, gyro_yaw, gyro_pitch);
|
||||
/* (none while the chat's on-screen keyboard is up: what was swiped is
|
||||
dropped, not saved for after) */
|
||||
@@ -772,6 +778,11 @@ int halo_push_to_talk_held(void)
|
||||
struct platform_input_state input;
|
||||
int slot;
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* (port: in VR its button too, vr.button_talk: halo_vr_push_to_talk_held) */
|
||||
if (halo_vr_active() && halo_vr_push_to_talk_held())
|
||||
return 1;
|
||||
#endif
|
||||
/* (the window losing the focus lets every key go: sdl_platform.c) */
|
||||
bindings_read();
|
||||
platform_input_read(&input, FALSE);
|
||||
@@ -1251,6 +1262,192 @@ static int controller_port(HANDLE device)
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* ---------- VR controllers (vr.c)
|
||||
|
||||
In play, laid out for VR shooters rather than by position:
|
||||
|
||||
right trigger fire left trigger grenade: held in the hand, thrown as it lets go (vr.c)
|
||||
right grip melee (B) left grip hold the weapon with both hands (vr.c)
|
||||
(a hand thrust forward, vr.melee_swing_speed, is a melee attack too)
|
||||
A jump X action, reload (X), as the game's prompts say
|
||||
B flashlight (white); held, switch grenade (black)
|
||||
Y switch weapon (Y)
|
||||
right stick turns (vr.c); click zooms left stick move; click crouches
|
||||
menu pause (start)
|
||||
|
||||
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"
|
||||
#include "halo_xr_abi.h"
|
||||
|
||||
int halo_vr_controllers(struct halo_xr_hand hands[HALO_XR_HAND_COUNT], int *menus);
|
||||
int halo_vr_wheel_held(void);
|
||||
|
||||
/* a click that stands for the pad's A (ui_widget.c: the screen where
|
||||
controllers join by pressing it), held a moment */
|
||||
static Uint64 vr_pad_a_until;
|
||||
|
||||
void halo_vr_pulse_pad_a(void)
|
||||
{
|
||||
vr_pad_a_until = SDL_GetTicks() + 120;
|
||||
}
|
||||
|
||||
static SHORT vr_stick(float value)
|
||||
{
|
||||
if (value > 1.0f)
|
||||
value = 1.0f;
|
||||
if (value < -1.0f)
|
||||
value = -1.0f;
|
||||
return (SHORT)(value * 32767.0f);
|
||||
}
|
||||
|
||||
static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
{
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
const struct halo_xr_hand *left = &hands[HALO_XR_HAND_LEFT];
|
||||
const struct halo_xr_hand *right = &hands[HALO_XR_HAND_RIGHT];
|
||||
int menus = 0;
|
||||
SHORT value;
|
||||
|
||||
if (!halo_vr_controllers(hands, &menus))
|
||||
return;
|
||||
value = vr_stick(left->thumbstick[0]);
|
||||
if (abs(value) > abs(pad->sThumbLX)) pad->sThumbLX = value;
|
||||
value = vr_stick(left->thumbstick[1]);
|
||||
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
|
||||
{
|
||||
/* the menu button: a press pauses, as Start; held, the scores show
|
||||
while it is, as Back (the Quest's controllers have no button of
|
||||
their own for it). In the menus it is Start at once */
|
||||
static Uint64 menu_pressed_at, menu_pulse_until;
|
||||
static int menu_held_handled;
|
||||
Uint64 now = SDL_GetTicks();
|
||||
int down = (left->buttons & HALO_XR_BUTTON_MENU) != 0;
|
||||
|
||||
if (menus)
|
||||
{
|
||||
if (down)
|
||||
pad->wButtons |= XINPUT_GAMEPAD_START;
|
||||
menu_pressed_at = 0;
|
||||
}
|
||||
else if (down)
|
||||
{
|
||||
if (!menu_pressed_at)
|
||||
{
|
||||
menu_pressed_at = now;
|
||||
menu_held_handled = 0;
|
||||
}
|
||||
else if (now - menu_pressed_at >= 400)
|
||||
{
|
||||
pad->wButtons |= XINPUT_GAMEPAD_BACK;
|
||||
menu_held_handled = 1;
|
||||
}
|
||||
}
|
||||
else if (menu_pressed_at)
|
||||
{
|
||||
if (!menu_held_handled)
|
||||
menu_pulse_until = now + 100;
|
||||
menu_pressed_at = 0;
|
||||
}
|
||||
if (now < menu_pulse_until)
|
||||
pad->wButtons |= XINPUT_GAMEPAD_START;
|
||||
}
|
||||
/* (the Steam Frame's controllers: the left one's View button and D-pad) */
|
||||
if (left->buttons & HALO_XR_BUTTON_VIEW) pad->wButtons |= XINPUT_GAMEPAD_BACK;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_UP) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_DOWN) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_LEFT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
|
||||
if (left->buttons & HALO_XR_BUTTON_DPAD_RIGHT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_RIGHT;
|
||||
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
|
||||
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
|
||||
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
|
||||
if (!menus && (BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = (BYTE)(right->trigger * 255.0f);
|
||||
if (!menus && (BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = (BYTE)(left->trigger * 255.0f);
|
||||
if (!menus && halo_vr_wheel_held())
|
||||
{
|
||||
/* at the wheel: the triggers drive, and neither fire nor throw */
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0;
|
||||
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = 0;
|
||||
value = vr_stick(right->trigger - left->trigger);
|
||||
if (abs(value) > abs(pad->sThumbLY)) pad->sThumbLY = value;
|
||||
}
|
||||
if (menus)
|
||||
{
|
||||
/* the right stick scrolls too; turning is off in the menus */
|
||||
value = vr_stick(right->thumbstick[0]);
|
||||
if (abs(value) > abs(pad->sThumbRX)) pad->sThumbRX = value;
|
||||
value = vr_stick(right->thumbstick[1]);
|
||||
if (abs(value) > abs(pad->sThumbRY)) pad->sThumbRY = value;
|
||||
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0 ||
|
||||
SDL_GetTicks() < vr_pad_a_until);
|
||||
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_B) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_Y, (left->buttons & HALO_XR_BUTTON_B) != 0);
|
||||
return;
|
||||
}
|
||||
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: 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) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_BLACK, (left->buttons & HALO_XR_BUTTON_BUMPER) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_WHITE, (right->buttons & HALO_XR_BUTTON_BUMPER) != 0);
|
||||
{
|
||||
/* B: a tap is the flashlight, holding it switches grenades; the
|
||||
game sees a short press of either */
|
||||
static Uint64 pressed_at, pulse_until;
|
||||
static int pulse_button = -1, held_handled;
|
||||
Uint64 now = SDL_GetTicks();
|
||||
int down = (right->buttons & HALO_XR_BUTTON_B) != 0;
|
||||
|
||||
if (down && !pressed_at)
|
||||
{
|
||||
pressed_at = now;
|
||||
held_handled = 0;
|
||||
}
|
||||
else if (down && !held_handled && now - pressed_at >= 500)
|
||||
{
|
||||
pulse_button = XINPUT_GAMEPAD_BLACK;
|
||||
pulse_until = now + 100;
|
||||
held_handled = 1;
|
||||
}
|
||||
else if (!down && pressed_at)
|
||||
{
|
||||
if (!held_handled)
|
||||
{
|
||||
pulse_button = XINPUT_GAMEPAD_WHITE;
|
||||
pulse_until = now + 100;
|
||||
}
|
||||
pressed_at = 0;
|
||||
}
|
||||
if (pulse_button >= 0 && now < pulse_until)
|
||||
merge_button(pad, pulse_button, TRUE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* reads a controller's state; for port 0 the keyboard, mouse, debug input
|
||||
and touchscreen are merged into the first gamepad's; runs on the game's main
|
||||
thread (touch_input_gamepad relies on it); returns ERROR_SUCCESS or
|
||||
@@ -1290,11 +1487,21 @@ DWORD WINAPI XInputGetState(HANDLE device, PXINPUT_STATE state)
|
||||
}
|
||||
if (port_gamepad(gamepads, count, 0))
|
||||
sdl_gamepad_state(gamepads[0], &state->Gamepad);
|
||||
test_input_gamepad(&state->Gamepad);
|
||||
touch_input_gamepad(&state->Gamepad);
|
||||
#if defined(HALO_ANDROID) || defined(HALO_WEB)
|
||||
touch_input_controls(&state->Gamepad, input.menus);
|
||||
#ifdef HALO_ANDROID
|
||||
vr_gamepad_state(&state->Gamepad);
|
||||
#endif
|
||||
test_input_gamepad(&state->Gamepad);
|
||||
#ifdef HALO_ANDROID
|
||||
/* port: in VR the touchscreen's controls neither show (the overlay
|
||||
stays hidden until touch_input_controls reports the scene) nor act */
|
||||
if (!halo_vr_active())
|
||||
#endif
|
||||
{
|
||||
touch_input_gamepad(&state->Gamepad);
|
||||
#if defined(HALO_ANDROID) || defined(HALO_WEB)
|
||||
touch_input_controls(&state->Gamepad, input.menus);
|
||||
#endif
|
||||
}
|
||||
/* (the text chat's on-screen keyboard reads them, and takes them
|
||||
while it is up) */
|
||||
chat_pad_record(&state->Gamepad);
|
||||
@@ -1333,6 +1540,19 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
|
||||
if (port < 0)
|
||||
return ERROR_DEVICE_NOT_CONNECTED;
|
||||
#if defined(HALO_ANDROID) || defined(HALO_WEB)
|
||||
#ifdef HALO_ANDROID
|
||||
/* 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())
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
/* the phone vibrates for the touch controls' player (in a browser, where
|
||||
it can) */
|
||||
if (port == 0)
|
||||
|
||||
@@ -681,6 +681,46 @@ unsigned long tag_groups_checksum(
|
||||
return cache_file_globals.tag_header->checksum;
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR test aid: the loaded tag of the group after the given one (NONE: the
|
||||
first), wrapping round; NONE if the map has none. Its name too. */
|
||||
long tag_vr_next_loaded(
|
||||
long group_tag,
|
||||
long after_tag_index,
|
||||
char const **name)
|
||||
{
|
||||
short absolute_index;
|
||||
short start = 0;
|
||||
short count;
|
||||
|
||||
if (!cache_file_globals.tags_loaded)
|
||||
return NONE;
|
||||
count = (short)cache_file_globals.tag_header->tag_count;
|
||||
if (after_tag_index != NONE)
|
||||
{
|
||||
for (absolute_index = 0; absolute_index < count; absolute_index++)
|
||||
{
|
||||
if (global_tag_instances[absolute_index].tag_index == after_tag_index)
|
||||
{
|
||||
start = absolute_index + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (absolute_index = 0; absolute_index < count; absolute_index++)
|
||||
{
|
||||
struct cache_file_tag_instance *instance = &global_tag_instances[(start + absolute_index) % count];
|
||||
|
||||
if (instance->group_tag == group_tag)
|
||||
{
|
||||
*name = instance->name;
|
||||
return instance->tag_index;
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
#endif
|
||||
long tag_loaded(
|
||||
long group_tag,
|
||||
char const *name)
|
||||
|
||||
@@ -139,6 +139,9 @@ symbols in this file:
|
||||
#include "units/unit_definitions.h"
|
||||
#include "units/units.h"
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
/* network_game_globals.c's */
|
||||
boolean network_game_distributed_client(void);
|
||||
|
||||
@@ -373,6 +376,11 @@ short director_desired_perspective(
|
||||
if (*perspective == 1 || *perspective == 3)
|
||||
following = TRUE;
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: seated, the player stays behind his own eyes (vr.vehicle_first_person) */
|
||||
if (halo_vr_vehicle_first_person())
|
||||
following = FALSE;
|
||||
#endif
|
||||
|
||||
return following;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ symbols in this file:
|
||||
#include "units/vehicle_definitions.h"
|
||||
#include "units/vehicles.h"
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* ---------- macros */
|
||||
@@ -156,6 +160,20 @@ void first_person_camera_update(
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: a rider who sits down in the hull (a Ghost, a Banshee, a Covenant
|
||||
gun, a Scorpion's driver) sees out over it: the eyes rise by a height set
|
||||
for the kind of hull (vr.ghost_eye_raise and the like). Not by the hull's
|
||||
collision model: it leaves out the Ghost's hood and the Shade's shield, and
|
||||
has the Scorpion's turret, which swings over the driver. */
|
||||
static void first_person_camera_vr_over_hull(
|
||||
long vehicle_index,
|
||||
real_point3d *eyes)
|
||||
{
|
||||
eyes->z += halo_vr_enclosed_seat_raise(vehicle_vr_hull(vehicle_index));
|
||||
}
|
||||
|
||||
#endif
|
||||
static void first_person_camera_for_unit_and_vector(
|
||||
long unit_index,
|
||||
real_vector3d const *forward,
|
||||
@@ -226,6 +244,58 @@ static void first_person_camera_for_unit_and_vector(
|
||||
matrix4x3_transform_normal(&matrix, &result->up, &result->up);
|
||||
}
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
if (halo_vr_vehicle_first_person())
|
||||
{
|
||||
/* VR: seated, behind the rider's own eyes, not at the point
|
||||
the seat's camera turns about (the vehicle's middle) */
|
||||
struct object_marker marker;
|
||||
struct unit_datum *seated_in = vehicle_try_and_get(unit->object.parent_object_index);
|
||||
unsigned long seat_flags = seated_in ?
|
||||
TAG_BLOCK_GET_ELEMENT(
|
||||
&vehicle_definition_get(seated_in->definition_index)->unit.seats,
|
||||
unit->unit.parent_seat_index,
|
||||
struct unit_seat)->flags :
|
||||
0;
|
||||
|
||||
if (object_get_marker_by_name(unit_index, "head", &marker, 1))
|
||||
{
|
||||
result->position = marker.matrix.position;
|
||||
if (TEST_FLAG(seat_flags, _unit_seat_gunner_bit))
|
||||
{
|
||||
/* a gunner leans into the gun's sights: back from them
|
||||
(vr.gunner_camera_back) */
|
||||
real_vector3d back = unit->unit.aiming_vector;
|
||||
|
||||
back.k = 0.f;
|
||||
if (normalize3d(&back) > 0.f)
|
||||
point_from_line3d(&result->position, &back, -halo_vr_gunner_camera_back(), &result->position);
|
||||
}
|
||||
if (seated_in && TEST_FLAG(seat_flags, _unit_seat_driver_bit) &&
|
||||
!vehicle_vr_encloses_riders(unit->object.parent_object_index))
|
||||
{
|
||||
/* at an open wheel (a Warthog's), nearer the windscreen
|
||||
(vr.driver_camera_forward) */
|
||||
real_vector3d vehicle_forward;
|
||||
real_vector3d vehicle_up;
|
||||
|
||||
object_get_orientation(unit->object.parent_object_index, &vehicle_forward, &vehicle_up);
|
||||
vehicle_forward.k = 0.f;
|
||||
if (normalize3d(&vehicle_forward) > 0.f)
|
||||
{
|
||||
point_from_line3d(&result->position, &vehicle_forward, halo_vr_driver_camera_forward(),
|
||||
&result->position);
|
||||
}
|
||||
}
|
||||
/* (the riders on a tank's back sit on it, not in it) */
|
||||
if (seated_in && vehicle_vr_encloses_riders(unit->object.parent_object_index) &&
|
||||
(TEST_FLAG(seat_flags, _unit_seat_driver_bit) || TEST_FLAG(seat_flags, _unit_seat_gunner_bit)))
|
||||
{
|
||||
first_person_camera_vr_over_hull(unit->object.parent_object_index, &result->position);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
result->flags = FLAG(0);
|
||||
|
||||
@@ -69,6 +69,10 @@ symbols in this file:
|
||||
#include "units/unit_definitions.h"
|
||||
#include "units/units.h"
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
@@ -577,6 +581,35 @@ long player_aim_projectile_preview(
|
||||
real autoaim_level;
|
||||
|
||||
director_camera_deterministic(player->unit_index, &camera_position, &camera_direction);
|
||||
#ifdef HALO_ANDROID
|
||||
if (halo_vr_active() && player->local_player_index==0)
|
||||
{
|
||||
struct unit_datum *vr_unit= unit_get(player->unit_index);
|
||||
float vr_eyes[3], vr_neck[3], vr_heading[3];
|
||||
|
||||
if (vr_unit->object.parent_object_index!=NONE &&
|
||||
halo_vr_body_frame(player->local_player_index, vr_eyes, vr_neck, vr_heading))
|
||||
{
|
||||
/* VR, seated: as the game aims a vehicle's gun, at what lies
|
||||
along the player's aim, from the eyes (vr.c draws the crosshair
|
||||
there); its barrel alone points elsewhere (a Banshee's nose
|
||||
rises as it hovers, a tank's turret lags) */
|
||||
camera_position.x= vr_eyes[0];
|
||||
camera_position.y= vr_eyes[1];
|
||||
camera_position.z= vr_eyes[2];
|
||||
camera_direction= vr_unit->unit.aiming_vector;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* VR: the weapon aims from the hand, not from the eyes: trace
|
||||
and seek targets along its own line, where its crosshair is
|
||||
(from the camera the shots would meet the head's parallel
|
||||
line, above) */
|
||||
camera_position= *position;
|
||||
camera_direction= *direction;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
target_direction= *direction;
|
||||
autoaim_level= 0.f;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -662,6 +662,10 @@ void game_initialize_for_new_map(
|
||||
game_engine_initialize(&game_variant_global);
|
||||
real_math_reset_precision();
|
||||
rasterizer_initialize_for_new_map();
|
||||
/* port: the shader programs the map drew with last time compiled now,
|
||||
while it loads, not as its first frames draw (port/linux/src/
|
||||
xgpu_program_cache.c) */
|
||||
halo_shader_programs_map_loaded(game_globals->options.map_name);
|
||||
game_state_initialize_for_new_map();
|
||||
game_time_initialize_for_new_map();
|
||||
interface_initialize_for_new_map();
|
||||
@@ -911,6 +915,8 @@ void game_precache_new_map(
|
||||
void game_dispose_from_old_map(
|
||||
void)
|
||||
{
|
||||
/* port: the map's list of the shader programs it drew with kept (port/linux/src/xgpu_program_cache.c) */
|
||||
halo_shader_programs_map_disposed();
|
||||
rasterizer_dispose_from_old_map();
|
||||
game_state_dispose_from_old_map();
|
||||
cheats_dispose_from_old_map();
|
||||
|
||||
@@ -1685,3 +1685,40 @@ boolean game_engine_race_read_network_state(
|
||||
csmemcpy(race_events.best_laps, state.best_laps, sizeof(race_events.best_laps));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
boolean game_engine_race_next_flag(
|
||||
long player_index,
|
||||
real_point3d const *from,
|
||||
real_point3d *position)
|
||||
{
|
||||
struct scenario *scenario = global_scenario_get();
|
||||
real best_distance = REAL_MAX;
|
||||
boolean found = FALSE;
|
||||
long itr;
|
||||
|
||||
if (!scenario || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MULTIPLAYER_MAXIMUM_PLAYERS)
|
||||
return FALSE;
|
||||
for (itr = 0; itr < scenario->netgame_flags.count; itr++)
|
||||
{
|
||||
struct scenario_netgame_flag *flag = TAG_BLOCK_GET_ELEMENT(
|
||||
&scenario->netgame_flags,
|
||||
itr,
|
||||
struct scenario_netgame_flag);
|
||||
real distance;
|
||||
|
||||
if (flag->type != _netgame_flag_race_track ||
|
||||
!TEST_FLAG(race_globals.lap_completed_value, flag->team_index) ||
|
||||
!can_touch_team(player_index, flag->team_index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
distance = distance3d(from, &flag->position);
|
||||
if (distance < best_distance)
|
||||
{
|
||||
best_distance = distance;
|
||||
*position = flag->position;
|
||||
found = TRUE;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
@@ -18,6 +18,13 @@ struct race_globals;
|
||||
|
||||
void race_flags_make_unique(
|
||||
void);
|
||||
/* port: the race flag a player is to touch next, the one of those it may
|
||||
touch nearest the point (the VR port's bots, port/linux/game/bots.c);
|
||||
FALSE: none */
|
||||
boolean game_engine_race_next_flag(
|
||||
long player_index,
|
||||
real_point3d const *from,
|
||||
real_point3d *position);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
|
||||
@@ -392,6 +392,13 @@ real player_look_pitch_rate[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS] = {0};
|
||||
boolean controls_enable_crouch = FALSE;
|
||||
boolean controls_enable_doubled_spin = FALSE;
|
||||
boolean controls_swap_doubled_spin_state = FALSE;
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#include "tag_files/tag_groups.h"
|
||||
void platform_log(const char *format, ...);
|
||||
/* how far up or down the head can aim the player */
|
||||
#define VR_PITCH_LIMIT (85.f * _pi / 180.f)
|
||||
#endif
|
||||
/* The native builds read input once a frame and draw several frames per
|
||||
30 Hz tick (port/linux/game/render_interpolation.c). The pitch autolevel and
|
||||
limits were stepped once per input update, a tick on the Xbox, so they step
|
||||
@@ -399,6 +406,49 @@ by the ticks the frame lasted; an impulse applied from a tick steps by one. */
|
||||
static real player_control_angle_step_ticks = 1.f;
|
||||
static real player_control_autolevel_time[MAXIMUM_NUMBER_OF_LOCAL_PLAYERS];
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR test aid (debug.vehicle_spawn, both sticks held in): the map's next
|
||||
vehicle, a few metres ahead of the player; not the flying transports */
|
||||
static void player_control_vr_spawn_vehicle(
|
||||
long unit_index)
|
||||
{
|
||||
static long last_definition_index = NONE;
|
||||
struct object_placement_data data;
|
||||
struct unit_datum *unit = unit_get(unit_index);
|
||||
real_vector3d forward = unit->unit.aiming_vector;
|
||||
real_point3d position;
|
||||
char const *name = NULL;
|
||||
long definition_index = last_definition_index;
|
||||
short tries;
|
||||
|
||||
for (tries = 0; tries < 64; tries++)
|
||||
{
|
||||
definition_index = tag_vr_next_loaded(VEHICLE_DEFINITION_TAG, definition_index, &name);
|
||||
if (definition_index == NONE || definition_index == last_definition_index && tries > 0)
|
||||
break;
|
||||
if (!strstr(name, "pelican") && !strstr(name, "dropship") && !strstr(name, "lifepod"))
|
||||
break;
|
||||
}
|
||||
if (definition_index == NONE)
|
||||
{
|
||||
platform_log("VR: no vehicle in this map");
|
||||
return;
|
||||
}
|
||||
last_definition_index = definition_index;
|
||||
forward.k = 0.f;
|
||||
if (normalize3d(&forward) == 0.f)
|
||||
forward = *global_forward3d;
|
||||
object_get_origin(unit_index, &position);
|
||||
point_from_line3d(&position, &forward, 2.f, &position);
|
||||
position.z += 0.3f;
|
||||
object_placement_data_new(&data, definition_index, NONE);
|
||||
data.position = position;
|
||||
data.forward = forward;
|
||||
data.up = *global_up3d;
|
||||
platform_log("VR: spawning %s: %s", name, object_new(&data) == NONE ? "failed" : "done");
|
||||
}
|
||||
|
||||
#endif
|
||||
/* ---------- public code */
|
||||
|
||||
short unit_get_local_player_index(
|
||||
@@ -758,6 +808,44 @@ static void handle_one_player_input(
|
||||
player->zoom_level);
|
||||
}
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
{
|
||||
/* VR: the seat the player sits in, and its vehicle's heading
|
||||
(vr.c: the body turns with it, and the right stick steers or the
|
||||
right controller aims) */
|
||||
int vr_seat = HALO_VR_SEAT_NONE;
|
||||
int vr_armed = FALSE;
|
||||
real vr_vehicle_yaw = 0.f;
|
||||
struct unit_datum *vr_vehicle = unit->object.parent_object_index == NONE ?
|
||||
NULL :
|
||||
vehicle_try_and_get(unit->object.parent_object_index);
|
||||
|
||||
if (vr_vehicle)
|
||||
{
|
||||
struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(
|
||||
&vehicle_definition_get(vr_vehicle->definition_index)->unit.seats,
|
||||
unit->unit.parent_seat_index,
|
||||
struct unit_seat);
|
||||
real_vector3d vehicle_forward;
|
||||
real_vector3d vehicle_up;
|
||||
|
||||
object_get_orientation(unit->object.parent_object_index, &vehicle_forward, &vehicle_up);
|
||||
vr_vehicle_yaw = (real)atan2(vehicle_forward.j, vehicle_forward.i);
|
||||
if (TEST_FLAG(seat->flags, _unit_seat_gunner_bit))
|
||||
{
|
||||
vr_armed = HALO_VR_SEAT_ARMED |
|
||||
(TEST_FLAG(seat->flags, _unit_seat_driver_bit) ? 0 : HALO_VR_SEAT_TURRET);
|
||||
}
|
||||
vr_seat = TEST_FLAG(seat->flags, _unit_seat_driver_bit) &&
|
||||
vehicle_vr_steered_by_facing(unit->object.parent_object_index) ?
|
||||
HALO_VR_SEAT_STEER :
|
||||
HALO_VR_SEAT_AIM;
|
||||
}
|
||||
halo_vr_set_seat(local_player_index, vr_seat, vr_armed, vr_vehicle_yaw);
|
||||
if (halo_vr_take_vehicle_spawn(local_player_index) && !vr_vehicle)
|
||||
player_control_vr_spawn_vehicle(player->unit_index);
|
||||
}
|
||||
#endif
|
||||
if (!director_inhibited_facing(local_player_index))
|
||||
{
|
||||
player_control_angle_step_ticks = time_delta_sec * TICKS_PER_SECOND;
|
||||
@@ -766,6 +854,26 @@ static void handle_one_player_input(
|
||||
input.facing_delta.yaw,
|
||||
input.facing_delta.pitch);
|
||||
player_control_angle_step_ticks = 1.f;
|
||||
#ifdef HALO_ANDROID
|
||||
{
|
||||
/* VR: the player faces where the head does (vr.c); not in a
|
||||
cinematic, whose camera the game has (seated, the controller
|
||||
would turn it on the screen) */
|
||||
real vr_yaw;
|
||||
real vr_pitch;
|
||||
|
||||
if (!cinematic_in_progress() && halo_vr_aim(
|
||||
local_player_index,
|
||||
player->desired_angles.yaw,
|
||||
player->desired_angles.pitch,
|
||||
&vr_yaw,
|
||||
&vr_pitch))
|
||||
{
|
||||
player->desired_angles.yaw = vr_yaw;
|
||||
player->desired_angles.pitch = PIN(vr_pitch, -VR_PITCH_LIMIT, VR_PITCH_LIMIT);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (unit->object.parent_object_index == NONE)
|
||||
@@ -1787,6 +1895,26 @@ void player_control_action_test_note(
|
||||
{
|
||||
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
|
||||
}
|
||||
#ifdef HALO_ANDROID
|
||||
{
|
||||
/* VR: turning the head is looking around too (the training at the
|
||||
start of the campaign waits for it) */
|
||||
real vr_yaw;
|
||||
real vr_pitch;
|
||||
|
||||
if (halo_vr_look_delta(&vr_yaw, &vr_pitch))
|
||||
{
|
||||
if (vr_pitch > 0.f)
|
||||
globals->action_flags |= FLAG(_player_control_look_relative_up_bit);
|
||||
else if (vr_pitch < 0.f)
|
||||
globals->action_flags |= FLAG(_player_control_look_relative_down_bit);
|
||||
if (vr_yaw > 0.f)
|
||||
globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
|
||||
else if (vr_yaw < 0.f)
|
||||
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (throttle->i > 0.f)
|
||||
{
|
||||
globals->action_flags |= FLAG(_player_control_move_relative_forward_bit);
|
||||
|
||||
@@ -281,6 +281,9 @@ symbols in this file:
|
||||
#include "units/units.h"
|
||||
#include "units/vehicle_definitions.h"
|
||||
#include "units/vehicles.h"
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#endif
|
||||
|
||||
/* network_game_globals.c's */
|
||||
boolean network_game_distributed_client(void);
|
||||
@@ -609,8 +612,11 @@ void players_initialize_for_new_map(
|
||||
machine_to_player_table,
|
||||
NONE,
|
||||
sizeof(machine_to_player_table));
|
||||
/* port: the bots of local games (port/linux/game/bots.c) */
|
||||
/* port: the bots of local games (port/linux/game/bots.c; not in the VR
|
||||
port, whose own are host_bots.c's) */
|
||||
#ifndef HALO_ANDROID
|
||||
bots_initialize_for_new_map();
|
||||
#endif
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -618,8 +624,11 @@ void players_initialize_for_new_map(
|
||||
void players_dispose_from_old_map(
|
||||
void)
|
||||
{
|
||||
/* port: the bots of local games (port/linux/game/bots.c) */
|
||||
/* port: the bots of local games (port/linux/game/bots.c; not in the VR
|
||||
port) */
|
||||
#ifndef HALO_ANDROID
|
||||
bots_dispose_from_old_map();
|
||||
#endif
|
||||
data_make_invalid(player_data);
|
||||
data_make_invalid(team_data);
|
||||
|
||||
@@ -4793,7 +4802,9 @@ void players_update_before_game(
|
||||
{
|
||||
/* port: the controls of a local game's bots this tick
|
||||
(port/linux/game/bots.c), in place of what their queues held */
|
||||
#ifndef HALO_ANDROID
|
||||
bots_update_actions(actions);
|
||||
#endif
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while (player = data_iterator_next(&iterator))
|
||||
{
|
||||
@@ -4952,6 +4963,45 @@ void players_update_before_game(
|
||||
&action->desired_facing);
|
||||
control_data.facing_vector =
|
||||
control_data.looking_vector = control_data.aiming_vector;
|
||||
#ifdef HALO_ANDROID
|
||||
if (unit->object.parent_object_index == NONE)
|
||||
{
|
||||
/* VR: on foot, the weapon aims where the right controller
|
||||
points, while the body and the view keep facing where the
|
||||
head looks (port/linux/src/vr.c) */
|
||||
real vr_aim[3];
|
||||
real vr_look[3];
|
||||
|
||||
/* and it looks where the head looks, whether or not the
|
||||
game lets the player turn now (scripts test it:
|
||||
unit_can_see_point) */
|
||||
if (halo_vr_look_vector(player->local_player_index, vr_look))
|
||||
{
|
||||
control_data.looking_vector.i = vr_look[0];
|
||||
control_data.looking_vector.j = vr_look[1];
|
||||
control_data.looking_vector.k = vr_look[2];
|
||||
}
|
||||
/* (a VR player of another machine: where their
|
||||
machine says their weapon aims, network_vr.c) */
|
||||
if (player->local_player_index != NONE ?
|
||||
halo_vr_aim_vector(player->local_player_index, vr_aim) :
|
||||
halo_vr_network_aim(iterator.datum_index, vr_aim))
|
||||
{
|
||||
control_data.aiming_vector.i = vr_aim[0];
|
||||
control_data.aiming_vector.j = vr_aim[1];
|
||||
control_data.aiming_vector.k = vr_aim[2];
|
||||
}
|
||||
}
|
||||
{
|
||||
real unit_look[3];
|
||||
|
||||
unit_look[0] = unit->unit.looking_vector.i;
|
||||
unit_look[1] = unit->unit.looking_vector.j;
|
||||
unit_look[2] = unit->unit.looking_vector.k;
|
||||
halo_vr_debug_look(player->local_player_index, unit_look,
|
||||
unit->object.parent_object_index != NONE);
|
||||
}
|
||||
#endif
|
||||
control_data.throttle.i = action->throttle.i;
|
||||
control_data.throttle.j = action->throttle.j;
|
||||
control_data.throttle.k = 0.f;
|
||||
@@ -5280,6 +5330,17 @@ boolean player_name_valid(
|
||||
return FALSE;
|
||||
for (index = 0; index < count && (cleaned[index] || name[index]); index++)
|
||||
{
|
||||
/* (but the spaces it trims from the end: the Xbox's menus pad a new
|
||||
profile's name, "New001 ") */
|
||||
if (!cleaned[index])
|
||||
{
|
||||
for (; index < count && name[index]; index++)
|
||||
{
|
||||
if (name[index] != ' ')
|
||||
return FALSE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cleaned[index] != name[index])
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,13 @@ enum
|
||||
_event_type_right_stick,
|
||||
_event_type_button,
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* (the VR port's: a headset's controllers' thumbsticks are pushed as
|
||||
far less readily than a gamepad's) */
|
||||
STICK_EVENT_THRESHOLD = 22937,
|
||||
#else
|
||||
STICK_EVENT_THRESHOLD = 29490,
|
||||
#endif
|
||||
STICK_EVENT_REPEAT_MILLISECONDS = 250
|
||||
};
|
||||
|
||||
@@ -71,6 +77,24 @@ typedef char verify_event_manager_state_size[
|
||||
typedef char verify_event_manager_globals_size[
|
||||
sizeof(struct event_manager_globals) == 0x168 ? 1 : -1];
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* The VR port's: a stick held moves on once after a pause, then quickly,
|
||||
as menus do now (the Xbox's moved on every quarter second, which in a
|
||||
headset, with the keyboard's letters to cross, is slow going). */
|
||||
enum
|
||||
{
|
||||
STICK_REPEAT_FIRST_MILLISECONDS = 350,
|
||||
STICK_REPEAT_MILLISECONDS = 90,
|
||||
};
|
||||
|
||||
static short stick_repeats[NUMBER_OF_GAMEPAD_STICKS][MAXIMUM_GAMEPADS];
|
||||
|
||||
#define STICK_REPEAT_INTERVAL(stick, controller_index) \
|
||||
(stick_repeats[stick][controller_index] ? STICK_REPEAT_MILLISECONDS : STICK_REPEAT_FIRST_MILLISECONDS)
|
||||
#else
|
||||
#define STICK_REPEAT_INTERVAL(stick, controller_index) STICK_EVENT_REPEAT_MILLISECONDS
|
||||
#endif
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
static void queue_event(
|
||||
@@ -83,6 +107,30 @@ struct event_manager_globals event_manager_globals = { 0 };
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
/* an event the stick posts: a fresh push (it was within the threshold on
|
||||
that axis) starts the count of repeats again */
|
||||
static void stick_note_event(
|
||||
short stick,
|
||||
short controller_index,
|
||||
long x,
|
||||
long y,
|
||||
unsigned long time)
|
||||
{
|
||||
boolean fresh =
|
||||
(ABS(x) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[stick][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[stick][1][controller_index]) < STICK_EVENT_THRESHOLD);
|
||||
|
||||
(void)time;
|
||||
if (fresh)
|
||||
stick_repeats[stick][controller_index] = 0;
|
||||
else if (stick_repeats[stick][controller_index] < SHORT_MAX)
|
||||
stick_repeats[stick][controller_index]++;
|
||||
}
|
||||
|
||||
#endif
|
||||
void event_manager_initialize(
|
||||
void)
|
||||
{
|
||||
@@ -209,12 +257,16 @@ static void queue_event(
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_left][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] >=
|
||||
STICK_REPEAT_INTERVAL(_gamepad_stick_left, controller_index)))
|
||||
{
|
||||
post = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
stick_note_event(_gamepad_stick_left, controller_index, x, y, time);
|
||||
#endif
|
||||
event_manager_globals.stick_event_times[_gamepad_stick_left][controller_index] = time;
|
||||
post = TRUE;
|
||||
|
||||
@@ -260,12 +312,16 @@ static void queue_event(
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][0][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
(ABS(y) >= STICK_EVENT_THRESHOLD &&
|
||||
ABS(event_manager_globals.previous_stick_axes[_gamepad_stick_right][1][controller_index]) < STICK_EVENT_THRESHOLD) ||
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >= STICK_EVENT_REPEAT_MILLISECONDS))
|
||||
time - event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] >=
|
||||
STICK_REPEAT_INTERVAL(_gamepad_stick_right, controller_index)))
|
||||
{
|
||||
post = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
stick_note_event(_gamepad_stick_right, controller_index, x, y, time);
|
||||
#endif
|
||||
event_manager_globals.stick_event_times[_gamepad_stick_right][controller_index] = time;
|
||||
post = TRUE;
|
||||
|
||||
|
||||