Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aad697ebe |
No files matched your search
@@ -1,38 +0,0 @@
|
||||
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.
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: How to install
|
||||
url: https://github.com/Cesarus85/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
|
||||
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
|
||||
@@ -67,9 +67,7 @@ jobs:
|
||||
run: python tools/ci_build.py linux release
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pytest -q tools/test_linux_port.py
|
||||
python tools/test_light_storage.py
|
||||
run: python -m pytest -q tools/test_linux_port.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
@@ -202,9 +200,6 @@ jobs:
|
||||
- name: Release
|
||||
run: python tools/ci_build.py windows release
|
||||
|
||||
- name: Test light storage
|
||||
run: python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
|
||||
@@ -237,13 +232,6 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
port/android
|
||||
port/third_party/extract-xiso
|
||||
tools
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: halo-*
|
||||
@@ -252,18 +240,10 @@ jobs:
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir release
|
||||
# (Ringfall: Mjolnir VR publishes its Android builds only, under
|
||||
# its own name: Updater.java looks for them; the desktop ports are
|
||||
# built to keep them building)
|
||||
for config in debug release; do
|
||||
(cd "artifacts/halo-android-$config" &&
|
||||
zip -qr "$GITHUB_WORKSPACE/release/ringfall-mjolnir-vr-android-$config.zip" .)
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
done
|
||||
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
|
||||
# bundle with the release APK, and the desktop file that fetches it
|
||||
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
|
||||
# the APK itself, for sideloading on the Quest (SideQuest, adb)
|
||||
cp artifacts/halo-android-release/app-release.apk release/RingfallMjolnirVR.apk
|
||||
ls -l release
|
||||
|
||||
- name: Publish
|
||||
@@ -275,18 +255,10 @@ jobs:
|
||||
run: |
|
||||
tag="build-$GITHUB_RUN_NUMBER"
|
||||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
|
||||
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
||||
{
|
||||
printf '%s\n\n' "$subject"
|
||||
printf '**Ringfall: Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
|
||||
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
|
||||
printf '| Meta Quest 3 / 3S | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
|
||||
printf '| Steam Frame | `Install-Ringfall-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
|
||||
printf 'The other files are for the built-in updater and the Steam Frame installer.\n\n'
|
||||
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
|
||||
} > notes.md
|
||||
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
|
||||
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
|
||||
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
|
||||
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
|
||||
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Keep the last five
|
||||
@@ -341,7 +313,7 @@ jobs:
|
||||
title="Build failed"; color=15548997
|
||||
fi
|
||||
latest="$SERVER/$REPOSITORY/releases/latest"
|
||||
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||||
else
|
||||
|
||||
@@ -30,8 +30,3 @@ 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,181 +1,140 @@
|
||||
<p align="center">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
|
||||
</p>
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
|
||||
# Ringfall: Mjolnir VR
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
|
||||
**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.
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
|
||||
Ringfall: Mjolnir VR 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.
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
|
||||
> **Unofficial fan project.** Ringfall: Mjolnir VR is 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).
|
||||
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).
|
||||
|
||||
## Contents
|
||||
## Download
|
||||
|
||||
- [What you get](#what-you-get)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
|
||||
- [Install on Steam Frame](#install-on-steam-frame)
|
||||
- [Playing](#playing)
|
||||
- [Problems and questions](#problems-and-questions)
|
||||
- [Credits](#credits)
|
||||
- [Legal](#legal)
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
|
||||
## What you get
|
||||
| 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) |
|
||||
|
||||
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
|
||||
the HUD, menus, scope and crosshair float as layers of their own.
|
||||
- **Your hands in the game**: the weapon sits in your hand and fires
|
||||
where you point it; arms and fingers follow your controllers;
|
||||
two-handed weapons, melee by swinging, grenades thrown with your hand,
|
||||
crouching and leaning with your body.
|
||||
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
|
||||
turrets with handles.
|
||||
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
|
||||
left-handed mode, buttons remapped freely, graphics presets, refresh
|
||||
rate and resolution, all in the in-game VR options (**Y**).
|
||||
- **Multiplayer**: the PC version's menus and server browser, join codes,
|
||||
password lobbies, co-op campaign and bots, up to 128 players;
|
||||
cross-play with the PC and phone builds of
|
||||
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
|
||||
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.
|
||||
|
||||
## What you need
|
||||
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).
|
||||
|
||||
- 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.
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
|
||||
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.
|
||||
## Game data
|
||||
|
||||
## Install on Meta Quest 3 / 3S
|
||||
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.
|
||||
|
||||
You install the game once with a computer (Windows, macOS or Linux); after
|
||||
that it updates itself.
|
||||
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.
|
||||
|
||||
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
|
||||
app on your phone, open your headset's settings, then *Developer
|
||||
Mode* (Meta asks you to create a free developer account first).
|
||||
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
|
||||
"Advanced Installer") on your computer and connect the Quest with a
|
||||
USB cable. In the headset, allow USB debugging for this computer.
|
||||
3. **Download `RingfallMjolnirVR.apk`** from the
|
||||
[latest release](../../releases/latest) and drag it onto the SideQuest
|
||||
window. It installs the game.
|
||||
4. **Copy your disc image** to the Quest's `Download` folder: in
|
||||
SideQuest, open the file manager (the folder icon), go to `Download`
|
||||
and drag the `.iso` file in.
|
||||
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
|
||||
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
|
||||
your file in `Download`. The game copies what it needs (about 1.8 GB,
|
||||
a minute or two) and starts.
|
||||
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).
|
||||
|
||||
You can delete the disc image from `Download` afterwards. When a new
|
||||
version comes out, the game asks you at start whether to update.
|
||||
## Platforms
|
||||
|
||||
## Install on Steam Frame
|
||||
Each platform has its own instructions:
|
||||
|
||||
No computer and no keyboard needed.
|
||||
| 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) |
|
||||
|
||||
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
|
||||
*Switch to Desktop*.
|
||||
2. **Put your disc image** in the `Downloads` folder (for example from a
|
||||
USB stick or memory card with the file manager, Dolphin).
|
||||
3. **Download `Install-Ringfall-Mjolnir-VR.desktop`** from the
|
||||
[latest release](../../releases/latest) with the web browser.
|
||||
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
|
||||
If you are asked whether to run it, choose to continue.
|
||||
5. **Answer the dialogs.** The installer finds your disc image, copies
|
||||
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
|
||||
library and offers to switch back to Gaming Mode.
|
||||
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
|
||||
To update, open the same file in Desktop Mode again; your settings,
|
||||
saves and game data stay. More about the Steam Frame:
|
||||
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
|
||||
## Multiplayer
|
||||
|
||||
## Playing
|
||||
The game can play system link games on a local network and on the internet:
|
||||
|
||||
- **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.
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](port/linux/NETCODE.md).
|
||||
|
||||
## Problems and questions
|
||||
## Build the game
|
||||
|
||||
Please open an [issue](../../issues) and include:
|
||||
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).
|
||||
|
||||
- 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).
|
||||
To build the game:
|
||||
|
||||
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
|
||||
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:
|
||||
|
||||
**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.
|
||||
| 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` |
|
||||
|
||||
## Credits
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
|
||||
Ringfall: Mjolnir VR stands on the work of others:
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
|
||||
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
|
||||
Community Edition): the port of the decompiled game to Linux, Windows
|
||||
and Android, with its renderer, netcode, PC menus and multiplayer, that
|
||||
this project builds on and follows.
|
||||
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
|
||||
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
|
||||
by moistman42069 (CC0): the arm IK, the forearm's twist and the
|
||||
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
|
||||
left-handed mode and the graphics presets follow its ideas.
|
||||
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
|
||||
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
|
||||
trace markers) are used here.
|
||||
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
|
||||
image readers follow its format handling.
|
||||
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
### Build options
|
||||
|
||||
Thank you all.
|
||||
Give these options to `configure.py`:
|
||||
|
||||
## Building it
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
|
||||
`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.
|
||||
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.
|
||||
|
||||
## Legal
|
||||
### Optimization profiles
|
||||
|
||||
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
|
||||
It is not affiliated with, endorsed, sponsored or approved by Microsoft
|
||||
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
|
||||
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
|
||||
here only to say which game this mod works with. Meta Quest is a trademark
|
||||
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
|
||||
Corporation.
|
||||
The builds use profiles of the game to optimize the code:
|
||||
|
||||
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.
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
|
||||
This project's own code is released under [CC0 1.0](LICENSE.md), as the
|
||||
decompilation it builds on. Third-party code and fonts keep their own
|
||||
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Third-party notices
|
||||
|
||||
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
|
||||
It includes or builds on the following, under their own licenses; each
|
||||
license's full text is in the file named.
|
||||
|
||||
## Code
|
||||
|
||||
| Component | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) |
|
||||
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
|
||||
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
|
||||
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
|
||||
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
|
||||
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
|
||||
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
|
||||
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
|
||||
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
|
||||
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
|
||||
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
|
||||
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
|
||||
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
|
||||
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
|
||||
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
|
||||
|
||||
The disc image readers include this acknowledgement, as extract-xiso's
|
||||
license asks: *This product includes software developed by in
|
||||
<in@fishtank.com>.*
|
||||
|
||||
## Fonts
|
||||
|
||||
| Font | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
|
||||
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
|
||||
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
|
||||
|
||||
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
|
||||
each font is used.
|
||||
|
||||
## Not included
|
||||
|
||||
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
|
||||
executables) is part of this repository or its releases. The game reads
|
||||
them from the player's own disc image.
|
||||
@@ -1,145 +0,0 @@
|
||||
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
|
||||
> the port Ringfall: Mjolnir VR is built on, kept for its build and
|
||||
> development notes. Its download links are OpenCE's. For the VR mod, see
|
||||
> the [main README](../README.md).
|
||||
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
|
||||
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
|
||||
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
|
||||
## Download
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
problems.
|
||||
|
||||
The game updates itself. At start-up it looks for a newer release, and asks
|
||||
if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](../port/linux/README.md#updates).
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
|
||||
## Game data
|
||||
|
||||
The port does not include the game data. It needs an Xbox disc image
|
||||
(`.xiso` or `.iso`) made from your own copy of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](../port/android/README.md).
|
||||
|
||||
## Platforms
|
||||
|
||||
Each platform has its own instructions:
|
||||
|
||||
| Platform | Instructions |
|
||||
| --- | --- |
|
||||
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](../port/linux/README.md) |
|
||||
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](../port/windows/README.md) |
|
||||
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](../port/android/README.md) |
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](../port/linux/NETCODE.md).
|
||||
|
||||
## Build the game
|
||||
|
||||
You do not need the Xbox SDK. The port supplies the SDK declarations that
|
||||
the game uses. Refer to [port/include/xdk](../port/include/xdk/README.md).
|
||||
|
||||
To build the game:
|
||||
|
||||
1. Install Python and [ninja](https://ninja-build.org/).
|
||||
2. Install the tools for your platform. Refer to the README for the
|
||||
platform.
|
||||
3. In the root folder of the repository, enter `python configure.py`.
|
||||
4. Enter `ninja` with the target for the platform:
|
||||
|
||||
| Target | Result |
|
||||
| --- | --- |
|
||||
| `ninja linux` | `build/linux/halo` |
|
||||
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
|
||||
### Build options
|
||||
|
||||
Give these options to `configure.py`:
|
||||
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
|
||||
Without `--portable`, the Linux and Windows builds use all the instructions
|
||||
of the processor that builds them (`-march=native`). Such a build does not
|
||||
always start on a different computer.
|
||||
|
||||
### Optimization profiles
|
||||
|
||||
The builds use profiles of the game to optimize the code:
|
||||
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
@@ -1,7 +1,5 @@
|
||||
# Android
|
||||
|
||||
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
|
||||
its Java classes keep the decompilation's package, `com.halo.decomp`).
|
||||
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
|
||||
`ninja android_apk` makes an app from it:
|
||||
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
@@ -43,8 +41,8 @@ ninja) and these items:
|
||||
## Game data
|
||||
|
||||
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
|
||||
`.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`.
|
||||
`.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`.
|
||||
|
||||
To install the data with the app:
|
||||
|
||||
@@ -59,16 +57,16 @@ To install the data with the app:
|
||||
To install the data from a computer:
|
||||
|
||||
1. Start the app one time. The app makes its folders.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
|
||||
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
|
||||
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
|
||||
| --- | --- |
|
||||
| Saved games (`z:\` and `u:\`) | `save` |
|
||||
| Log | `debug.txt` |
|
||||
| Settings | `config.toml` |
|
||||
|
||||
To make a copy of the saved games, enter
|
||||
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
|
||||
`adb pull /sdcard/Android/data/com.halo.decomp/files/save`.
|
||||
|
||||
## Controls
|
||||
|
||||
@@ -102,9 +100,9 @@ not accept touch input.
|
||||
The settings are in `config.toml` in the data folder of the app. To change
|
||||
them:
|
||||
|
||||
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
|
||||
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
|
||||
2. Change the file.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
|
||||
At the first start, the game writes the file with the default values. To
|
||||
get the default values again, delete the file.
|
||||
@@ -150,22 +148,14 @@ 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 the game install apps. Allow it.
|
||||
you to let Halo 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. 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
|
||||
repository. 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/`.
|
||||
|
||||
@@ -192,7 +182,7 @@ instead.
|
||||
|
||||
Controls in play (in the menus the buttons are the Xbox's by position):
|
||||
|
||||
| Controller | Game |
|
||||
| Controller | Halo |
|
||||
| --- | --- |
|
||||
| right trigger / left trigger | fire / throw grenade |
|
||||
| right grip / left grip | melee / switch weapon |
|
||||
@@ -240,7 +230,7 @@ through a shader that decodes to linear, and the stored bytes are the
|
||||
game's. Camera shake is off in VR.
|
||||
|
||||
A file named `xr_test` in the data folder
|
||||
(`adb shell touch /sdcard/Android/data/io.github.cesarus85.ringfall/files/xr_test`)
|
||||
(`adb shell touch /sdcard/Android/data/com.halo.decomp/files/xr_test`)
|
||||
starts a self-test instead of the game, which needs no game data: each eye
|
||||
in its own colour (red while the right trigger is held), a checkerboard
|
||||
screen in the room (its top-left square red), and the controllers' buttons
|
||||
|
||||
@@ -4,43 +4,6 @@ The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
What differs is found at run time, not by a build of its own.
|
||||
|
||||
## Installing it (players)
|
||||
|
||||
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
|
||||
1. Put your own disc image of Halo: Combat Evolved for the original Xbox (`.iso` or
|
||||
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
|
||||
card).
|
||||
2. Download `Install-Ringfall-Mjolnir-VR.desktop` from the latest release and open it
|
||||
(double-click; confirm running it).
|
||||
3. Answer the dialogs: the installer finds the disc image (or asks for it),
|
||||
copies the game data out of it, adds "Ringfall: Mjolnir VR" to the Steam library
|
||||
and switches back to Gaming Mode. Start it from the library there.
|
||||
|
||||
Opening the same file again updates the game (settings, saves and game data
|
||||
stay).
|
||||
|
||||
The desktop file (`steam_frame_installer.desktop`) downloads the latest
|
||||
release's `ringfall-mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
|
||||
the release job of `.github/workflows/build.yml`) and runs its
|
||||
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
|
||||
- `~/Documents/RingfallMjolnirVR`: the launcher (`ringfall-mjolnir-vr.sh`)
|
||||
and the release APK (`RingfallMjolnirVR.apk`); the folder of the builds
|
||||
before the name, `~/Documents/HaloCE`, renamed to it, and their "Halo VR"
|
||||
shortcut replaced;
|
||||
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
|
||||
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
|
||||
- the Steam shortcut, unless it is there: added by Steam itself
|
||||
(`steam://addnonsteamgame`, as SteamOS's "Add to Steam" does) from
|
||||
`Ringfall Mjolnir VR.desktop` beside the game, then the library pictures
|
||||
under the id Steam gave it. Steam is not closed: on the Frame, Desktop
|
||||
Mode runs inside Gaming Mode's session, and closing Steam ends it (and the
|
||||
installer with it). An old "Halo VR" shortcut is left for the player to
|
||||
remove (Steam would write it back);
|
||||
- what it did in `install.log` beside the game.
|
||||
|
||||
`HALO_VR_UNATTENDED=1` (and `HALO_VR_IMAGE=<image>`) runs it without
|
||||
dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
|
||||
|
||||
## The device
|
||||
|
||||
- Steam Settings > System > Enable Developer Mode; then start "Lepton
|
||||
@@ -52,7 +15,7 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
|
||||
connected as well.
|
||||
- Install with `adb install -r --no-streaming`. It prints "Lepton post
|
||||
installation failed due to APP_PATH being unset", but installs. Start the
|
||||
game with `am start -n io.github.cesarus85.ringfall/com.halo.decomp.LauncherActivity`.
|
||||
game with `am start -n 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.
|
||||
@@ -70,7 +33,7 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | done |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/RingfallMjolnirVR` |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/HaloCE` |
|
||||
| 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 |
|
||||
@@ -80,10 +43,10 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
|
||||
|
||||
## Starting it on the Frame
|
||||
|
||||
A button in the Steam library ("Ringfall: Mjolnir VR", a non-Steam game)
|
||||
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
|
||||
A button in the Steam library ("Halo VR", a non-Steam game) runs
|
||||
`~/Documents/HaloCE/halo-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
|
||||
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.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.
|
||||
|
||||
@@ -98,7 +61,7 @@ What the script does that Steam would for an Android title:
|
||||
quit (it writes `quit_requested.txt`).
|
||||
|
||||
Steam's "Exit game" does not reach the game (its process is the container's,
|
||||
not Steam's): quit it with the VR options' "Quit the game" (Y in the main or
|
||||
not Steam's): quit it with the VR options' "Quit Halo" (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.
|
||||
|
||||
@@ -112,15 +75,13 @@ beside Lepton's `<frame-ip>:5555`.
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents` can be written, the launcher makes
|
||||
`/home/steamos/Documents/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:
|
||||
`/home/steamos/Documents/HaloCE` holds `maps/ui.map` and is writable, the
|
||||
launcher and the game take it as their data folder (config.toml, saves and
|
||||
debug.txt go there too). Once:
|
||||
|
||||
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
|
||||
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/HaloCE/maps
|
||||
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/HaloCE/maps/
|
||||
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/HaloCE
|
||||
|
||||
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
|
||||
@@ -175,3 +136,16 @@ page of buttons shows this layout on the Frame.
|
||||
`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.
|
||||
- 2026-10-05 later: the doubling was the eyes not fusing (each eye sharp
|
||||
alone) with multiview, and gone drawing the eyes one by one, which is now
|
||||
SteamVR's default (`debug.multiview_steamvr` for multiview). The per-eye
|
||||
corrections were checked against the projection (x, y from the top after
|
||||
the flip, the eyes' offsets in clip units) and agree with the Quest, where
|
||||
multiview fuses; the Frame's eyes are wider and differ from each other
|
||||
more, so a driver giving the right eye the left one's correction (zink on
|
||||
Turnip: ViewIndex, Turnip's multi-position output) would show there. For
|
||||
finding it: a self-test of the driver's views when the layers are made
|
||||
(logcat "multiview: self-test"), a frame whose views are turned apart
|
||||
drawn one by one, the views' shape in the log as it changes ("VR views:"),
|
||||
and `debug.gl_environment` ("TU_DEBUG=nomultipos", "ZINK_DEBUG=nobgc",
|
||||
"MESA_SHADER_CACHE_DISABLE=true") set before the driver starts.
|
||||
@@ -1,307 +0,0 @@
|
||||
# VR roadmap
|
||||
|
||||
What Ringfall: Mjolnir VR (Quest 3, Steam Frame) has, and what comes next, in the
|
||||
agreed order. Updated with every step: a step is **done** once the user has
|
||||
tried it in the headset and it is merged into `main`; **built** means in a
|
||||
branch, not yet tried in the headset.
|
||||
|
||||
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 | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
|
||||
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
|
||||
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
|
||||
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
|
||||
| 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 20): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
|
||||
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
|
||||
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
|
||||
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
|
||||
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
|
||||
|
||||
## Next, in order
|
||||
|
||||
| # | 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 | |
|
||||
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
|
||||
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
|
||||
| 4 | Later: disc image import in the launcher, sprint by arm swing (offline) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Sprint by arm swing still open |
|
||||
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
|
||||
|
||||
## Step 2, first part (done)
|
||||
|
||||
Left-handed mode and graphics presets, after HaloCE-Quest-VR (CC0): its
|
||||
mirrored first-person model, its graphics switches.
|
||||
|
||||
Left-handed (`vr.handedness = "left"`):
|
||||
- The weapon in the left hand: aim, shots, muzzle, laser, crosshair,
|
||||
scope disc, turret aim. The first-person model is posed as the right
|
||||
hand's in a mirror image of the room ("model space", `halo_vr.h`) and
|
||||
mirrored back; a model whose node matrices mirror has its triangles'
|
||||
winding turned over (`rasterizer_set_model_skinning`, `d3d8_gl.c`). The
|
||||
arms reach to the other shoulders, the foregrip is gripped with the
|
||||
right hand, the right hand's fingers follow its controller.
|
||||
- Grenades held and thrown with the right trigger and hand; melee by the
|
||||
left grip (a swing of either hand, as before); the motion tracker on the
|
||||
right wrist (`halo_xr_submit.wrist_hand`); the rumble's light motor in
|
||||
the weapon hand.
|
||||
- The left trigger fires, the right one throws; at the Warthog's wheel the
|
||||
left trigger accelerates, the right one brakes.
|
||||
- With `vr.mirror_controls` (default): the right stick moves, the left
|
||||
turns and steers; their clicks, the bumpers and the face buttons change
|
||||
sides (Quest: X jumps, Y light / held grenades, A action and reload, B
|
||||
switch weapon; Steam Frame: the left D-pad does A, B, X, Y as their
|
||||
mirror image, down jump, left light, right action, up switch weapon,
|
||||
and A, B, X, Y do the D-pad's). The menus keep their buttons; the
|
||||
laser pointer works from either hand as before.
|
||||
- Other machines see the shots leave the left hand (the network message
|
||||
is unchanged: aim and muzzle from the camera).
|
||||
- The assault rifle's ammo counter and compass read the right way round
|
||||
on the mirrored model (2026-10-06): their geometry stays mirrored, each
|
||||
digit's bitmap is drawn turned over in u and the digits' order across
|
||||
the display swapped (numeric shaders drawn mirrored,
|
||||
`rasterizer_xbox_transparent_geometry.c`). Checked flat in the emulator
|
||||
with `debug.mirror_first_person` (the first-person weapon mirrored off
|
||||
the headset): 60, 48, 36 ... 06 read correctly. Steam Frame good
|
||||
(2026-10-06).
|
||||
- Kept as they are (accepted 2026-10-06): the small static icons under the
|
||||
counter (round and "x") stay mirror images (a texture atlas, not
|
||||
numeric). The game's prompts still name the Xbox's buttons.
|
||||
- Settings kept: on Android `config.toml` is written beside itself and
|
||||
renamed over (`port_config.c`), so one pushed by adb (the shell's, which
|
||||
the app could not open for writing) no longer loses the VR options'
|
||||
changes at a restart.
|
||||
- Headset tests 2026-10-06: Quest 3 and Steam Frame good (left hand,
|
||||
controls mirrored and not, counter, presets, refresh rates, resolution,
|
||||
settings kept over a restart).
|
||||
|
||||
Graphics (`vr.graphics`, `port/linux/game/vr_graphics.c`), set each frame
|
||||
on the game's own switches:
|
||||
|
||||
| Preset | Leaves out |
|
||||
| --- | --- |
|
||||
| `"high"` (default) | nothing: as before |
|
||||
| `"medium"` | object shadows, environment reflections, lens flares |
|
||||
| `"low"` | also specular light, bump maps, grass and other detail objects, the screen's fog layer, contrails, weather, camouflage's second pass |
|
||||
| `"max"` | nothing; models lit for each pixel, shadows at 512x512 |
|
||||
|
||||
A client in another's game keeps grass, lens flares and the fog layer as
|
||||
the host's rules have them (`cheats.c`). Shadows, specular passes,
|
||||
reflections and detail objects are draws (CPU, which the Frame's GL
|
||||
driver is short of); bump maps, fog, camouflage and per-pixel lighting
|
||||
the GPU's fill.
|
||||
|
||||
VR options (Y): two pages, PLAY (the comfort rows, "Weapon hand",
|
||||
"Mirrored controls") and MORE ("Graphics", "Refresh rate", "Resolution",
|
||||
the controller buttons page, Quit); four since the second part (below). The refresh rate steps through what
|
||||
the runtime offers and changes at once; on the Steam Frame the row shows
|
||||
SteamOS's rate. The resolution (`vr.resolution_scale`, 70-120%) takes a
|
||||
restart. The controller buttons page shows the layout as set.
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.handedness` | `"right"` (default), `"left"` |
|
||||
| `vr.mirror_controls` | `true` (default): sticks and buttons mirrored when left-handed; `false`: as right-handed (triggers and grips still follow the hands) |
|
||||
| `vr.graphics` | `"low"`, `"medium"`, `"high"` (default), `"max"` |
|
||||
| `vr.refresh_rate` | Hz, as offered (Quest 3: 72, 80, 90, 120) |
|
||||
| `vr.resolution_scale` | 0.7 to 1.2, on restart |
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Left-handed: aim, laser, crosshair, scope of sniper and rocket
|
||||
launcher; weapon held right in the left hand, arms from the right
|
||||
shoulders, nothing inside out (faces culled the wrong way would show
|
||||
the inside of the gun or the glove).
|
||||
2. Two-handed grip with the right hand; grenades in the right hand (held
|
||||
and thrown); melee by grip and by swing; motion tracker on the right
|
||||
wrist.
|
||||
3. Buttons and sticks mirrored on the Touch and the Frame controllers
|
||||
(every function reachable), then `Mirrored controls: Off`.
|
||||
4. Vehicles: wheel, throttle and brake, steering stick, a gunner's aim;
|
||||
menus and the laser pointer; a network game (shots from the left hand
|
||||
on the other machine).
|
||||
5. Graphics: each preset's look and smoothness (`vr.log_performance`),
|
||||
in a bot game; Max on the Quest at 90 Hz; refresh rates 72/80/90/120
|
||||
on the Quest; the Frame's row.
|
||||
|
||||
## Step 2, second part (done)
|
||||
|
||||
In `main` 2026-10-06; tried on the Quest 3 (with the rename): good. The flashlight by tapping the head (after HaloCE-Quest-VR)
|
||||
did not work in the headset and was taken out again; the button stays.
|
||||
The VR
|
||||
options (Y) have four pages: PLAY ("Weapon hand", "Mirrored controls",
|
||||
laser, crosshair, motion tracker, body, fingers, the Warthog's wheel),
|
||||
COMFORT (turning, the vignettes), BUTTONS (the actions' buttons,
|
||||
"Reset buttons to defaults", the controller buttons page) and MORE (graphics, refresh rate, resolution, Quit).
|
||||
|
||||
Buttons remapped (BUTTONS page; `vr.c` `controls_remapped`):
|
||||
- Jump, the flashlight (held: switch grenades), action/reload, switch
|
||||
weapon, crouch, zoom and melee each go on one of seven buttons: A, B,
|
||||
X, Y, the move stick's click, the turning stick's, the weapon hand's
|
||||
grip. Moving an action to a button swaps it with the action there, so
|
||||
none is ever left without one.
|
||||
- The triggers, the free hand's grip, the menu button, the Steam Frame's
|
||||
bumpers, D-pad and View button keep theirs; the menus keep the buttons
|
||||
as they are.
|
||||
- Applied after the left-handed mirroring, on the gamepad's sides: the
|
||||
same on the Touch and the Frame controllers, and a left-handed player's
|
||||
buttons are the mirror image of the right-handed ones (on the Frame,
|
||||
mirrored, the left D-pad's). The defaults are the old layout, the
|
||||
host's bindings and the ABI unchanged.
|
||||
- The melee's aim and jab follow its button; the grips on a Warthog's
|
||||
wheel press nothing, whatever the grip does.
|
||||
- The controller buttons page shows each action where its button is.
|
||||
|
||||
Comfort (COMFORT page): snap turns 10 to 45 degrees by 5, 60, 75, 90;
|
||||
smooth turning 30 to 150 degrees a second by 15, then to 300 by 30; the
|
||||
vehicle vignette Off, 15 to 100%; the on-foot vignette (off by default)
|
||||
while the stick turns smoothly, or also while the move stick moves, as
|
||||
far as the sticks are pushed, with its own strength. The old values are
|
||||
among the steps; defaults unchanged.
|
||||
|
||||
Transparent geometry: room for 2048 groups a window (the Xbox's 384,
|
||||
below).
|
||||
|
||||
| Setting | Values |
|
||||
| --- | --- |
|
||||
| `vr.button_jump` | `"a"` (default), `"b"`, `"x"`, `"y"`, `"move_stick"`, `"turn_stick"`, `"grip"`: as right-handed, mirrored with `vr.mirror_controls` |
|
||||
| `vr.button_light` | the flashlight, held switch grenades; `"b"` (default) |
|
||||
| `vr.button_action` | action, reload; `"x"` (default) |
|
||||
| `vr.button_weapon` | switch weapon; `"y"` (default) |
|
||||
| `vr.button_crouch` | `"move_stick"` (default) |
|
||||
| `vr.button_zoom` | `"turn_stick"` (default) |
|
||||
| `vr.button_melee` | `"grip"` (default) |
|
||||
| `vr.foot_vignette` | `"off"` (default), `"turning"` (smooth turns), `"moving"` (and moving) |
|
||||
| `vr.foot_vignette_strength` | 0.15 to 1, `0.7` (default) |
|
||||
| `debug.vr_options` | `false` (default); `true` shows the VR options off the headset (the emulator) |
|
||||
|
||||
Checked in the emulator (`debug.vr_options`): the four pages without
|
||||
overlaps, each row's values, swaps and reset, the controller buttons page
|
||||
right- and left-handed after swaps, the settings kept over a restart, a
|
||||
15-bot game as before.
|
||||
|
||||
To try in the headset (Quest 3 and Steam Frame):
|
||||
1. Defaults: every button as before (jump, light/held grenades, action,
|
||||
switch weapon, crouch, zoom, melee by grip and by swing), right- and
|
||||
left-handed, mirrored and not; the menus' A and B.
|
||||
2. BUTTONS: swap jump and melee (jump on the grip, melee on A): both work,
|
||||
the melee jabs and aims where the head looks; light on a stick click
|
||||
(tap: light, hold: grenades); the controller buttons page says so.
|
||||
At the Warthog's wheel with jump on the grip: no jump while holding
|
||||
the wheel. Reset: all back.
|
||||
3. The same left-handed with mirrored controls (on the Frame the left
|
||||
D-pad), and with `Mirrored controls: Off`.
|
||||
4. COMFORT: snap 10 and 90, smooth 30 and 300; vehicle vignette 15% and
|
||||
100%; on-foot vignette "Smooth turns" (nothing with snap turns),
|
||||
"Moving, turning", strength 15% and 100%; no vignette in a vehicle
|
||||
from the on-foot one, none in the menus.
|
||||
5. A game of 30 and more players: effects, glass and water stay; the log
|
||||
(`adb logcat -s halo`) says `rasterizer: N transparent geometry groups
|
||||
in a window` once a window passes 384; frame time as before.
|
||||
|
||||
## Step 3a (done)
|
||||
|
||||
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.
|
||||
|
||||
## Open points outside the order
|
||||
|
||||
- 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) on
|
||||
`feature/vr-comfort`, to check in a large game (step 2, second part,
|
||||
test 6); upstream still has 384.
|
||||
|
||||
## Headset tests (2026-10-06, all three in `main`)
|
||||
|
||||
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,30 +13,16 @@ 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; 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
|
||||
// main signed with it can install over the one before; without it, 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 {
|
||||
// (the app's own id; the Java classes keep the decompilation's package)
|
||||
applicationId "io.github.cesarus85.ringfall"
|
||||
applicationId "com.halo.decomp"
|
||||
minSdk 28
|
||||
targetSdk 35
|
||||
versionCode Math.max(1, buildNumber)
|
||||
@@ -74,9 +60,9 @@ android {
|
||||
if (signingKey) {
|
||||
halo {
|
||||
storeFile file(signingKey)
|
||||
storePassword signingPassword
|
||||
keyAlias signingAlias
|
||||
keyPassword signingPassword
|
||||
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
|
||||
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- the downloaded new version, for the package installer (Updater) -->
|
||||
<provider
|
||||
android:name=".UpdateProvider"
|
||||
android:authorities="${applicationId}.update"
|
||||
android:authorities="com.halo.decomp.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/<application id>/files.
|
||||
* app's external files directory, /sdcard/Android/data/com.halo.decomp/files.
|
||||
* If it is missing, this screen lets the player pick an Xbox disc image of
|
||||
* the game (.xiso or .iso, any version) with the system file picker, and
|
||||
* copies its maps folder there (XisoExtractor), as the desktop games do; or
|
||||
@@ -35,7 +35,7 @@ import java.nio.channels.FileChannel;
|
||||
*/
|
||||
public class LauncherActivity extends Activity {
|
||||
private static final int PICK_IMAGE = 1;
|
||||
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
|
||||
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
|
||||
|
||||
private File dataRoot;
|
||||
private TextView status;
|
||||
@@ -49,15 +49,10 @@ public class LauncherActivity extends Activity {
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, 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;
|
||||
}
|
||||
// Documents instead, when it is there (port/android/STEAM_FRAME.md;
|
||||
// the same choice as host_main.c's)
|
||||
if (new File(SHARED_DATA_ROOT, "maps/ui.map").isFile() && new File(SHARED_DATA_ROOT).canWrite())
|
||||
dataRoot = new File(SHARED_DATA_ROOT);
|
||||
// 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)
|
||||
@@ -147,15 +142,15 @@ public class LauncherActivity extends Activity {
|
||||
layout.setBackgroundColor(Color.rgb(12, 16, 20));
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText("Ringfall: Mjolnir VR needs game data");
|
||||
title.setText("Halo needs its 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 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), "
|
||||
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), "
|
||||
+ "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://<application id>.update/update.apk.
|
||||
* (Updater), read-only, the one file content://com.halo.decomp.update/halo.apk.
|
||||
* (The app has no AndroidX, whose FileProvider does the same.)
|
||||
*/
|
||||
public class UpdateProvider extends ContentProvider {
|
||||
static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".update";
|
||||
static final String AUTHORITY = "com.halo.decomp.update";
|
||||
static final String DIRECTORY = "update";
|
||||
static final String APK = "update.apk";
|
||||
static final String APK = "halo.apk";
|
||||
|
||||
private File apk() {
|
||||
return new File(new File(getContext().getCacheDir(), DIRECTORY), APK);
|
||||
|
||||
@@ -49,8 +49,7 @@ import java.util.zip.ZipInputStream;
|
||||
final class Updater {
|
||||
/* the VR port's own releases: the original's, made without VR, must
|
||||
never replace it (they share the package name) */
|
||||
private static final String APP_NAME = "Ringfall: Mjolnir VR";
|
||||
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
|
||||
private static final String REPOSITORY = "Cesarus85/halo-ce-vr";
|
||||
private static final String USER_AGENT = "halo-ce-universal-updater";
|
||||
private static final int TIMEOUT_MILLISECONDS = 20000;
|
||||
|
||||
@@ -203,8 +202,8 @@ final class Updater {
|
||||
if (activity.isFinishing())
|
||||
return;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setMessage("A new version of " + APP_NAME + " was detected (build " + latest + "; this is build "
|
||||
.setTitle("Halo: new version")
|
||||
.setMessage("A new version of Halo 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)
|
||||
@@ -216,7 +215,7 @@ final class Updater {
|
||||
|
||||
private static void confirmNever(Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setTitle("Halo: new version")
|
||||
.setMessage("Stop asking about new versions?\n\nTo ask again, set auto = true in the [update] section "
|
||||
+ "of config.toml.")
|
||||
.setCancelable(false)
|
||||
@@ -233,7 +232,7 @@ final class Updater {
|
||||
/* ---------- updating */
|
||||
|
||||
private static void update(Activity activity, int latest) {
|
||||
String asset = "ringfall-mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
File directory = new File(activity.getCacheDir(), UpdateProvider.DIRECTORY);
|
||||
LinearLayout layout = new LinearLayout(activity);
|
||||
TextView status = new TextView(activity);
|
||||
@@ -248,7 +247,7 @@ final class Updater {
|
||||
layout.addView(status);
|
||||
layout.addView(bar);
|
||||
AlertDialog progress = new AlertDialog.Builder(activity)
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setTitle("Halo: new version")
|
||||
.setView(layout)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
@@ -276,7 +275,7 @@ final class Updater {
|
||||
activity.runOnUiThread(() -> {
|
||||
progress.dismiss();
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setTitle("Halo: new version")
|
||||
.setMessage("The update failed:\n\n" + e.getMessage())
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Ringfall: Mjolnir VR</string>
|
||||
<string name="app_name">Halo</string>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -135,11 +135,6 @@ into a layer's image, opaque */
|
||||
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
void host_xr_set_vignette(float strength);
|
||||
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
|
||||
it lets none be chosen), and the one it runs at (0: unknown); and asking
|
||||
for the highest offered at or below wanted (0: the highest) */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current);
|
||||
void host_xr_set_refresh_rate(float wanted);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ 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);
|
||||
|
||||
/* ---------- VR (host_xr.c) */
|
||||
|
||||
|
||||
@@ -11,52 +11,8 @@ 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)
|
||||
{
|
||||
@@ -132,13 +88,14 @@ half-drawn targets out of tile memory, for every lens flare. */
|
||||
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)
|
||||
{
|
||||
GLint read_binding = 0, write_binding = 0;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !buffer || !size)
|
||||
return;
|
||||
glGetIntegerv(GL_COPY_READ_BUFFER_BINDING, &read_binding);
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
if (!counter_snapshots[slot])
|
||||
glGenBuffers(1, &counter_snapshots[slot]);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
@@ -151,17 +108,19 @@ void host_gl_counters_snapshot(uint32_t buffer, uint32_t size, uint32_t slot)
|
||||
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);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, (GLuint)read_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
GLint write_binding = 0;
|
||||
const void *mapping;
|
||||
|
||||
if (slot >= COUNTER_SNAPSHOTS || !counter_snapshots[slot] || size > counter_snapshot_sizes[slot])
|
||||
return 0;
|
||||
glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, counter_snapshots[slot]);
|
||||
mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, GL_MAP_READ_BIT);
|
||||
if (mapping)
|
||||
@@ -169,7 +128,7 @@ int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
memcpy(out, mapping, size);
|
||||
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
|
||||
}
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
return mapping != NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ void host_fatal(const char *format, ...)
|
||||
vsnprintf(message, sizeof(message), format, arguments);
|
||||
va_end(arguments);
|
||||
__android_log_write(ANDROID_LOG_FATAL, "halo", message);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Ringfall: Mjolnir VR", message, NULL);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Halo", message, NULL);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ int host_errno(void)
|
||||
static char data_root[512];
|
||||
static char save_root[512];
|
||||
/* where a device that empties the app's folders keeps the game's data */
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/RingfallMjolnirVR"
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/HaloCE"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -205,6 +205,24 @@ static double config_number(const char *path, const char *key, double fallback)
|
||||
return value;
|
||||
}
|
||||
|
||||
/* a string from config.toml into text (size bytes), or "" when it is not
|
||||
set */
|
||||
static void config_text(const char *path, const char *key, char *text, size_t size)
|
||||
{
|
||||
toml_result_t result = toml_parse_file_ex(path);
|
||||
|
||||
text[0] = 0;
|
||||
if (!result.ok)
|
||||
return;
|
||||
{
|
||||
toml_datum_t datum = toml_seek(result.toptab, key);
|
||||
|
||||
if (datum.type == TOML_STRING && datum.u.s)
|
||||
snprintf(text, size, "%s", datum.u.s);
|
||||
}
|
||||
toml_free(result);
|
||||
}
|
||||
|
||||
/* POSIX TZ for the current local offset (the guest's musl has no zone
|
||||
database) */
|
||||
static void time_zone(char *buffer, size_t size)
|
||||
@@ -273,9 +291,9 @@ static void *game_main(void *unused)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, 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)
|
||||
instead, when it is there (port/android/STEAM_FRAME.md; the same choice
|
||||
as LauncherActivity's) */
|
||||
if (directory_has_maps(SHARED_DATA_ROOT) && access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
@@ -292,7 +310,7 @@ static void *game_main(void *unused)
|
||||
}
|
||||
if (!directory_has_maps(data_root))
|
||||
{
|
||||
host_fatal("The game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
|
||||
host_fatal("The Halo 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);
|
||||
}
|
||||
|
||||
@@ -346,6 +364,25 @@ static void *game_main(void *unused)
|
||||
if (deferred >= 0.0)
|
||||
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
|
||||
}
|
||||
{
|
||||
/* (debug.gl_environment: "NAME=value ..." for the GL driver, set
|
||||
before it starts: the Steam Frame's Mesa (zink on Turnip) reads
|
||||
ZINK_DEBUG, TU_DEBUG, MESA_SHADER_CACHE_DISABLE and the like) */
|
||||
char text[512];
|
||||
char *item, *rest = NULL;
|
||||
|
||||
config_text(path, "debug.gl_environment", text, sizeof(text));
|
||||
for (item = strtok_r(text, " ;", &rest); item; item = strtok_r(NULL, " ;", &rest))
|
||||
{
|
||||
char *equals = strchr(item, '=');
|
||||
|
||||
if (!equals || equals == item)
|
||||
continue;
|
||||
*equals = 0;
|
||||
setenv(item, equals + 1, 1);
|
||||
host_logf(HOST_LOG_INFO, "GL driver environment: %s=%s (debug.gl_environment)", item, equals + 1);
|
||||
}
|
||||
}
|
||||
{
|
||||
int eye_width, eye_height;
|
||||
|
||||
@@ -400,7 +437,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
host_logf(HOST_LOG_INFO, "Ringfall: Mjolnir VR starting");
|
||||
host_logf(HOST_LOG_INFO, "Halo for Android starting");
|
||||
host_install_signal_handlers();
|
||||
if (host_native_thread_create(game_main, NULL, MAIN_STACK_SIZE) != 0)
|
||||
host_fatal("cannot start the game thread");
|
||||
|
||||
@@ -151,11 +151,7 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
|
||||
{
|
||||
SDL_Window *object = handle_get(window, _handle_window);
|
||||
|
||||
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;
|
||||
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
}
|
||||
|
||||
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
|
||||
|
||||
@@ -105,10 +105,6 @@ static struct
|
||||
XrAction button_b; /* B on the right controller, Y on the left */
|
||||
XrAction menu;
|
||||
XrAction thumbrest;
|
||||
/* the fingers' touch, for the hands drawn (where the runtime has it):
|
||||
the thumb on the stick, a button or the rest, the index on the trigger */
|
||||
XrAction thumb_touch;
|
||||
XrAction index_touch;
|
||||
/* the Steam Frame's controllers' own (XR_VALVE_frame_controller_interaction) */
|
||||
int has_frame_controller;
|
||||
int frame_layout;
|
||||
@@ -120,10 +116,10 @@ static struct
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
/* where the head and the hands are this frame (the wrist layer) */
|
||||
/* where the head and the left hand are this frame (the wrist layer) */
|
||||
XrPosef head;
|
||||
XrPosef grips[HALO_XR_HAND_COUNT];
|
||||
int grips_valid[HALO_XR_HAND_COUNT];
|
||||
XrPosef left_grip;
|
||||
int left_grip_valid;
|
||||
|
||||
int has_performance_settings;
|
||||
/* (XR_META_performance_metrics: the compositor's counts, in the log,
|
||||
@@ -589,51 +585,22 @@ struct binding
|
||||
const char *path;
|
||||
};
|
||||
|
||||
/* a profile's bindings: those it has to have, and those it may (a path the
|
||||
runtime does not know refuses the whole suggestion, which leaves the
|
||||
profile with none: then the first alone, as before). Nonzero if taken. */
|
||||
#define MAXIMUM_SUGGESTED_BINDINGS 64
|
||||
|
||||
static int suggest_bindings(const char *profile, const struct binding *bindings, int count,
|
||||
const struct binding *optional, int optional_count)
|
||||
static void suggest(const char *profile, const struct binding *bindings, int count)
|
||||
{
|
||||
XrActionSuggestedBinding suggested[MAXIMUM_SUGGESTED_BINDINGS];
|
||||
XrActionSuggestedBinding suggested[32];
|
||||
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||||
XrResult result;
|
||||
int index, total = 0;
|
||||
int index;
|
||||
|
||||
if (count + optional_count > MAXIMUM_SUGGESTED_BINDINGS)
|
||||
for (index = 0; index < count && index < 32; index++)
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "xr: %d bindings for %s, room for %d: the rest left out", count + optional_count,
|
||||
profile, MAXIMUM_SUGGESTED_BINDINGS);
|
||||
}
|
||||
for (index = 0; index < count + optional_count && total < MAXIMUM_SUGGESTED_BINDINGS; index++)
|
||||
{
|
||||
const struct binding *binding = index < count ? &bindings[index] : &optional[index - count];
|
||||
|
||||
suggested[total].action = *binding->action;
|
||||
suggested[total].binding = path_from_string(binding->path);
|
||||
total++;
|
||||
suggested[index].action = *bindings[index].action;
|
||||
suggested[index].binding = path_from_string(bindings[index].path);
|
||||
}
|
||||
information.interactionProfile = path_from_string(profile);
|
||||
information.suggestedBindings = suggested;
|
||||
information.countSuggestedBindings = (uint32_t)total;
|
||||
result = xrSuggestInteractionProfileBindings(xr.instance, &information);
|
||||
if (XR_SUCCEEDED(result))
|
||||
return 1;
|
||||
if (optional_count > 0)
|
||||
{
|
||||
host_logf(HOST_LOG_WARN, "xr: %s refused with its optional bindings (%d); suggested without them", profile,
|
||||
(int)result);
|
||||
return suggest_bindings(profile, bindings, count, NULL, 0);
|
||||
}
|
||||
host_logf(HOST_LOG_WARN, "xr: no bindings for %s (%d)", profile, (int)result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void suggest(const char *profile, const struct binding *bindings, int count)
|
||||
{
|
||||
suggest_bindings(profile, bindings, count, NULL, 0);
|
||||
information.countSuggestedBindings = (uint32_t)index;
|
||||
if (!XR_SUCCEEDED(xrSuggestInteractionProfileBindings(xr.instance, &information)))
|
||||
host_logf(HOST_LOG_WARN, "xr: no bindings for %s", profile);
|
||||
}
|
||||
|
||||
static int actions_initialize(void)
|
||||
@@ -660,8 +627,6 @@ static int actions_initialize(void)
|
||||
xr.button_b = action_create("button_upper", "B / Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.menu = action_create("menu", "Menu", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumbrest = action_create("thumbrest", "Thumb rest", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.thumb_touch = action_create("thumb_touch", "Thumb resting", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.index_touch = action_create("index_touch", "Finger on the trigger", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_x = action_create("button_x", "X", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.button_y = action_create("button_y", "Y", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.view = action_create("view", "View", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
@@ -767,62 +732,13 @@ static int actions_initialize(void)
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
/* the fingers' touch, as each profile has it (after HaloCE-Quest-VR,
|
||||
CC0; the Frame's from Valve's "Steam Frame Input" page): optional, a
|
||||
profile refusing them keeps its buttons */
|
||||
static const struct binding touch_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbrest/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbrest/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/x/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/y/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
static const struct binding index_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/trackpad/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/trackpad/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/b/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
static const struct binding frame_fingers[] =
|
||||
{
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/a/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/b/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/x/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/right/input/y/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_up/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_down/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_left/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/dpad_right/touch" },
|
||||
{ &xr.thumb_touch, "/user/hand/left/input/view/touch" },
|
||||
{ &xr.index_touch, "/user/hand/left/input/trigger/touch" },
|
||||
{ &xr.index_touch, "/user/hand/right/input/trigger/touch" },
|
||||
};
|
||||
|
||||
if (xr.has_frame_controller)
|
||||
{
|
||||
suggest_bindings("/interaction_profiles/valve/frame_controller_valve", frame_controller,
|
||||
sizeof(frame_controller) / sizeof(frame_controller[0]), frame_fingers,
|
||||
sizeof(frame_fingers) / sizeof(frame_fingers[0]));
|
||||
suggest("/interaction_profiles/valve/frame_controller_valve", frame_controller,
|
||||
sizeof(frame_controller) / sizeof(frame_controller[0]));
|
||||
}
|
||||
suggest_bindings("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]),
|
||||
touch_fingers, sizeof(touch_fingers) / sizeof(touch_fingers[0]));
|
||||
suggest_bindings("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]),
|
||||
index_fingers, sizeof(index_fingers) / sizeof(index_fingers[0]));
|
||||
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
|
||||
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
|
||||
suggest("/interaction_profiles/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
|
||||
}
|
||||
|
||||
@@ -865,21 +781,6 @@ static int action_boolean(XrAction action, int hand)
|
||||
return state.currentState != XR_FALSE;
|
||||
}
|
||||
|
||||
/* (and whether the runtime gives the action for the hand at all) */
|
||||
static int action_boolean_active(XrAction action, int hand, int *active)
|
||||
{
|
||||
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
|
||||
XrActionStateBoolean state = { XR_TYPE_ACTION_STATE_BOOLEAN };
|
||||
|
||||
*active = 0;
|
||||
information.action = action;
|
||||
information.subactionPath = xr.hands[hand].path;
|
||||
if (!XR_SUCCEEDED(xrGetActionStateBoolean(xr.session, &information, &state)) || !state.isActive)
|
||||
return 0;
|
||||
*active = 1;
|
||||
return state.currentState != XR_FALSE;
|
||||
}
|
||||
|
||||
static int action_vector(XrAction action, int hand, float vector[2])
|
||||
{
|
||||
XrActionStateGetInfo information = { XR_TYPE_ACTION_STATE_GET_INFO };
|
||||
@@ -946,8 +847,11 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
{
|
||||
out->flags = flags_from_location(location.locationFlags);
|
||||
pose_from_xr(&out->grip, &location.pose);
|
||||
xr.grips[hand] = location.pose;
|
||||
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
if (hand == HALO_XR_HAND_LEFT)
|
||||
{
|
||||
xr.left_grip = location.pose;
|
||||
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
}
|
||||
location.locationFlags = 0;
|
||||
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
|
||||
@@ -977,17 +881,6 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
(action_boolean(xr.dpad[1], hand) ? HALO_XR_BUTTON_DPAD_DOWN : 0) |
|
||||
(action_boolean(xr.dpad[2], hand) ? HALO_XR_BUTTON_DPAD_LEFT : 0) |
|
||||
(action_boolean(xr.dpad[3], hand) ? HALO_XR_BUTTON_DPAD_RIGHT : 0);
|
||||
{
|
||||
/* the fingers' touch, and whether there is any */
|
||||
int thumb_active = 0, index_active = 0;
|
||||
|
||||
if (action_boolean_active(xr.thumb_touch, hand, &thumb_active))
|
||||
out->buttons |= HALO_XR_BUTTON_THUMB_TOUCH;
|
||||
if (action_boolean_active(xr.index_touch, hand, &index_active))
|
||||
out->buttons |= HALO_XR_BUTTON_INDEX_TOUCH;
|
||||
if (thumb_active)
|
||||
out->buttons |= HALO_XR_BUTTON_TOUCH_KNOWN;
|
||||
}
|
||||
}
|
||||
frame->controller_layout = xr.frame_layout ? HALO_XR_LAYOUT_FRAME : HALO_XR_LAYOUT_TOUCH;
|
||||
if (xr.frame_layout)
|
||||
@@ -1045,9 +938,7 @@ On the back of the left wrist, like a watch: behind the palm, where the
|
||||
grip pose is, and out of the back of the hand. In the left grip pose -Z
|
||||
runs to the fingers, +Y up the handle and -X out of the back of the hand;
|
||||
the layer faces -X with its top towards the elbow, as the wrist turned to
|
||||
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
|
||||
right wrist, as the mirror image: there +X runs out of the back of the
|
||||
hand. */
|
||||
the eyes reads it. Shown only while it is. */
|
||||
|
||||
#define WRIST_IMAGE_SIZE 512
|
||||
#define CROSSHAIR_IMAGE_SIZE 256
|
||||
@@ -1087,32 +978,26 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
|
||||
|
||||
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
|
||||
{
|
||||
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z; in the
|
||||
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
|
||||
way round: its x turned back) */
|
||||
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
|
||||
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
|
||||
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
|
||||
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z */
|
||||
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
|
||||
WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
|
||||
/* clockwise as read: about the layer's normal, negatively (the mirror
|
||||
image's the other way) */
|
||||
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
/* clockwise as read: about the layer's normal, negatively */
|
||||
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
|
||||
XrQuaternionf unturned;
|
||||
XrVector3f moved, normal, to_eyes;
|
||||
float distance;
|
||||
|
||||
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
|
||||
if (!xr.left_grip_valid)
|
||||
return 0;
|
||||
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
|
||||
pose->position.x = grip->position.x + moved.x;
|
||||
pose->position.y = grip->position.y + moved.y;
|
||||
pose->position.z = grip->position.z + moved.z;
|
||||
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
|
||||
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
|
||||
pose->position.x = xr.left_grip.position.x + moved.x;
|
||||
pose->position.y = xr.left_grip.position.y + moved.y;
|
||||
pose->position.z = xr.left_grip.position.z + moved.z;
|
||||
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
|
||||
quaternion_multiply(&unturned, &turn, &pose->orientation);
|
||||
|
||||
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
|
||||
@@ -1232,7 +1117,7 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
android.applicationVM = machine;
|
||||
android.applicationActivity = activity;
|
||||
instance.next = &android;
|
||||
strcpy(instance.applicationInfo.applicationName, "Ringfall Mjolnir VR");
|
||||
strcpy(instance.applicationInfo.applicationName, "Halo VR");
|
||||
instance.applicationInfo.applicationVersion = 1;
|
||||
strcpy(instance.applicationInfo.engineName, "halo-ce-universal");
|
||||
instance.applicationInfo.engineVersion = 1;
|
||||
@@ -1375,10 +1260,6 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, &info->refresh_rate);
|
||||
}
|
||||
|
||||
/* (the runtime's GL binding brings contexts of its own, with their own
|
||||
trace markers: host_gl.c) */
|
||||
host_gl_quiet_trace_markers();
|
||||
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
PFN_xrEnumeratePerformanceMetricsCounterPathsMETA enumerate = NULL;
|
||||
@@ -1560,7 +1441,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
|
||||
pose_from_xr(&frame->head, &head.pose);
|
||||
xr.head = head.pose;
|
||||
}
|
||||
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
|
||||
xr.left_grip_valid = 0;
|
||||
if (xr.focused)
|
||||
hands_read(frame);
|
||||
return 1;
|
||||
@@ -2052,34 +1933,6 @@ void host_xr_set_vignette(float strength)
|
||||
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
|
||||
}
|
||||
|
||||
/* the VR options' refresh rates: those the runtime offers (none where it
|
||||
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
|
||||
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
float offered[16];
|
||||
uint32_t count = 0, index;
|
||||
|
||||
*current = 0.0f;
|
||||
if (!xr.initialized)
|
||||
return 0;
|
||||
if (xr.has_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, current);
|
||||
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
|
||||
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
|
||||
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
|
||||
return 0;
|
||||
for (index = 0; index < count && (int)index < maximum; index++)
|
||||
rates[index] = offered[index];
|
||||
return (int)index;
|
||||
}
|
||||
|
||||
void host_xr_set_refresh_rate(float wanted)
|
||||
{
|
||||
if (xr.initialized)
|
||||
refresh_rate_choose(wanted);
|
||||
}
|
||||
|
||||
void host_xr_haptic(int hand, float amplitude, float seconds)
|
||||
{
|
||||
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
|
||||
@@ -2167,7 +2020,7 @@ void host_xr_test(void)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||
window = SDL_CreateWindow("Ringfall: Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
window = SDL_CreateWindow("Halo", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
context = window ? SDL_GL_CreateContext(window) : NULL;
|
||||
if (!context || !SDL_GL_MakeCurrent(window, context))
|
||||
host_fatal("VR test: no OpenGL ES context: %s", SDL_GetError());
|
||||
|
||||
@@ -61,8 +61,6 @@ host_xr_end_frame
|
||||
host_xr_haptic
|
||||
host_xr_set_vignette
|
||||
host_xr_copy_layer_array
|
||||
host_xr_refresh_rates
|
||||
host_xr_set_refresh_rate
|
||||
host_android_path
|
||||
|
||||
host_errno
|
||||
@@ -24,7 +24,7 @@ enum
|
||||
HALO_XR_LAYER_EYE_LEFT = 0,
|
||||
HALO_XR_LAYER_EYE_RIGHT = 1,
|
||||
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left 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 */
|
||||
@@ -63,12 +63,6 @@ enum
|
||||
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 */
|
||||
@@ -155,18 +149,15 @@ struct halo_xr_submit
|
||||
float overlay_width; /* metres; the height follows from 4:3 */
|
||||
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
|
||||
struct halo_xr_pose screen;
|
||||
/* the wrist layer was filled: shown on the back of the left wrist (of
|
||||
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
|
||||
(metres), while the wrist is turned to the eyes */
|
||||
/* the wrist layer was filled: shown on the back of the left wrist,
|
||||
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) */
|
||||
/* its placement as the wrist turned to the eyes reads it: turned this
|
||||
many degrees clockwise, and moved this far right and down (metres) */
|
||||
float wrist_turn;
|
||||
float wrist_right;
|
||||
float wrist_down;
|
||||
int wrist_hand;
|
||||
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
|
||||
space, facing back along the weapon */
|
||||
int scope;
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 22 KiB |
@@ -1,288 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Ringfall: Mjolnir VR's installer for the Steam Frame, run in its Desktop
|
||||
# Mode from the bundle the release's "Install Ringfall: Mjolnir VR" desktop
|
||||
# file downloads (steam_frame_installer.desktop; the bundle: this script,
|
||||
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
|
||||
# steam_frame_xiso.py and the library pictures in grid/). Everything is
|
||||
# answered with dialogs, no keyboard:
|
||||
# - the game in ~/Documents/RingfallMjolnirVR (port/android/STEAM_FRAME.md),
|
||||
# its APK replaced (Lepton installs a newer one at the next start), so
|
||||
# running the installer again updates the game; the folder of the builds
|
||||
# before the name, ~/Documents/HaloCE, becomes it;
|
||||
# - the game data: the maps folder copied out of a disc image of the
|
||||
# original Xbox game (.iso or .xiso) found in Downloads, Documents, the
|
||||
# Desktop or on a memory card, or chosen, unless it is there already;
|
||||
# - the game in the Steam library, with its pictures, unless it is there
|
||||
# already: added by Steam itself (steam://addnonsteamgame, as SteamOS's
|
||||
# "Add to Steam" does), from a desktop file beside the game. Steam is not
|
||||
# closed: it keeps its shortcuts in memory and writes them back, and on the
|
||||
# Steam Frame Desktop Mode runs inside Gaming Mode's session, which ends
|
||||
# with Steam. The shortcut of the builds before the name ("Halo VR") is
|
||||
# left for the player to remove (Steam would write it back).
|
||||
# RINGFALL_UNATTENDED=1 answers every question with its default and prints
|
||||
# instead of showing dialogs (RINGFALL_IMAGE: the disc image to use), for
|
||||
# testing over adb; it never closes Steam.
|
||||
|
||||
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
|
||||
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
|
||||
STEAM_DIR="$HOME/.local/share/Steam"
|
||||
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
|
||||
TITLE="Ringfall: Mjolnir VR"
|
||||
NAME="Ringfall: Mjolnir VR"
|
||||
APK_NAME="RingfallMjolnirVR.apk"
|
||||
LAUNCHER="ringfall-mjolnir-vr.sh"
|
||||
EXE="\"$GAME_DIR/$LAUNCHER\""
|
||||
# (the desktop file Steam adds the game from)
|
||||
NAME_FILE="Ringfall Mjolnir VR"
|
||||
# the builds before the name: their folder and Steam shortcut
|
||||
OLD_DIR="$HOME/Documents/HaloCE"
|
||||
OLD_NAME="Halo VR"
|
||||
|
||||
# what the installer did, for looking into problems: written to /tmp while
|
||||
# it runs, then kept beside the game as install.log
|
||||
LOG="/tmp/ringfall-mjolnir-vr-install.log"
|
||||
: > "$LOG"
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
|
||||
trap keep_log EXIT
|
||||
log "installer from $BUNDLE"
|
||||
|
||||
# whether Steam runs: its pid file (~/.steam/steam.pid), or its client's
|
||||
# process (ubuntu12_32/steam: the process is not always named "steam")
|
||||
steam_running()
|
||||
{
|
||||
local pid
|
||||
pid="$(cat "$HOME/.steam/steam.pid" 2>/dev/null)"
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
|
||||
}
|
||||
|
||||
unattended() { [ "${RINGFALL_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
|
||||
|
||||
info()
|
||||
{
|
||||
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
|
||||
}
|
||||
|
||||
fail()
|
||||
{
|
||||
log "error: $1"
|
||||
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# yes/no; unattended: the default given ($2, "yes" or "no")
|
||||
ask()
|
||||
{
|
||||
if unattended; then [ "$2" = yes ]; return; fi
|
||||
zenity --question --title="$TITLE" --width=460 --text="$1"
|
||||
}
|
||||
|
||||
# the disc image the player chooses
|
||||
choose_image()
|
||||
{
|
||||
if unattended; then return 1; fi
|
||||
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved for the Xbox (.iso or .xiso)" \
|
||||
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
|
||||
--file-filter="All files | *"
|
||||
}
|
||||
|
||||
# the disc images near at hand, largest first (the game's image is a few GB;
|
||||
# anything under 500 MB is not one)
|
||||
find_images()
|
||||
{
|
||||
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
|
||||
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
|
||||
sort -rn | cut -f2-
|
||||
}
|
||||
|
||||
extract_maps()
|
||||
{
|
||||
local image="$1" log
|
||||
log="$(mktemp)"
|
||||
if unattended; then
|
||||
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" | grep '^#' | tail -n 1
|
||||
status=${PIPESTATUS[0]}
|
||||
else
|
||||
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" |
|
||||
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
|
||||
--text="Copying the game data out of the disc image..."
|
||||
status=${PIPESTATUS[0]}
|
||||
fi
|
||||
if [ "$status" != 0 ]; then
|
||||
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
|
||||
rm -f "$log"
|
||||
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
|
||||
fi
|
||||
rm -f "$log"
|
||||
}
|
||||
|
||||
# the Steam users' shortcut files (userdata/0 is no user)
|
||||
shortcut_files()
|
||||
{
|
||||
local config
|
||||
for config in "$STEAM_DIR"/userdata/*/config; do
|
||||
case "$config" in */userdata/0/*) continue ;; esac
|
||||
[ -f "$config/shortcuts.vdf" ] && echo "$config/shortcuts.vdf"
|
||||
done
|
||||
}
|
||||
|
||||
# whether a Steam user's library holds the launcher given
|
||||
have_shortcut_to()
|
||||
{
|
||||
local file
|
||||
while read -r file; do
|
||||
grep -qaF "$1" "$file" && return 0
|
||||
done < <(shortcut_files)
|
||||
return 1
|
||||
}
|
||||
|
||||
have_shortcut() { have_shortcut_to "$GAME_DIR/$LAUNCHER"; }
|
||||
|
||||
# the id Steam gave the shortcut whose Exe is $2, in shortcuts.vdf $1 (Steam
|
||||
# picks it; its library pictures in config/grid are named after it)
|
||||
shortcut_id()
|
||||
{
|
||||
python3 - "$1" "$2" <<'PYTHON'
|
||||
import struct, sys
|
||||
|
||||
path, exe = sys.argv[1], sys.argv[2].encode()
|
||||
|
||||
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
|
||||
def read_map(data, at):
|
||||
items = []
|
||||
while True:
|
||||
kind = data[at]; at += 1
|
||||
if kind == 0x08:
|
||||
return items, at
|
||||
end = data.index(b"\0", at); key = data[at:end].lower(); at = end + 1
|
||||
if kind == 0x00:
|
||||
value, at = read_map(data, at)
|
||||
elif kind == 0x01:
|
||||
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
|
||||
elif kind == 0x02:
|
||||
value = struct.unpack_from("<I", data, at)[0]; at += 4
|
||||
else:
|
||||
raise ValueError("unknown VDF type %d" % kind)
|
||||
items.append((key, value))
|
||||
|
||||
with open(path, "rb") as file:
|
||||
top, _ = read_map(file.read(), 0)
|
||||
for key, shortcuts in top:
|
||||
if key == b"shortcuts":
|
||||
for _, entry in shortcuts:
|
||||
fields = dict(entry)
|
||||
if fields.get(b"exe") == exe and b"appid" in fields:
|
||||
print(fields[b"appid"])
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
PYTHON
|
||||
}
|
||||
|
||||
# the game added to the library of the Steam user logged in, by Steam (it
|
||||
# must run), from a desktop file beside the game; then its pictures
|
||||
add_shortcut()
|
||||
{
|
||||
local desktop="$GAME_DIR/$NAME_FILE.desktop" url file id
|
||||
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
|
||||
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
|
||||
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
|
||||
log "asking Steam: $url"
|
||||
# (Steam's own "Add to Steam" touches this first: steamos-add-to-steam)
|
||||
touch /tmp/addnonsteamgamefile
|
||||
steam "$url" >/dev/null 2>&1 &
|
||||
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
|
||||
have_shortcut || return 1
|
||||
while read -r file; do
|
||||
id="$(shortcut_id "$file" "$EXE")" || continue
|
||||
log "shortcut $id in $file"
|
||||
mkdir -p "$(dirname "$file")/grid"
|
||||
cp "$BUNDLE/grid/library.png" "$(dirname "$file")/grid/${id}.png"
|
||||
cp "$BUNDLE/grid/libraryp.png" "$(dirname "$file")/grid/${id}p.png"
|
||||
cp "$BUNDLE/grid/library_hero.png" "$(dirname "$file")/grid/${id}_hero.png"
|
||||
cp "$BUNDLE/grid/library_logo.png" "$(dirname "$file")/grid/${id}_logo.png"
|
||||
done < <(shortcut_files)
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- the game
|
||||
|
||||
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
|
||||
for file in "$APK_NAME" "$LAUNCHER" steam_frame_xiso.py grid/library.png; do
|
||||
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
|
||||
done
|
||||
|
||||
# the builds before the name: their folder becomes the game's (settings,
|
||||
# saves and game data kept), without their APK and launcher
|
||||
if [ -d "$OLD_DIR" ] && [ ! -e "$GAME_DIR" ]; then
|
||||
log "renaming $OLD_DIR to $GAME_DIR"
|
||||
mv "$OLD_DIR" "$GAME_DIR" || fail "Could not rename $OLD_DIR to $GAME_DIR."
|
||||
rm -f "$GAME_DIR/HaloVR.apk" "$GAME_DIR/halo-vr.sh"
|
||||
fi
|
||||
|
||||
if [ -f "$GAME_DIR/$APK_NAME" ] || [ -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
ask "$NAME is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
|
||||
else
|
||||
ask "This installs $NAME on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$GAME_DIR/maps" || fail "Could not create $GAME_DIR."
|
||||
# (the game runs in Lepton's container as a user of its own)
|
||||
chmod 777 "$GAME_DIR" "$GAME_DIR/maps"
|
||||
cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCHER.tmp" &&
|
||||
mv -f "$GAME_DIR/$LAUNCHER.tmp" "$GAME_DIR/$LAUNCHER" || fail "Could not write $GAME_DIR/$LAUNCHER."
|
||||
# (newer than the installed one: Lepton installs it at the next start)
|
||||
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
|
||||
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
|
||||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
|
||||
|
||||
# --- the game data
|
||||
|
||||
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
image="${RINGFALL_IMAGE:-}"
|
||||
if [ -z "$image" ]; then
|
||||
mapfile -t images < <(find_images)
|
||||
for candidate in "${images[@]}"; do
|
||||
if ask "Use this disc image?\n\n$candidate" yes; then
|
||||
image="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -z "$image" ]; then
|
||||
if [ ${#images[@]} = 0 ]; then
|
||||
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
|
||||
fi
|
||||
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
|
||||
fi
|
||||
log "extracting $image"
|
||||
extract_maps "$image"
|
||||
chmod -R a+rwX "$GAME_DIR/maps"
|
||||
image_note="\n\nThe disc image itself is no longer needed:\n$image"
|
||||
fi
|
||||
|
||||
# --- Steam
|
||||
|
||||
if have_shortcut; then
|
||||
log "the Steam shortcut is there"
|
||||
elif unattended; then
|
||||
echo "info: the Steam shortcut is missing (not added unattended)"
|
||||
elif ! steam_running; then
|
||||
info "Steam is not running, so $NAME could not be added to your library.\n\nStart Steam and run the installer again."
|
||||
else
|
||||
add_shortcut || fail "Steam did not add $NAME to your library. Run the installer again; if it happens again, add\n$GAME_DIR/$NAME_FILE.desktop\nwith Steam's \"Add a Non-Steam Game\"."
|
||||
fi
|
||||
|
||||
if unattended; then
|
||||
echo "info: installed in $GAME_DIR"
|
||||
exit 0
|
||||
fi
|
||||
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
|
||||
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
|
||||
fi
|
||||
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
|
||||
steamos-session-select gamescope >/dev/null 2>&1 ||
|
||||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
[Desktop Entry]
|
||||
# Ringfall: Mjolnir VR's installer for the Steam Frame
|
||||
# (port/android/STEAM_FRAME.md): a release asset,
|
||||
# "Install-Ringfall-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
|
||||
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
|
||||
# the repository) and runs its installer, steam_frame_install.sh.
|
||||
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
|
||||
# escaped)
|
||||
Type=Application
|
||||
Name=Install Ringfall: Mjolnir VR
|
||||
Comment=Installs Ringfall: Mjolnir VR for the Steam Frame, or updates it
|
||||
Icon=steam
|
||||
Terminal=false
|
||||
Exec=bash -c "if curl -fL --retry 3 -o /tmp/ringfall-mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/ringfall-mjolnir-vr-steam-frame.zip && rm -rf /tmp/ringfall-mjolnir-vr-install && python3 -m zipfile -e /tmp/ringfall-mjolnir-vr-steam-frame.zip /tmp/ringfall-mjolnir-vr-install; then exec bash /tmp/ringfall-mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Ringfall: Mjolnir VR' --text='Ringfall: Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Ringfall: Mjolnir VR on the Steam Frame: Lepton runs the APK in a container of its own,
|
||||
# Halo 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.
|
||||
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
|
||||
# (a container of its own, not the shortcut's id: with that, SteamVR's
|
||||
# client in the container waited on xrCreateInstance)
|
||||
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
|
||||
@@ -14,11 +14,10 @@ export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/
|
||||
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
|
||||
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
|
||||
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
|
||||
DATA="$HOME/Documents/RingfallMjolnirVR"
|
||||
APK="$DATA/RingfallMjolnirVR.apk"
|
||||
LOG="$DATA/launcher.log"
|
||||
APK="$HOME/Documents/HaloCE/HaloVR.apk"
|
||||
LOG="$HOME/Documents/HaloCE/launcher.log"
|
||||
# (written by the game when the player quits it: no start again)
|
||||
QUIT="$DATA/quit_requested.txt"
|
||||
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
|
||||
rm -f "$QUIT"
|
||||
CONTEXT="steamlaunch-$SteamAppId"
|
||||
|
||||
@@ -50,7 +49,7 @@ setsid bash -c '
|
||||
|
||||
# the game's log while it runs (Lepton's logcat of its container), for
|
||||
# looking into problems: logcat.txt, anew each start
|
||||
LOGCAT="$DATA/logcat.txt"
|
||||
LOGCAT="$HOME/Documents/HaloCE/logcat.txt"
|
||||
: > "$LOGCAT"
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
|
||||
disc's ".iso") on the Steam Frame's SteamOS side, for its installer
|
||||
(steam_frame_install.sh). It reads the image as the app's own import does
|
||||
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
|
||||
the desktop games' is port/linux/src/xiso.c).
|
||||
|
||||
steam_frame_xiso.py <image> <destination>
|
||||
|
||||
writes <destination>/maps (through maps.partial, so an interrupted copy
|
||||
never leaves maps/ui.map behind) and prints its progress for zenity
|
||||
--progress: a percentage on a line of its own, and "# <text>" lines. On
|
||||
failure it prints "error: <reason the player can act on>" to stderr and
|
||||
exits 1.
|
||||
|
||||
The image's file system is XDVDFS, read as extract-xiso does
|
||||
(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format
|
||||
handling this follows; its license is below and in
|
||||
port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume
|
||||
descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and
|
||||
gives the root directory's sector and size; and directories whose entries
|
||||
form a binary tree (each entry: left and right subtree offsets in 4-byte
|
||||
units, the start sector, the size, attributes, the name's length and the
|
||||
name). Images made from a whole disc put the game partition further in, at
|
||||
one of the offsets below.
|
||||
|
||||
Some parts of this code are copyright in@fishtank.com. This product
|
||||
includes software developed by in <in@fishtank.com>.
|
||||
|
||||
Copyright (c) 2003 in <in@fishtank.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
|
||||
This product includes software developed by in <in@fishtank.com>.
|
||||
|
||||
4. Neither the name of "in" nor the email address "in@fishtank.com"
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
SECTOR_SIZE = 2048
|
||||
VOLUME_DESCRIPTOR_OFFSET = 0x10000
|
||||
ENTRY_HEADER_SIZE = 14
|
||||
ATTRIBUTE_DIRECTORY = 0x10
|
||||
# directory tables are a few sectors; anything much larger is not one
|
||||
MAXIMUM_DIRECTORY_SIZE = 4 << 20
|
||||
MAXIMUM_FILES = 256
|
||||
COPY_BUFFER_SIZE = 1 << 20
|
||||
VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA"
|
||||
# where the game partition starts: a plain image, and whole-disc images
|
||||
# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets)
|
||||
PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000)
|
||||
|
||||
|
||||
class ExtractError(Exception):
|
||||
"""a reason the player can act on"""
|
||||
|
||||
|
||||
def read_at(image, offset, size):
|
||||
image.seek(offset)
|
||||
data = image.read(size)
|
||||
if len(data) != size:
|
||||
raise ExtractError("Could not read the disc image (is it complete?).")
|
||||
return data
|
||||
|
||||
|
||||
def find_volume(image):
|
||||
"""the game partition's offset, and the root directory's sector and size"""
|
||||
for offset in PARTITION_OFFSETS:
|
||||
try:
|
||||
descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE)
|
||||
except ExtractError:
|
||||
continue
|
||||
if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC:
|
||||
continue
|
||||
sector, size = struct.unpack_from("<II", descriptor, 20)
|
||||
return offset, sector, size
|
||||
raise ExtractError("This is not an Xbox disc image.")
|
||||
|
||||
|
||||
def read_directory(image, partition, sector, size, what):
|
||||
if size <= 0 or size > MAXIMUM_DIRECTORY_SIZE:
|
||||
raise ExtractError(what)
|
||||
return read_at(image, partition + sector * SECTOR_SIZE, size)
|
||||
|
||||
|
||||
def walk(table, offset, depth, directories, out, visited):
|
||||
"""collects the entries (name, sector, size) of the subtree at offset
|
||||
(4-byte units), files or directories as asked"""
|
||||
at = offset * 4
|
||||
# a malformed table must not loop or run off its end
|
||||
visited[0] += 1
|
||||
if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table):
|
||||
return
|
||||
left, right, sector, size, attributes, name_length = struct.unpack_from("<HHIIBB", table, at)
|
||||
# 0xFFFF: padding, an empty directory
|
||||
if left == 0xFFFF:
|
||||
return
|
||||
if left:
|
||||
walk(table, left, depth + 1, directories, out, visited)
|
||||
is_directory = bool(attributes & ATTRIBUTE_DIRECTORY)
|
||||
if (at + ENTRY_HEADER_SIZE + name_length <= len(table) and name_length > 0
|
||||
and is_directory == directories and len(out) < MAXIMUM_FILES):
|
||||
name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1")
|
||||
# (as extract-xiso refuses them: no name may leave the folder)
|
||||
if name not in (".", "..") and "/" not in name and "\\" not in name:
|
||||
out.append((name, sector, size))
|
||||
if right:
|
||||
walk(table, right, depth + 1, directories, out, visited)
|
||||
|
||||
|
||||
def extract(image_path, destination):
|
||||
with open(image_path, "rb") as image:
|
||||
partition, root_sector, root_size = find_volume(image)
|
||||
|
||||
# the root's maps folder
|
||||
table = read_directory(image, partition, root_sector, root_size,
|
||||
"The disc image's file system is damaged.")
|
||||
directories = []
|
||||
walk(table, 0, 0, True, directories, [0])
|
||||
maps = [entry for entry in directories if entry[0].lower() == "maps"]
|
||||
if not maps:
|
||||
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
|
||||
|
||||
# its files
|
||||
table = read_directory(image, partition, maps[-1][1], maps[-1][2],
|
||||
"The disc image's maps folder is damaged.")
|
||||
files = []
|
||||
walk(table, 0, 0, False, files, [0])
|
||||
if not any(name.lower() == "ui.map" for name, _, _ in files):
|
||||
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
|
||||
total = sum(size for _, _, size in files) or 1
|
||||
|
||||
partial = os.path.join(destination, "maps.partial")
|
||||
finished = os.path.join(destination, "maps")
|
||||
try:
|
||||
os.makedirs(partial, exist_ok=True)
|
||||
except OSError:
|
||||
raise ExtractError("Could not create %s." % partial)
|
||||
done = 0
|
||||
shown = -1
|
||||
for name, sector, size in files:
|
||||
path = os.path.join(partial, name)
|
||||
offset = partition + sector * SECTOR_SIZE
|
||||
remaining = size
|
||||
print("# Copying maps/%s" % name, flush=True)
|
||||
try:
|
||||
with open(path, "wb") as out:
|
||||
while remaining > 0:
|
||||
count = min(remaining, COPY_BUFFER_SIZE)
|
||||
out.write(read_at(image, offset, count))
|
||||
offset += count
|
||||
remaining -= count
|
||||
done += count
|
||||
# (zenity closes at 100: that is left to the caller)
|
||||
percent = min(99, done * 100 // total)
|
||||
if percent != shown:
|
||||
print(percent, flush=True)
|
||||
shown = percent
|
||||
except ExtractError:
|
||||
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
|
||||
except OSError:
|
||||
raise ExtractError("Could not write %s (is the storage full?)." % path)
|
||||
|
||||
# (the maps folder may be there, empty)
|
||||
try:
|
||||
os.makedirs(finished, exist_ok=True)
|
||||
for name, _, _ in files:
|
||||
os.replace(os.path.join(partial, name), os.path.join(finished, name))
|
||||
os.rmdir(partial)
|
||||
except OSError as error:
|
||||
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
extract(sys.argv[1], sys.argv[2])
|
||||
except ExtractError as error:
|
||||
print("error: %s" % error, file=sys.stderr)
|
||||
return 1
|
||||
except OSError as error:
|
||||
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -315,9 +315,6 @@
|
||||
<bitmap name="main_menu/multiplayer_type_select/join_game/header_direct_link">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="main_menu/multiplayer_type_select/join_game/header_password">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_password.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="main_menu/multiplayer_type_select/join_game/header_server_browser">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
|
||||
@@ -1,57 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
|
||||
<menus>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/password_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
|
||||
<on event="b" run="port password back"/>
|
||||
<on event="back" run="port password back"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/header_password"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/options_menu"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/header_password" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_password">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/help" type="text" controller="1" left="68" top="120" width="482" height="60" string_list="main_menu/multiplayer_type_select/join_game/password/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/join_game/password/help">
|
||||
<data input="port password update"/>
|
||||
<on event="created" run="port password init"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/op_password" x="54" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/button_ok" x="380" y="1"/>
|
||||
<child widget="common_button_cancel" x="510" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/op_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<on event="a" run="port password edit"/>
|
||||
<on event="start" run="port password edit"/>
|
||||
<on event="b"/>
|
||||
<on event="back"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/password_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/password/password_value" x="230" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/join_game/password/labels" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/password/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="JOIN GAME" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="port password join"/>
|
||||
<on event="start" run="port password join"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<strings name="main_menu/multiplayer_type_select/join_game/password/labels">
|
||||
<string text="PASSWORD:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/join_game/password/help_strings">
|
||||
<string text=""/>
|
||||
<string text="This game has a password. Type it, then press\nEnter to join."/>
|
||||
<string text="That is not the game's password. Try again."/>
|
||||
<string text="Could not join the game."/>
|
||||
</strings>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/header_server_browser" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_server_browser">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/header_direct_link" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/join_game/header_direct_link">
|
||||
|
||||
@@ -9,27 +9,25 @@
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/header_server_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/header_server_settings">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="366" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/help" type="text" controller="1" left="68" top="358" width="482" height="60" string_list="main_menu/multiplayer_type_select/server_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/help">
|
||||
<data input="server settings update"/>
|
||||
<on event="created" run="server settings init"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_server_name" x="54" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="99"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="125"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="151"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_password" x="54" y="177"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="203"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="229"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_collisions" x="54" y="281"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="203"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="229"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="255"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="281"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="307"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="333"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_max_players" x="54" y="101"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="129"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="157"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="185"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="213"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="241"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="241"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="185"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="213"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="241"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="269"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="297"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="325"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
@@ -80,19 +78,6 @@
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/listing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="PUBLIC|PRIVATE" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/op_password" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<on event="a" run="ss edit server password"/>
|
||||
<on event="start" run="ss edit server password"/>
|
||||
<on event="b"/>
|
||||
<on event="back"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/password_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/password_value" x="230" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="15" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" x="286" y="1"/>
|
||||
@@ -137,17 +122,6 @@
|
||||
<on event="deleted" run="port setting save"/>
|
||||
<on event="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/op_player_collisions" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="14" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="network.coop_player_collisions" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
|
||||
<on event="created" run="port setting load"/>
|
||||
<on event="deleted" run="port setting save"/>
|
||||
<on event="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/op_game_type" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<on event="a" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
|
||||
<on event="start" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
|
||||
@@ -237,8 +211,6 @@
|
||||
<string text="EXTRA ENEMIES:"/>
|
||||
<string text="PER PLAYER:"/>
|
||||
<string text="MULTIPLIER:"/>
|
||||
<string text="PLAYER COLLISIONS:"/>
|
||||
<string text="PASSWORD:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
|
||||
<string text=""/>
|
||||
@@ -262,8 +234,5 @@
|
||||
<string text="Enemy squads are the amount below times as large,\nhowever many players there are."/>
|
||||
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
|
||||
<string text="Each enemy squad is this many times as large."/>
|
||||
<string text="Players bump into each other, as in the campaign."/>
|
||||
<string text="Players walk through each other, so that no one\nblocks a doorway. The AI's characters still block."/>
|
||||
<string text="Players from the Server Browser must type it to\njoin (an invite link needs none). Enter sets it."/>
|
||||
</strings>
|
||||
</menus>
|
||||
@@ -16,8 +16,7 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="73"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="103"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="133"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
@@ -52,20 +51,11 @@
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.effects_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.reverb" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.enabled" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
@@ -84,7 +74,6 @@
|
||||
<string text="MASTER VOLUME:"/>
|
||||
<string text="MUSIC VOLUME:"/>
|
||||
<string text="EFFECTS VOLUME:"/>
|
||||
<string text="REVERB:"/>
|
||||
<string text="SOUND:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/help_strings">
|
||||
@@ -92,7 +81,6 @@
|
||||
<string text="The volume of everything."/>
|
||||
<string text="The music's volume."/>
|
||||
<string text="The volume of every other sound: effects and\nspeech."/>
|
||||
<string text="Echo sounds as the place you are in does, and\nmuffle those behind walls, as the Xbox did."/>
|
||||
<string text="Play sound at all; from the next time the game\nstarts."/>
|
||||
</strings>
|
||||
</menus>
|
||||
@@ -9,24 +9,20 @@
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="364" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings" font="ui\large_ui" color="#FFFFFFFF">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help">
|
||||
<data input="port settings help"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_mode" x="54" y="73" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="97" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="97" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="121" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="145"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="169" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="217" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="241"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="265"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" x="54" y="289" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" x="54" y="289" platform="android"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" x="54" y="313"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" x="54" y="337"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="99" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="99" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="125" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="151"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="177" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="203"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="229" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="255"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="281"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
@@ -124,42 +120,6 @@
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_text" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|SMAA|SSAA 2X|MSAA 2X|MSAA 4X|MSAA 8X" setting="display.anti_aliasing" values="off|fxaa|smaa|ssaa2x|msaa2x|msaa4x|msaa8x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="android">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|MSAA 2X|MSAA 4X" setting="display.anti_aliasing" values="off|fxaa|msaa2x|msaa4x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="128|256|512|1024" setting="display.shadow_resolution" values="128|256|512|1024" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.per_pixel_lighting" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
|
||||
<on event="created" run="port setting load"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="port settings defaults"/>
|
||||
<on event="start" run="port settings defaults"/>
|
||||
@@ -181,10 +141,6 @@
|
||||
<string text="INSTANT AIM:"/>
|
||||
<string text="HIGH-RES HUD:"/>
|
||||
<string text="HIGH-RES TEXT:"/>
|
||||
<string text="ANTI-ALIASING:"/>
|
||||
<string text="ANTI-ALIASING:"/>
|
||||
<string text="SHADOW RESOLUTION:"/>
|
||||
<string text="PER-PIXEL LIGHTING:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings">
|
||||
<string text=""/>
|
||||
@@ -198,9 +154,5 @@
|
||||
<string text="In first person, turn the view the moment the\nmouse moves, not up to two ticks later."/>
|
||||
<string text="Draw the HUD from the high-res redraws; off\ndraws the game's own pictures."/>
|
||||
<string text="Draw text and titles with high-res fonts; off\ndraws the game's own."/>
|
||||
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nand SMAA are cheap; SSAA and MSAA are sharper."/>
|
||||
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nis cheap; MSAA is sharper."/>
|
||||
<string text="The size objects' shadows are drawn at: 128 as on\nthe Xbox; larger for smoother, as soft, edges."/>
|
||||
<string text="Light models for each pixel, without the facets\nof the Xbox's lighting for each vertex."/>
|
||||
</strings>
|
||||
</menus>
|
||||
|
Before Width: | Height: | Size: 65 KiB |
@@ -54,7 +54,6 @@
|
||||
"ce/port/blank__0.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/join_game/header_direct_link.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/join_game/header_password.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png",
|
||||
"ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
|
||||
<defs>
|
||||
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
|
||||
<feGaussianBlur stdDeviation="14.23 13.06"/>
|
||||
</filter>
|
||||
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
|
||||
<rect width="512" height="59" fill="#fff"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g mask="url(#cut)">
|
||||
<rect x="29.08" y="28.05" width="216.45" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 563 B |
@@ -56,17 +56,13 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
BSP the team hasn't been in brings every player to whoever crossed it,
|
||||
as split screen does; one back into a BSP it has been in switches only
|
||||
with two thirds of the living players at it (in the trigger, or within
|
||||
15 world units of the player in it, about 45 metres), so one player can't
|
||||
drag the team back through the level; a player held back is told how
|
||||
many are there and how many it needs. A player outside the loaded BSP
|
||||
and falling for two seconds is brought back beside a teammate. A dead
|
||||
player watches a living teammate (`coop_spectate.c`) and comes back
|
||||
beside one once it is safe. With everyone dead they come back where they
|
||||
were at the last checkpoint, without a revert. A mission the scripts fail
|
||||
with players still alive reverts to the last checkpoint on the host, as a
|
||||
skipped cutscene does (below); a client never reverts on its own. A level
|
||||
won ends the round as in multiplayer, and the next round is the
|
||||
campaign's next level.
|
||||
4 world units of the player in it), so one player can't drag the team
|
||||
back through the level. A player outside the loaded BSP and falling for
|
||||
two seconds is brought back beside a teammate. A dead player watches a
|
||||
living teammate (`coop_spectate.c`) and comes back beside one once it is
|
||||
safe. With everyone dead they come back where they were at the last
|
||||
checkpoint, since only the host could revert. A level won ends the round
|
||||
as in multiplayer, and the next round is the campaign's next level.
|
||||
|
||||
Cutscenes are skipped by vote: more than half the machines must press
|
||||
skip. The host then reverts as single player does, but keeps its clock
|
||||
@@ -77,13 +73,12 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
The host's EXTRA ENEMIES (`coop_enemies.c`, `network.coop_enemies_mode`)
|
||||
give each squad of enemies a level places more of itself: PER PLAYER, a
|
||||
percentage of itself for each player past the first; STATIC MULTIPLIER,
|
||||
that many times itself for any number of players. They stand around its
|
||||
starting locations on free ground (the same floor, clear of crates and
|
||||
other actors, with room to stand), or where none is left on rings about
|
||||
them as before, and never take the actors a level needs for its own (the
|
||||
actor pool, `halo_port_capacity.h`, holds 1024). Riders a dropship has no
|
||||
seats for are kept, and placed beside its riders once they get out. Only
|
||||
the host runs the AI, so the clients see them as the host's other actors.
|
||||
that many times itself for any number of players. They stand in rings
|
||||
about its starting locations where the ground is open, and never take the
|
||||
actors a level needs for its own (the actor pool, `halo_port_capacity.h`,
|
||||
holds 1024). Riders a dropship has no seats for are kept, and placed
|
||||
beside its riders once they get out. Only the host runs the AI, so the
|
||||
clients see them as the host's other actors.
|
||||
- **Host authoritative.** The host alone decides damage, deaths, spawns,
|
||||
pickups, scores and the game's objects; clients do not decide them but
|
||||
apply what the host sends.
|
||||
@@ -164,16 +159,7 @@ version 13 drives up to 1056 of the host's AI units on its clients (co-op's
|
||||
extra enemies), where 12 drove 288; version 14 sends co-op's device positions
|
||||
and its units opening and closing; version 15 sends co-op's allegiances with
|
||||
its presentation, a message of another size; version 16 has a client's input
|
||||
say which structure BSP it has loaded (co-op); version 17 breaks the host's
|
||||
glass and destructible scenery on every machine (and takes a client's hits on
|
||||
scenery), sends the cluster a co-op cutscene keeps active, and leaves a
|
||||
failed co-op mission's revert to the host; version 18 sends with an object
|
||||
the bitmap of its shaders it draws with when its actor variant set one (co-op:
|
||||
the Elite major's and commander's armor); version 19 sends with the game's
|
||||
settings whether co-op's players collide with each other (Server Setup's PLAYER
|
||||
COLLISIONS: each machine's players then pass through the others'); version 20
|
||||
lists a public game with a password with its invite's token sealed with the
|
||||
password's key (`p2p_lobby.c`), a listing of another layout.
|
||||
say which structure BSP it has loaded (co-op).
|
||||
|
||||
A client plays by its host's rules: in another's game (searching for it,
|
||||
in its lobby, or playing it) the developer console, the telnet console
|
||||
@@ -195,13 +181,9 @@ only ever jumps forward to it when behind, so an honest one is never ahead
|
||||
while going faster (one that caught up, or a host that stalled, is one or
|
||||
the other, not both). One more than a tenth faster and half a second ahead
|
||||
has its players' predictions refused at once (the host's copies go as its
|
||||
own ticks have them), and after ten seconds of it is dropped, and every
|
||||
machine is told who, in red on its console and in its `debug.txt`. Its
|
||||
address is kept out of the host's games while the host runs only when a
|
||||
message that came over its connection's stream was that far ahead too: a
|
||||
datagram is known to be the machine's only by the address it came from,
|
||||
which another machine can send one as, so on its datagrams alone it is
|
||||
dropped, logged, and may join again
|
||||
own ticks have them), and after ten seconds of it is dropped, its address
|
||||
kept out of the host's games while the host runs, and every machine is
|
||||
told who, in red on its console and in its `debug.txt`
|
||||
(`distributed_note_client_clock`, `network_game_server_kick_machine`,
|
||||
`_distributed_message_notice`). The host also adds a line to
|
||||
`cheaters.txt` beside its `debug.txt`: when, the player's address (an
|
||||
@@ -216,8 +198,7 @@ A joining machine tells the host its hardware id: a keyed hash (HMAC-SHA-256,
|
||||
16 bytes as hex) of what its machine is known by (Windows' SMBIOS UUID, else
|
||||
its MachineGuid; Linux's `/etc/machine-id`; Android's `ANDROID_ID`, which the
|
||||
launcher writes to `hardware_id.txt`: `p2p_hardware_id`), kept by the host
|
||||
as hex only. A player dropped for cheating (on its stream's word, as
|
||||
above), and one the host bans with the
|
||||
as hex only. A player dropped for cheating, and one the host bans with the
|
||||
console's `ban <player name>` (Tab completes the name; the host's alone), is
|
||||
added to `bans.txt` beside `debug.txt` (a line each, as in `cheaters.txt`,
|
||||
with `ip=` and `hwid=`): the host refuses a machine joining whose address or
|
||||
|
||||
@@ -228,25 +228,21 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | Used when `display.window_size` is empty: the size of the window, as a multiple of 640x480. |
|
||||
| `display.vsync` | `true` | `HALO_NO_VSYNC=1` sets `false` | `true`: each frame waits for the display. |
|
||||
| `display.max_fps` | `0` | `HALO_MAX_FPS` | With vsync off, the most frames each second. `0`: twice the display's refresh rate. `-1`: no limit, which can hang some Intel graphics (Raptor Lake), resetting the desktop's graphics too. |
|
||||
| `display.anti_aliasing` | `"off"` | `HALO_ANTI_ALIASING` | The smoothing of jagged edges, which the Xbox did not have. `"off"`: none, as on the Xbox. `"fxaa"` or `"smaa"`: a pass over the 3D view after the game draws it. The HUD and the menus stay sharp. SMAA is the sharper and costs more. `"ssaa2x"`: the game draws at two times the resolution in each direction (at most the GPU's largest texture), and the picture is scaled down. The GPU does four times the work. Not with `display.resolution_scaling = "original"`. `"msaa2x"`, `"msaa4x"` or `"msaa8x"`: each pixel of the 3D view has that many samples (at most the GPU's). On Android, `"smaa"` gives FXAA and `"ssaa2x"` none. A change applies from the next frame. Refer to "Anti-aliasing" in "What operates". |
|
||||
| `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. |
|
||||
| `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". |
|
||||
| `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". |
|
||||
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
|
||||
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
|
||||
| `display.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. |
|
||||
| `display.menus` | `"pc"` | `HALO_MENUS` | `"pc"`: the PC version's menus, from the files in `port/assets/menus` and a `menus` folder next to `config.toml`. Refer to "Menus". `"xbox"`: the Xbox's menus. |
|
||||
| `display.player_names` | `"all"` | `HALO_PLAYER_NAMES` | In multiplayer, whose names are drawn above their heads: `"all"`, `"allies"`, `"enemies"` or `"none"`. An ally's name is drawn above the triangle the game shows over teammates. An enemy's name shows only within the motion sensor's reach, while the enemy is in sight and not camouflaged, so it never shows where an enemy hides. The gametype's motion tracker setting also applies: no names if it shows no players, only allies' if it shows only friends. |
|
||||
| `display.player_name_scale` | `1.0` | `HALO_PLAYER_NAME_SCALE` | How large the players' names are drawn: `1.0` is three quarters of the size of the HUD's text, from `0.25` to `4`. With high-res text, larger names are rasterized at their size, so they stay sharp. |
|
||||
| `display.scoreboard_team_layout` | `"teams"` | `HALO_SCOREBOARD_TEAM_LAYOUT` | How the multiplayer scoreboard (hold BACK, or tab) lists a team game's players. `"teams"`: a column for each team, red on the left and blue on the right. `"score"`: all the players in order of score. With more players than fit, the mouse wheel and Page Up / Page Down scroll the scoreboard. |
|
||||
| `display.scoreboard_background` | `true` | `HALO_SCOREBOARD_BACKGROUND` | `true`: the multiplayer scoreboard (hold BACK, or tab) has a panel behind its text, for clearer text. |
|
||||
| `display.scoreboard_background_color` | `"16, 16, 16, 150"` | `HALO_SCOREBOARD_BACKGROUND_COLOR` | The colour of the scoreboard's panel: `"red, green, blue, alpha"`, each from `0` to `255`. Alpha `0` is see-through, `255` is solid. |
|
||||
| `display.per_pixel_lighting` | `false` | `HALO_PER_PIXEL_LIGHTING` | `false`: the models (characters, weapons, vehicles, scenery) are lit at each vertex and the light is blended between them, as on the Xbox. The light across a curved surface then shows facets, and a point light that passes close lights only the vertices it reaches. `true`: the models are lit at each pixel by the same lights (the ambient light, two distant lights and two point lights), which changes their look. |
|
||||
| `audio.enabled` | `true` | `HALO_NO_AUDIO=1` sets `false` | `false`: no audio device. The sound continues without output. |
|
||||
| `audio.volume` | `1.0` | `HALO_VOLUME` | The master volume. |
|
||||
| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. |
|
||||
| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. |
|
||||
| `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). |
|
||||
| `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. |
|
||||
| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. |
|
||||
| `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. |
|
||||
@@ -265,7 +261,6 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". |
|
||||
| `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". |
|
||||
| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. |
|
||||
| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup writes its choice here. |
|
||||
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. |
|
||||
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad). PER PLAYER in co-op's Server Setup writes its choice here. |
|
||||
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. |
|
||||
@@ -465,18 +460,10 @@ Each new game starts as PUBLIC (`network.host_public = false` makes new
|
||||
games start as PRIVATE). An online co-op game starts as PRIVATE, and keeps
|
||||
the last choice of its LISTING (`network.coop_public`). A LAN game is never listed. `network.public_lobby = false` turns the server browser off.
|
||||
|
||||
A PUBLIC game can also have a PASSWORD (a row of Server Setup, below
|
||||
LISTING). The Server Browser shows a lock at the left of a game with a
|
||||
password. A player who selects that game must type the password, and JOIN
|
||||
GAME joins only with the correct password. The invite link of the game joins
|
||||
it without the password. The host keeps the password only while the game
|
||||
runs.
|
||||
|
||||
In the Server Browser, select a game to join it. The game joins the invite
|
||||
of the game, as for a link. When it reaches the host, it opens the lobby.
|
||||
If it cannot reach the host in 30 seconds, it marks the game FAILED. A game
|
||||
that is full or starting shows CLOSED. REFRESH asks the hosts for their
|
||||
listings again.
|
||||
If it cannot reach the host in 30 seconds, it marks the game FAILED. REFRESH
|
||||
asks the hosts for their listings again.
|
||||
|
||||
How it operates (`src/p2p_lobby.c`):
|
||||
|
||||
@@ -498,14 +485,6 @@ How it operates (`src/p2p_lobby.c`):
|
||||
seconds.
|
||||
- When a public game becomes private, the host makes a new invite. Thus a
|
||||
player who saw the listing cannot join with the old invite.
|
||||
- The listing of a game with a password does not hold the invite in clear
|
||||
text. The secret part of the invite (its token) is encrypted with a key
|
||||
from the password (Argon2id, salted with the key of the host, then
|
||||
XChaCha20-Poly1305). The browser makes the key from the password that the
|
||||
player types, and opens the invite only if the password is correct. When
|
||||
the host sets or changes the password, it makes a new invite. A player who
|
||||
has the listing can try passwords on their own machine without the host,
|
||||
so use a long password.
|
||||
|
||||
A public game does not publish the address of the host. But any machine
|
||||
with the invite can ask the host to connect, and the host then sends its
|
||||
@@ -567,15 +546,12 @@ Only machines with the invite can find the game:
|
||||
flood of requests can make players join more slowly. A player asks again
|
||||
for 90 seconds.
|
||||
- The host drops a player whose game runs faster than time (a speed hack)
|
||||
for ten seconds. Each player sees who in red on the console. The host
|
||||
adds a line to `cheaters.txt` (beside `debug.txt`) with the address and
|
||||
hardware id of the player, and the Discord name and id that the game of
|
||||
the player told it (a player can change these). If the messages on the
|
||||
player's connection were also ahead, the host keeps that address out of
|
||||
its games and bans the player: it adds the line to `bans.txt`, and refuses
|
||||
a machine whose address or hardware id is in it. If only the player's
|
||||
datagrams were ahead, the player can join again: another machine can send
|
||||
datagrams with the player's address.
|
||||
for ten seconds, and keeps that address out of its games. Each player
|
||||
sees who in red on the console. The host adds a line to `cheaters.txt`
|
||||
(beside `debug.txt`) with the address and hardware id of the player, and
|
||||
the Discord name and id that the game of the player told it (a player can
|
||||
change these). The host also bans the player: it adds the line to
|
||||
`bans.txt`, and refuses a machine whose address or hardware id is in it.
|
||||
- The host can ban a player with `ban <player name>` in the developer
|
||||
console (Tab completes the name). Remove a line from `bans.txt` to unban.
|
||||
Refer to `NETCODE.md`. `kick <player name>` drops the player the same
|
||||
@@ -651,7 +627,6 @@ The game sends the activity only to a Discord client of the same user.
|
||||
| Graphics | Direct3D 8 on OpenGL 4.5 core through SDL3 (`src/d3d8_gl.c`). The port translates the NV2A vertex shaders and register combiners to GLSL. It decodes all the Xbox texture formats. The vertex and index buffers come from a GL copy of the Xbox memory. |
|
||||
| High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws, and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. |
|
||||
| High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. `display.high_res_text = false` turns it off. |
|
||||
| Anti-aliasing | Off unless `display.anti_aliasing` is set (`src/d3d8_gl.c`, `src/xgpu_post.c`). FXAA (written in the port) and SMAA (`port/third_party/smaa`, MIT licensed, at its HIGH preset, compiled as GLSL) are passes over the 3D view of each window, after the lens flares and before the HUD and the menus (`render/render.c`). Their programs are built when the setting is chosen. Supersampling draws the render targets the size of the screen at two times the resolution in each direction, and the display blit scales them down. Multisampling draws the back buffer and its depth buffer into multisampled renderbuffers, and with them any target that is drawn together with one of them (a mirror's view, in the secondary target with the back buffer's depth buffer), so that the attachments of a framebuffer are all multisampled or none is. A target's pixels are resolved into its texture before something reads the texture (as a texture, or at the display blit). Visibility tests count samples, divided by the samples of a pixel. An alpha-tested surface (foliage, grates) covers the samples of a pixel in proportion to its alpha past the reference (`gl_SampleMask`, not on Android). |
|
||||
| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. |
|
||||
| Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. |
|
||||
| Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. |
|
||||
@@ -740,8 +715,6 @@ Other changes:
|
||||
| `interface/ui_widget.c`, `interface/ui_widget_event_handler_functions.c`, `interface/ui_widget_game_data_input_functions.c` | The main menu is the PC version's from `port/assets/menus` (`display.menus`); the menus' widgets can call the port's functions (`game/menu_functions.c`) and send the PC version's custom activation event; the widgets' memory is 256 KB, not 16 KB; the main menu and Multiplayer clear co-op's controllers, so that a gamepad going back to controller 1 is not a controller unplugged; the lobby's split screen players leave alone, and one who quits in game is not joined to the next game (`interface/player_ui.c`). |
|
||||
| `input/input_abstraction.c`, `game/player_control.c`, `game/players.c`, `game/player_queues_new.c`, `units/units.h` | The keyboard and mouse's actions (`src/xinput_sdl.c`, `include/halo_keyboard.h`) join controller 1's game controls; their reload key reloads on its own, and their action key only acts (a control flag of the port's, sent with the player's action, stops the reload the controller's X falls back to). |
|
||||
| `sound/sound_manager.c`, `interface/hud.c`, `game/game_engine.c` | The music's and the other sounds' volumes; the HUD's and the scoreboard's settings are read again when Settings changes them. |
|
||||
| `rasterizer/xbox/rasterizer_xbox_shadows.c` | With larger shadow maps (`display.shadow_resolution`), the blur's taps stay half a texel apart, and more passes widen it to cover the same part of the map as on the Xbox. |
|
||||
| `render/render.c` | The 3D view of each window is antialiased before the HUD is drawn (`display.anti_aliasing`). |
|
||||
| `interface/hud.c` | In multiplayer, players' names are drawn above their heads (`display.player_names`, `display.player_name_scale`). |
|
||||
| `rasterizer/rasterizer_text.c`, `text/draw_string.c` | Text is drawn from an atlas of the fonts' glyphs, rasterized at the resolution the game draws at (`src/text_hires.c`), when the font has every character of the string. Text can be drawn scaled about a point (`rasterizer_text_set_scale`), as the players' names are. Each glyph's advance is centred on the font tag character's, so the layout is the same, and a glyph is cut at a text box only where the font tag's character visibly was. |
|
||||
|
||||
|
||||
@@ -7,15 +7,9 @@ percentage): for each player past the first, each squad of the players'
|
||||
enemies that a level places (encounters.c's encounter_create) gets that
|
||||
much of its count more; at 100%, two players meet twice the squad, four
|
||||
players four times it. STATIC MULTIPLIER (network.coop_enemies_multiplier):
|
||||
each squad is that many times as large, for any number of players.
|
||||
|
||||
Extra enemies are placed in widening rings around the squad's starting
|
||||
locations, on a spot on the same floor, reachable without passing through
|
||||
a wall, crate or machine, with room to stand and nobody already there. A
|
||||
starting location with no room left passes its enemy to the squad's next
|
||||
one; if none has room, the enemy goes where extra enemies went before free
|
||||
ground was looked for (rings around the starting location, whoever stands
|
||||
there), so a squad still gets every enemy the setting asks for.
|
||||
each squad is that many times as large, for any number of players. The
|
||||
extra enemies stand in rings about the squad's starting locations, on
|
||||
open ground, so they do not start inside each other.
|
||||
|
||||
A squad a script loads into a dropship (vehicle_load_magic) can be larger
|
||||
than the dropship's seats. The riders left without a seat are erased, and
|
||||
@@ -66,12 +60,9 @@ enum
|
||||
COOP_ENEMIES_MAXIMUM_PERCENT = 200,
|
||||
COOP_ENEMIES_MINIMUM_MULTIPLIER = 2,
|
||||
COOP_ENEMIES_MAXIMUM_MULTIPLIER = 32,
|
||||
/* the rings tried around a starting location, the first holding
|
||||
SPREAD_PLACES_PER_RING places and each further one that many more */
|
||||
SPREAD_RINGS = 5,
|
||||
/* the places tried for an extra enemy, a ring further out each */
|
||||
SPREAD_ATTEMPTS = 8,
|
||||
SPREAD_PLACES_PER_RING = 6,
|
||||
/* the rings coop_enemies_fallback_position tries, a place on each */
|
||||
FALLBACK_RINGS = 8,
|
||||
MAXIMUM_RIDING_VEHICLES = 32,
|
||||
MAXIMUM_SEATED_RIDERS = 16,
|
||||
MAXIMUM_KEPT_RIDERS = 512,
|
||||
@@ -80,25 +71,11 @@ enum
|
||||
RIDER_RELEASE_TICKS = 4,
|
||||
};
|
||||
|
||||
/* (coop_enemies.h's count of the places on all the rings) */
|
||||
typedef char coop_enemies_spread_spots_assert[
|
||||
COOP_ENEMIES_SPREAD_SPOTS == SPREAD_PLACES_PER_RING * SPREAD_RINGS * (SPREAD_RINGS + 1) / 2 ? 1 : -1];
|
||||
|
||||
/* distance between rings */
|
||||
/* the rings' spacing, and the height over the ground the way to a place is
|
||||
looked along, and the ground looked for below it */
|
||||
#define SPREAD_SPACING 0.8f
|
||||
/* height above the floor at which the way to a spot is checked */
|
||||
#define SPREAD_STEP_HEIGHT 0.6f
|
||||
/* how far a spot's floor may be above or below the starting location's:
|
||||
a step, not a ledge */
|
||||
#define SPREAD_FLOOR_DIFFERENCE 0.5f
|
||||
/* room a spot needs above its floor */
|
||||
#define SPREAD_HEADROOM 1.6f
|
||||
/* how close across the ground a spot may be to a biped already there
|
||||
(about two bipeds' collision radius), and how far around it to look */
|
||||
#define SPREAD_CLEARANCE 0.6f
|
||||
#define SPREAD_SEARCH_RADIUS 1.0f
|
||||
/* how far below a fallback place the ground is looked for */
|
||||
#define FALLBACK_GROUND_DEPTH 2.0f
|
||||
#define SPREAD_GROUND_DEPTH 2.0f
|
||||
/* a kept rider's place beside a rider who got out, each a little apart */
|
||||
#define RIDER_SPACING 0.5f
|
||||
|
||||
@@ -183,105 +160,35 @@ static short coop_enemies_player_count(
|
||||
return count;
|
||||
}
|
||||
|
||||
/* what a spot is tested against: the level, and the objects that block
|
||||
the way (crates and other scenery, machines, vehicles) */
|
||||
#define SPREAD_COLLISION_FLAGS (FLAG(_collision_test_structure_bit) | FLAG(_collision_test_objects_bit) | \
|
||||
_collision_test_objects_sight_blocking_flags | FLAG(_collision_test_front_facing_surfaces_bit) | \
|
||||
FLAG(_collision_test_back_facing_surfaces_bit))
|
||||
|
||||
/* whether anything is in the way from `from`, along `vector` */
|
||||
static boolean coop_enemies_blocked(
|
||||
real_point3d const *from,
|
||||
real_vector3d const *vector)
|
||||
/* whether the way from a to b is open (no structure between them) */
|
||||
static boolean coop_enemies_open(
|
||||
real_point3d const *a,
|
||||
real_point3d const *b)
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_vector3d vector;
|
||||
|
||||
return collision_test_vector(SPREAD_COLLISION_FLAGS, from, vector, NONE, &collision);
|
||||
vector.i = b->x - a->x;
|
||||
vector.j = b->y - a->y;
|
||||
vector.k = b->z - a->z;
|
||||
return !collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
|
||||
FLAG(_collision_test_back_facing_surfaces_bit), a, &vector, NONE, &collision);
|
||||
}
|
||||
|
||||
/* the floor under a point, looked for from SPREAD_STEP_HEIGHT above it to
|
||||
SPREAD_FLOOR_DIFFERENCE below it */
|
||||
static boolean coop_enemies_floor(
|
||||
/* the ground below a point, within SPREAD_GROUND_DEPTH of it */
|
||||
static boolean coop_enemies_ground(
|
||||
real_point3d const *point,
|
||||
real_point3d *floor)
|
||||
real_point3d *ground)
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_point3d from = *point;
|
||||
real_vector3d down = { 0.0f, 0.0f, -(2.0f * SPREAD_STEP_HEIGHT + SPREAD_FLOOR_DIFFERENCE) };
|
||||
real_vector3d vector = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + SPREAD_GROUND_DEPTH) };
|
||||
|
||||
from.z += SPREAD_STEP_HEIGHT;
|
||||
if (!collision_test_vector(SPREAD_COLLISION_FLAGS, &from, &down, NONE, &collision))
|
||||
return FALSE;
|
||||
*floor = collision.point;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* whether a biped stands within SPREAD_CLEARANCE of a spot on the floor,
|
||||
or the spot is inside a vehicle */
|
||||
static boolean coop_enemies_occupied(
|
||||
real_point3d const *floor)
|
||||
{
|
||||
real_point3d body = { floor->x, floor->y, floor->z + SPREAD_STEP_HEIGHT };
|
||||
struct location location;
|
||||
long object_indices[32];
|
||||
short object_count;
|
||||
short index;
|
||||
|
||||
scenario_location_from_point(&location, &body);
|
||||
if (location.cluster_index == NONE)
|
||||
return TRUE;
|
||||
object_count = objects_in_sphere(0, _object_mask_biped | _object_mask_vehicle, &location, &body,
|
||||
SPREAD_SEARCH_RADIUS, object_indices, NUMBEROF(object_indices));
|
||||
for (index = 0; index < object_count; index++)
|
||||
{
|
||||
struct object_datum *object = object_get(object_indices[index]);
|
||||
|
||||
if (object->object.type == _object_type_vehicle)
|
||||
{
|
||||
if (point_in_sphere(&body, &object->object.bounding_sphere_center, object->object.bounding_sphere_radius))
|
||||
return TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
real dx = object->object.position.x - floor->x;
|
||||
real dy = object->object.position.y - floor->y;
|
||||
|
||||
if (dx * dx + dy * dy < SPREAD_CLEARANCE * SPREAD_CLEARANCE)
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Whether an enemy can stand at `spot`: reachable from `from` (beside the
|
||||
starting location) with nothing in the way, a floor within a step of
|
||||
`floor_height` (the starting location's), room above it, and nobody there.
|
||||
The floor goes in `position`. */
|
||||
static boolean coop_enemies_spot_free(
|
||||
real_point3d const *from,
|
||||
real_point3d const *spot,
|
||||
real floor_height,
|
||||
real_point3d *position)
|
||||
{
|
||||
/* (level, at step height: down to the starting location's floor, a spot
|
||||
whose floor is a little higher would end the look inside it) */
|
||||
real_vector3d way = { spot->x - from->x, spot->y - from->y, 0.0f };
|
||||
real_vector3d up = { 0.0f, 0.0f, SPREAD_HEADROOM };
|
||||
real_point3d floor;
|
||||
real_point3d feet;
|
||||
|
||||
if (coop_enemies_blocked(from, &way) || !coop_enemies_floor(spot, &floor) ||
|
||||
fabsf(floor.z - floor_height) > SPREAD_FLOOR_DIFFERENCE)
|
||||
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
point, &vector, NONE, &collision))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
/* (headroom is checked from just above the floor, so the floor itself
|
||||
doesn't count as in the way) */
|
||||
feet = floor;
|
||||
feet.z += 0.1f;
|
||||
if (coop_enemies_blocked(&feet, &up) || coop_enemies_occupied(&floor))
|
||||
return FALSE;
|
||||
*position = floor;
|
||||
*ground = collision.point;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -356,7 +263,6 @@ static void coop_enemies_rider_place(
|
||||
short out_count = 0;
|
||||
short index, round;
|
||||
real angle;
|
||||
real_point3d spread;
|
||||
|
||||
*position = object->object.position;
|
||||
*facing = (real)atan2(object->object.forward.j, object->object.forward.i);
|
||||
@@ -393,13 +299,7 @@ static void coop_enemies_rider_place(
|
||||
}
|
||||
}
|
||||
}
|
||||
/* free ground around that spot, as the squad's own extra enemies get;
|
||||
failing that, a ring around it, further out each round */
|
||||
if (coop_enemies_spread_position(position, (short)(number + 1), NULL, &spread))
|
||||
{
|
||||
*position = spread;
|
||||
return;
|
||||
}
|
||||
/* (never on the rider's own place: a ring about it, further each round) */
|
||||
angle = (real)number * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
|
||||
position->x += (real)cos(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
|
||||
position->y += (real)sin(angle) * RIDER_SPACING * (real)(round / SPREAD_PLACES_PER_RING + 1);
|
||||
@@ -457,48 +357,6 @@ short coop_enemies_extra_count(
|
||||
}
|
||||
|
||||
boolean coop_enemies_spread_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
long *taken,
|
||||
real_point3d *position)
|
||||
{
|
||||
real_point3d from = *origin;
|
||||
real_point3d floor;
|
||||
real floor_height = coop_enemies_floor(origin, &floor) ? floor.z : origin->z;
|
||||
short ring, first_spot = 0;
|
||||
|
||||
from.z = floor_height + SPREAD_STEP_HEIGHT;
|
||||
for (ring = 0; ring < SPREAD_RINGS; ring++)
|
||||
{
|
||||
short place_count = (short)(SPREAD_PLACES_PER_RING * (ring + 1));
|
||||
real radius = SPREAD_SPACING * (real)(ring + 1);
|
||||
short place;
|
||||
|
||||
/* (each enemy starts at a different place on the ring, so a squad
|
||||
spreads all the way round rather than filling one side first) */
|
||||
for (place = 0; place < place_count; place++)
|
||||
{
|
||||
short around = (short)((number + place) % place_count);
|
||||
short spot_index = (short)(first_spot + around);
|
||||
real angle = (real)around * (_pi * 2.0f / (real)place_count);
|
||||
real_point3d spot = from;
|
||||
|
||||
if (taken && BIT_VECTOR_TEST_FLAG(taken, spot_index))
|
||||
continue;
|
||||
spot.x += (real)cos(angle) * radius;
|
||||
spot.y += (real)sin(angle) * radius;
|
||||
spot.z = floor_height;
|
||||
if (coop_enemies_spot_free(&from, &spot, floor_height, position))
|
||||
return TRUE;
|
||||
if (taken)
|
||||
BIT_VECTOR_SET_FLAG(taken, spot_index, TRUE);
|
||||
}
|
||||
first_spot = (short)(first_spot + place_count);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
boolean coop_enemies_fallback_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
real_point3d *position)
|
||||
@@ -509,30 +367,18 @@ boolean coop_enemies_fallback_position(
|
||||
short attempt;
|
||||
|
||||
from.z += SPREAD_STEP_HEIGHT;
|
||||
for (attempt = 0; attempt < FALLBACK_RINGS; attempt++, ring++)
|
||||
for (attempt = 0; attempt < SPREAD_ATTEMPTS; attempt++, ring++)
|
||||
{
|
||||
/* (each ring turned half a place from the last, so its places fall
|
||||
between the last's) */
|
||||
real angle = ((real)place + 0.5f * (real)(ring % 2)) * (_pi * 2.0f / SPREAD_PLACES_PER_RING);
|
||||
real radius = SPREAD_SPACING * (real)(ring + 1);
|
||||
real_point3d to = from;
|
||||
real_vector3d way = { 0.0f, 0.0f, 0.0f };
|
||||
real_vector3d down = { 0.0f, 0.0f, -(SPREAD_STEP_HEIGHT + FALLBACK_GROUND_DEPTH) };
|
||||
struct collision_result collision;
|
||||
|
||||
way.i = (real)cos(angle) * radius;
|
||||
way.j = (real)sin(angle) * radius;
|
||||
to.x += way.i;
|
||||
to.y += way.j;
|
||||
/* (the level only: the way open, and ground below) */
|
||||
if (!collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit) |
|
||||
FLAG(_collision_test_back_facing_surfaces_bit), &from, &way, NONE, &collision) &&
|
||||
collision_test_vector(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
&to, &down, NONE, &collision))
|
||||
{
|
||||
*position = collision.point;
|
||||
to.x += (real)cos(angle) * radius;
|
||||
to.y += (real)sin(angle) * radius;
|
||||
if (coop_enemies_open(&from, &to) && coop_enemies_ground(&to, position))
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -21,27 +21,10 @@ short coop_enemies_extra_count(
|
||||
long encounter_index,
|
||||
short count);
|
||||
|
||||
/* the places coop_enemies_spread_position tries around a starting location */
|
||||
#define COOP_ENEMIES_SPREAD_SPOTS 90
|
||||
|
||||
/* encounters.c: finds a spot for a squad's `number`th extra enemy
|
||||
(counting from 1) around the starting location `origin`, on its floor,
|
||||
clear of walls and objects, with room to stand and nobody there. FALSE if
|
||||
there's no room. `taken` (COOP_ENEMIES_SPREAD_SPOTS bits, or NULL) marks the
|
||||
places found taken, which stay so while the squad's enemies are placed: they
|
||||
aren't tried again. */
|
||||
/* encounters.c: the place of a squad's `number`th extra enemy (from 1) at
|
||||
its starting location `origin`: rings about it, on open ground; FALSE if
|
||||
none is (the starting location itself then) */
|
||||
boolean coop_enemies_spread_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
long *taken,
|
||||
real_point3d *position);
|
||||
/* encounters.c: where the `number`th extra enemy (counting from 1) goes
|
||||
when no starting location has room: a place on rings around `origin` with
|
||||
the way open and ground below, whoever stands there (as extra enemies were
|
||||
placed before free ground was looked for), so the squad still gets every
|
||||
enemy the setting asks for. FALSE if none (the starting location itself
|
||||
then) */
|
||||
boolean coop_enemies_fallback_position(
|
||||
real_point3d const *origin,
|
||||
short number,
|
||||
real_point3d *position);
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
COOP_SCRIPTS.C
|
||||
|
||||
Campaign scripts made to work for every player in network co-op.
|
||||
|
||||
The campaign's scripts were written for one or two players. Only the host
|
||||
runs them (game.c), against every player's unit, so most of what they do
|
||||
already happens for everyone: a trigger volume tested against (players)
|
||||
fires for whoever walks into it, and what it starts is sent to the clients
|
||||
(network_coop.c). Three habits of the scripts don't carry over:
|
||||
|
||||
- Waiting for every player at once (volume_test_objects_all on the
|
||||
players): the Maw's run to the Pillar of Autumn's bridge, a30's Pelican
|
||||
pickups. Split-screen players stand together; network players don't, so
|
||||
the wait would never end. In co-op one player arriving is enough.
|
||||
- Waiting until it's safe (game_safe_to_save) before a cutscene: the
|
||||
game's test fails while any enemy sees any player. In co-op one player
|
||||
out of danger is enough.
|
||||
- Naming players one at a time: the scripts reach the players through
|
||||
player0 and player1 (the first two in the players list), to put them in
|
||||
a Pelican, take them out of it, stage them for a cutscene or give them a
|
||||
starting profile. Everyone past the second would be left out, so they
|
||||
follow player0: wherever the scripts put player0, they go too.
|
||||
*/
|
||||
|
||||
/* ---------- headers */
|
||||
|
||||
#include "cseries.h"
|
||||
#include "ai/ai.h"
|
||||
#include "game/game.h"
|
||||
#include "game/players.h"
|
||||
#include "hs/object_lists.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/object_types.h"
|
||||
#include "units/bipeds.h"
|
||||
#include "units/units.h"
|
||||
|
||||
#include "coop_scripts.h"
|
||||
#include "network_coop.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* players the scripts name: player0 and player1 */
|
||||
#define NAMED_PLAYERS 2
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean coop_scripts_host(
|
||||
void)
|
||||
{
|
||||
return game_connection() == _game_connection_network_server && network_coop_active();
|
||||
}
|
||||
|
||||
/* the seat label to look for when boarding a follower: player0's own up to
|
||||
and including "rider" (Pelicans have "p-riderlf", "p-riderrf", "p-riderlb"
|
||||
and so on), else up to the '-' ("b-driver" gives "b-") */
|
||||
static void follower_seat_name(
|
||||
char const *seat_name,
|
||||
char *follower_seat,
|
||||
size_t size)
|
||||
{
|
||||
char *rider;
|
||||
char *dash;
|
||||
|
||||
/* (lower case: vehicle_scripting_find_available_seats compares it with
|
||||
lowered seat labels) */
|
||||
snprintf(follower_seat, size, "%s", seat_name);
|
||||
strlwr(follower_seat);
|
||||
rider = strstr(follower_seat, "rider");
|
||||
dash = strchr(follower_seat, '-');
|
||||
if (rider)
|
||||
rider[strlen("rider")] = 0;
|
||||
else if (dash)
|
||||
dash[1] = 0;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
boolean coop_scripts_any_player_will_do(
|
||||
long object_list_index)
|
||||
{
|
||||
long reference_index;
|
||||
long object_index;
|
||||
boolean any = FALSE;
|
||||
|
||||
if (!coop_scripts_host())
|
||||
return FALSE;
|
||||
for (object_index = object_list_get_first(object_list_index, &reference_index);
|
||||
object_index != NONE;
|
||||
object_index = object_list_get_next(object_list_index, &reference_index))
|
||||
{
|
||||
if (player_index_from_unit_index(object_index) == NONE)
|
||||
return FALSE;
|
||||
any = TRUE;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
/* the units of the players following player0, if `unit_index` is
|
||||
player0's; how many */
|
||||
static short players_following(
|
||||
long unit_index,
|
||||
long *unit_indices,
|
||||
short maximum_count)
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
short position = 0;
|
||||
short count = 0;
|
||||
|
||||
if (unit_index == NONE || !coop_scripts_host())
|
||||
return 0;
|
||||
/* (in the order (players) lists them: player0 is the first with a unit) */
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
{
|
||||
if (player->unit_index == NONE)
|
||||
continue;
|
||||
if (position == 0 && player->unit_index != unit_index)
|
||||
return 0;
|
||||
if (position >= NAMED_PLAYERS && count < maximum_count)
|
||||
unit_indices[count++] = player->unit_index;
|
||||
position++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void coop_scripts_teleport_followers(
|
||||
long unit_index)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
player_teleport(player_index_from_unit_index(followers[index]), unit_index,
|
||||
&object_get(unit_index)->object.position);
|
||||
}
|
||||
}
|
||||
|
||||
void coop_scripts_suspend_followers(
|
||||
long unit_index,
|
||||
boolean suspended)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
unit_scripting_suspended(followers[index], suspended);
|
||||
}
|
||||
|
||||
void coop_scripts_exit_followers(
|
||||
long unit_index,
|
||||
long vehicle_index)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
if (object_get(followers[index])->object.parent_object_index == vehicle_index)
|
||||
unit_scripting_exit_vehicle(followers[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void coop_scripts_board_followers(
|
||||
long unit_index,
|
||||
long vehicle_index,
|
||||
char const *seat_name)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
char follower_seat[64];
|
||||
short index;
|
||||
|
||||
if (count == 0 || vehicle_index == NONE)
|
||||
return;
|
||||
follower_seat_name(seat_name, follower_seat, sizeof(follower_seat));
|
||||
/* (each list freed at once: there are few, and the scripts' own are only
|
||||
collected after the tick's threads have run) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
long list_index = object_list_new();
|
||||
|
||||
if (list_index == NONE)
|
||||
break;
|
||||
object_list_add(list_index, followers[index]);
|
||||
vehicle_scripting_load_magic(vehicle_index, follower_seat, list_index);
|
||||
object_list_delete(list_index);
|
||||
}
|
||||
}
|
||||
|
||||
boolean coop_scripts_safe_to_save(
|
||||
void)
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
|
||||
if (game_safe_to_save())
|
||||
return TRUE;
|
||||
if (!coop_scripts_host())
|
||||
return FALSE;
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
{
|
||||
struct biped_datum *biped = biped_try_and_get(player->unit_index);
|
||||
|
||||
if (biped && !TEST_FLAG(biped->object.damage_flags, _object_dead_bit) &&
|
||||
!TEST_FLAG(biped->biped.flags, _biped_airborne_bit) && !ai_port_enemies_can_see_unit(player->unit_index))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void coop_scripts_player_add_equipment(
|
||||
long unit_index,
|
||||
short starting_profile_index,
|
||||
boolean reset_equipment)
|
||||
{
|
||||
long followers[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short count = players_following(unit_index, followers, NUMBEROF(followers));
|
||||
short index;
|
||||
|
||||
player_add_equipment(unit_index, starting_profile_index, reset_equipment);
|
||||
for (index = 0; index < count; index++)
|
||||
player_add_equipment(followers[index], starting_profile_index, reset_equipment);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
COOP_SCRIPTS.H
|
||||
|
||||
Campaign scripts made to work for every player in network co-op
|
||||
(coop_scripts.c).
|
||||
*/
|
||||
|
||||
#ifndef __COOP_SCRIPTS_H
|
||||
#define __COOP_SCRIPTS_H
|
||||
|
||||
/* ---------- prototypes */
|
||||
|
||||
/* hs_library_external.c's volume_test_objects_all: whether to count the
|
||||
list as inside once any of it is, because every object in it is a player */
|
||||
boolean coop_scripts_any_player_will_do(
|
||||
long object_list_index);
|
||||
|
||||
/* What the scripts do to player0, done to the players they can't name too
|
||||
(the "followers"). Each does nothing unless `unit_index` is player0's in a
|
||||
co-op host's game. */
|
||||
|
||||
/* hs_library_external.c's object_teleport: around player0 */
|
||||
void coop_scripts_teleport_followers(
|
||||
long unit_index);
|
||||
/* units.c's unit_suspended */
|
||||
void coop_scripts_suspend_followers(
|
||||
long unit_index,
|
||||
boolean suspended);
|
||||
/* units.c's unit_exit_vehicle: those riding player0's vehicle get out */
|
||||
void coop_scripts_exit_followers(
|
||||
long unit_index,
|
||||
long vehicle_index);
|
||||
/* units.c's vehicle_load_magic and unit_enter_vehicle: into the vehicle,
|
||||
each in a free seat of the same kind as player0's (`seat_name`) */
|
||||
void coop_scripts_board_followers(
|
||||
long unit_index,
|
||||
long vehicle_index,
|
||||
char const *seat_name);
|
||||
|
||||
/* hs.c's game_safe_to_save, for the scripts: the game's own test, or in
|
||||
co-op, any one player alive, on the ground and unseen by enemies. The
|
||||
scripts wait on it before a cutscene, and with players spread out a fight
|
||||
somewhere else would hold up the one a player has reached. Checkpoints
|
||||
keep the game's own test. */
|
||||
boolean coop_scripts_safe_to_save(
|
||||
void);
|
||||
|
||||
/* hs.c's player_add_equipment, for the scripts: gives the starting profile
|
||||
to player0's followers too */
|
||||
void coop_scripts_player_add_equipment(
|
||||
long unit_index,
|
||||
short starting_profile_index,
|
||||
boolean reset_equipment);
|
||||
|
||||
#endif
|
||||
@@ -1970,10 +1970,6 @@ Xbox's networking, run by the engine's port entry points
|
||||
#define KEY_MODIFIER_CONTROL_BIT 1
|
||||
#define LOBBY_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\lobby_screen"
|
||||
#define PREVIEW_NAME "pc\\main_menu\\multiplayer_type_select\\lobby\\preview_screen"
|
||||
/* the server browser's password screen (tools/port_settings.py), and the
|
||||
longest password */
|
||||
#define PASSWORD_NAME "pc\\main_menu\\multiplayer_type_select\\join_game\\password\\password_screen"
|
||||
#define PASSWORD_LENGTH 32
|
||||
/* the lobby's panel's lines (the gametype, the players, the countdown, the
|
||||
invite) */
|
||||
#define LOBBY_TEXT_LENGTH (ROW_TEXT_LENGTH * 4)
|
||||
@@ -2040,7 +2036,6 @@ boolean ui_widget_port_unjoin_player(struct widget_instance *widget, struct even
|
||||
boolean *widget_deleted);
|
||||
void network_game_server_port_set_settings(wchar_t const *name, long maximum_players);
|
||||
void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire);
|
||||
void network_game_server_port_set_cooperative_player_collisions(boolean player_collisions);
|
||||
void *global_network_game_client_get(void);
|
||||
void *global_network_game_server_get(void);
|
||||
boolean network_game_is_splitscreen_local(void);
|
||||
@@ -2097,8 +2092,6 @@ static struct
|
||||
boolean cooperative_maximum_players_set;
|
||||
/* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */
|
||||
short cooperative_friendly_fire;
|
||||
/* co-op's PLAYER COLLISIONS shown, OFF (network.coop_player_collisions's) */
|
||||
boolean cooperative_no_player_collisions;
|
||||
/* the browser's games */
|
||||
struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
|
||||
short game_count, game_chosen;
|
||||
@@ -2111,9 +2104,6 @@ static struct
|
||||
/* Server Setup's LISTING: PRIVATE (each new game starts with
|
||||
network.host_public's choice: PUBLIC unless set otherwise) */
|
||||
boolean game_private;
|
||||
/* Server Setup's PASSWORD (a public internet game's; empty: none), kept
|
||||
while the game runs, never written down */
|
||||
char game_password[PASSWORD_LENGTH + 1];
|
||||
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
|
||||
|
||||
/* ---- a text field (Direct Link's link, the game's name): the keyboard
|
||||
@@ -2126,8 +2116,6 @@ static struct
|
||||
char before[TEXT_FIELD_LENGTH];
|
||||
short maximum;
|
||||
void (*done)(char const *text);
|
||||
/* a password's: shown as stars (text_field_begin_masked) */
|
||||
boolean masked;
|
||||
} text_field;
|
||||
|
||||
/* when the field was last shown (a field not shown for a while is let go
|
||||
@@ -2173,7 +2161,6 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
|
||||
snprintf(text_field.before, sizeof(text_field.before), "%s", text);
|
||||
text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1);
|
||||
text_field.done = done;
|
||||
text_field.masked = FALSE;
|
||||
text_field_shown_time = system_milliseconds();
|
||||
while (input_get_key(&key))
|
||||
;
|
||||
@@ -2184,14 +2171,6 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
|
||||
#endif
|
||||
}
|
||||
|
||||
/* a password's field: as text_field_begin, its text shown as stars */
|
||||
static void text_field_begin_masked(struct widget_instance *row, char const *text, short maximum,
|
||||
void (*done)(char const *text))
|
||||
{
|
||||
text_field_begin(row, text, maximum, done);
|
||||
text_field.masked = TRUE;
|
||||
}
|
||||
|
||||
static void text_field_end(boolean keep)
|
||||
{
|
||||
void (*done)(char const *text) = text_field.done;
|
||||
@@ -2280,7 +2259,7 @@ static void text_field_show(struct widget_instance *value, char const *text, boo
|
||||
text = text_field.text;
|
||||
}
|
||||
for (index = 0; text[index] && index < TEXT_FIELD_LENGTH; index++)
|
||||
shown[index] = editing && text_field.masked ? L'*' : (wchar_t)(unsigned char)text[index];
|
||||
shown[index] = (wchar_t)(unsigned char)text[index];
|
||||
if (editing && (system_milliseconds() / 500) % 2)
|
||||
shown[index++] = L'_';
|
||||
shown[index] = 0;
|
||||
@@ -2664,8 +2643,6 @@ help_strings, tools/port_settings.py) */
|
||||
#define COOPERATIVE_EXTRA_ENEMIES_HELP 16
|
||||
#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19
|
||||
#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20
|
||||
#define COOPERATIVE_PLAYER_COLLISIONS_HELP 21
|
||||
#define SERVER_PASSWORD_HELP 23
|
||||
|
||||
/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this
|
||||
order): its amount's row is the choice's */
|
||||
@@ -2829,18 +2806,6 @@ static void server_settings_update(struct widget_instance *list)
|
||||
help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP;
|
||||
}
|
||||
}
|
||||
/* ... and its PLAYER COLLISIONS, below them: ON or OFF, its help its
|
||||
choice's */
|
||||
visible_set(named(list, "op_player_collisions", 0), cooperative);
|
||||
if (cooperative && (spinner = named(list, "player_collisions_spinner", 0)) != NULL)
|
||||
{
|
||||
short choice = (short)PIN(spinner->parameters.list.selected_index, 0, 1);
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
|
||||
multiplayer.cooperative_no_player_collisions = choice == 1;
|
||||
if (help && list->focused_child == named(list, "op_player_collisions", 0))
|
||||
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_PLAYER_COLLISIONS_HELP + choice);
|
||||
}
|
||||
}
|
||||
/* LISTING (an internet game's): PUBLIC, listed in everyone's server
|
||||
browser, or PRIVATE, for this game. Its help is its choice's */
|
||||
@@ -2856,41 +2821,6 @@ static void server_settings_update(struct widget_instance *list)
|
||||
if (help && list->focused_child == named(list, "op_listing", 0))
|
||||
help->parameters.text_box.string_list_index = (short)(10 + spinner->parameters.list.selected_index);
|
||||
}
|
||||
/* PASSWORD (a PUBLIC internet game's): its stars, NONE if it has none */
|
||||
row = named(list, "op_password", 0);
|
||||
visible_set(row, named(list, "op_listing", 0) && named(list, "op_listing", 0)->visible &&
|
||||
!server_settings_private());
|
||||
if (row && !row->visible && text_field_editing(row))
|
||||
text_field_end(FALSE);
|
||||
if (row && row->visible)
|
||||
{
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
short index;
|
||||
|
||||
for (index = 0; multiplayer.game_password[index] && index < (short)sizeof(text) - 1; index++)
|
||||
text[index] = '*';
|
||||
text[index] = 0;
|
||||
text_field_show(named(list, "password_value", 0), index ? text : "NONE", text_field_editing(row));
|
||||
if (help && list->focused_child == row)
|
||||
help->parameters.text_box.string_list_index = SERVER_PASSWORD_HELP;
|
||||
}
|
||||
}
|
||||
|
||||
static void server_password_done(char const *text)
|
||||
{
|
||||
snprintf(multiplayer.game_password, sizeof(multiplayer.game_password), "%s", text);
|
||||
}
|
||||
|
||||
/* "ss edit server password" (its row's A: begins, or ends) */
|
||||
static boolean server_password_edit(struct widget_instance *row)
|
||||
{
|
||||
if (text_field_editing(row))
|
||||
{
|
||||
text_field_end(TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
text_field_begin_masked(row, multiplayer.game_password, PASSWORD_LENGTH, server_password_done);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "ss edit server name" (its row's A: begins, or ends) */
|
||||
@@ -2927,16 +2857,12 @@ static boolean server_start(void)
|
||||
text_field_end(TRUE);
|
||||
network_game_server_port_set_settings(multiplayer.game_name,
|
||||
maximum_players[PIN(*server_settings_maximum_players_index(), 0, NUMBEROF(maximum_players) - 1)]);
|
||||
/* (its listing's password: a PUBLIC internet game's, else none) */
|
||||
p2p_set_hosting_password(multiplayer.mode == _multiplayer_mode_host_internet && !server_settings_private() ?
|
||||
multiplayer.game_password : NULL);
|
||||
/* the gametype as Server Setup's options left it (co-op keeps its own,
|
||||
and its FRIENDLY FIRE) */
|
||||
if (hosting_cooperative())
|
||||
{
|
||||
gametype_setup_end();
|
||||
network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire);
|
||||
network_game_server_port_set_cooperative_player_collisions(!multiplayer.cooperative_no_player_collisions);
|
||||
}
|
||||
else if (!gametype_setup_apply())
|
||||
return campaign_fail();
|
||||
@@ -3197,11 +3123,6 @@ static struct
|
||||
/* the last that failed, and when */
|
||||
char failed_name[P2P_LISTING_NAME_SIZE + 1];
|
||||
unsigned long failed_time;
|
||||
/* a game with a password chosen: the password screen's (its own copy:
|
||||
the rows' are read again each frame), and a join it began, which the
|
||||
browser takes up again as it comes back */
|
||||
struct p2p_listing password_game;
|
||||
boolean password_joined;
|
||||
} lobby_browser;
|
||||
|
||||
static void lobby_browser_begin(void)
|
||||
@@ -3210,10 +3131,7 @@ static void lobby_browser_begin(void)
|
||||
lobby_browser.first = 0;
|
||||
lobby_browser.chosen = 0;
|
||||
lobby_browser.shown = FALSE;
|
||||
/* (going on with the join the password screen began) */
|
||||
lobby_browser.joining = lobby_browser.password_joined;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.password_joined = FALSE;
|
||||
lobby_browser.joining = lobby_browser.ready = FALSE;
|
||||
lobby_browser.begin_time = system_milliseconds();
|
||||
p2p_lobby_browse(TRUE);
|
||||
}
|
||||
@@ -3361,18 +3279,8 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
usnprintf(text, ROW_TEXT_LENGTH - 1, L"%d/%d", game->player_count, game->maximum_player_count);
|
||||
text_set(named(row, "server_item_players", 0), text);
|
||||
/* (no ping yet: its host is reached only on joining) */
|
||||
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : !game->open ? L"CLOSED" :
|
||||
game->in_progress ? L"LIVE" : L"-");
|
||||
/* (the lock: a game with a password. Its bitmap's first frame is
|
||||
empty, the PC version's for no lock; the next is the lock, in the
|
||||
menus' blue) */
|
||||
{
|
||||
struct widget_instance *lock = named(row, "server_item_locked", 0);
|
||||
|
||||
visible_set(lock, game->locked);
|
||||
if (lock)
|
||||
lock->animation.current_frame_index = 1;
|
||||
}
|
||||
text_set(named(row, "server_item_ping", 0), game->failed ? L"FAILED" : game->in_progress ? L"LIVE" : L"-");
|
||||
visible_set(named(row, "server_item_locked", 0), !game->open);
|
||||
visible_set(named(row, "server_item_dedicated", 0), FALSE);
|
||||
visible_set(named(row, "server_item_classic", 0), FALSE);
|
||||
}
|
||||
@@ -3447,10 +3355,9 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
wchar_t gametype[P2P_LISTING_GAMETYPE_SIZE + 1];
|
||||
|
||||
text_to_wide(game->gametype, gametype, NUMBEROF(gametype));
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s%s", gametype, game->player_count,
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"%s: %d %s of %d%s", gametype, game->player_count,
|
||||
game->player_count == 1 ? L"player" : L"players", game->maximum_player_count,
|
||||
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"",
|
||||
game->locked ? L", password" : L"");
|
||||
!game->open ? L", full or starting" : game->in_progress ? L", under way" : L"");
|
||||
}
|
||||
else
|
||||
text[0] = 0;
|
||||
@@ -3480,26 +3387,9 @@ static void lobby_browser_update(struct widget_instance *list)
|
||||
browser_focus(list);
|
||||
}
|
||||
|
||||
/* the game's host reached by its invite (joined, once it is reached:
|
||||
lobby_browser_update) */
|
||||
static boolean lobby_browser_join(struct p2p_listing const *game, short controller)
|
||||
{
|
||||
if (!p2p_join_invite(game->invite))
|
||||
return FALSE;
|
||||
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
|
||||
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
|
||||
lobby_browser.joining = TRUE;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.join_time = system_milliseconds();
|
||||
lobby_browser.controller = controller;
|
||||
ui_play_audio_feedback_sound(SOUND_FORWARD);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* the server browser's rows and buttons: Refresh asks the hosts again; a
|
||||
row (or Join) joins its game's invite, a game with a password once its
|
||||
screen has it; the A press posted once its host is reached joins its game
|
||||
(or shows its lobby, if under way) */
|
||||
row (or Join) joins its game's invite; the A press posted once its host is
|
||||
reached joins its game (or shows its lobby, if under way) */
|
||||
static boolean lobby_browser_select(struct widget_instance *widget, short controller, short row,
|
||||
boolean *widget_deleted)
|
||||
{
|
||||
@@ -3544,117 +3434,15 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
|
||||
if (index >= lobby_browser.count)
|
||||
return campaign_fail();
|
||||
game = &lobby_browser.games[index];
|
||||
if (!game->open || !config_boolean("network.online"))
|
||||
if (!game->open || !config_boolean("network.online") || !p2p_join_invite(game->invite))
|
||||
return campaign_fail();
|
||||
/* (a game with a password: its screen asks for it, then joins) */
|
||||
if (game->locked)
|
||||
{
|
||||
lobby_browser.password_game = *game;
|
||||
lobby_browser.controller = controller;
|
||||
return ui_widget_port_open(widget, PASSWORD_NAME, widget_deleted);
|
||||
}
|
||||
return lobby_browser_join(game, controller) || campaign_fail();
|
||||
}
|
||||
|
||||
/* ---- the server browser's password screen (a game with a password
|
||||
chosen: lobby_browser_select, tools/port_settings.py): its password typed at
|
||||
once, shown as stars; JOIN GAME, or Enter, joins the game with it if it is
|
||||
the game's (the browser, back again, goes on with the join), else its help
|
||||
says it is not and the typing begins again */
|
||||
|
||||
/* its help's lines (tools/port_settings.py's) */
|
||||
enum
|
||||
{
|
||||
_password_help_ask = 1,
|
||||
_password_help_wrong,
|
||||
_password_help_failed,
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
char text[PASSWORD_LENGTH + 1];
|
||||
short help;
|
||||
} password_screen;
|
||||
|
||||
static void password_screen_done(char const *text)
|
||||
{
|
||||
snprintf(password_screen.text, sizeof(password_screen.text), "%s", text);
|
||||
}
|
||||
|
||||
/* "port password init" */
|
||||
static boolean password_screen_initialize(struct widget_instance *list)
|
||||
{
|
||||
struct widget_instance *row = named(list, "op_password", 0);
|
||||
|
||||
password_screen.text[0] = 0;
|
||||
password_screen.help = _password_help_ask;
|
||||
if (row)
|
||||
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password update" */
|
||||
static void password_screen_update(struct widget_instance *list)
|
||||
{
|
||||
struct widget_instance *help = list->parameters.list.extended_description;
|
||||
char stars[PASSWORD_LENGTH + 1];
|
||||
short index;
|
||||
|
||||
for (index = 0; password_screen.text[index] && index < PASSWORD_LENGTH; index++)
|
||||
stars[index] = '*';
|
||||
stars[index] = 0;
|
||||
text_field_show(named(list, "password_value", 0), stars, text_field_editing(named(list, "op_password", 0)));
|
||||
if (help)
|
||||
help->parameters.text_box.string_list_index = password_screen.help;
|
||||
}
|
||||
|
||||
/* "port password join" (JOIN GAME, and Enter: password_screen_edit) */
|
||||
static boolean password_screen_join(struct widget_instance *widget, boolean *widget_deleted)
|
||||
{
|
||||
struct p2p_listing game = lobby_browser.password_game;
|
||||
struct widget_instance *row = named(screen_of(widget), "op_password", 0);
|
||||
|
||||
if (text_field_editing(NULL))
|
||||
text_field_end(TRUE);
|
||||
if (!p2p_listing_unlock(&game, password_screen.text))
|
||||
{
|
||||
password_screen.help = _password_help_wrong;
|
||||
password_screen.text[0] = 0;
|
||||
if (row)
|
||||
text_field_begin_masked(row, "", PASSWORD_LENGTH, password_screen_done);
|
||||
return campaign_fail();
|
||||
}
|
||||
if (!lobby_browser_join(&game, lobby_browser.controller))
|
||||
{
|
||||
password_screen.help = _password_help_failed;
|
||||
return campaign_fail();
|
||||
}
|
||||
password_screen.text[0] = 0;
|
||||
lobby_browser.password_joined = TRUE;
|
||||
ui_widget_port_go_back(widget);
|
||||
*widget_deleted = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password back" (B, Escape): back to the browser at once, though the
|
||||
password is being typed */
|
||||
static boolean password_screen_back(struct widget_instance *widget, boolean *widget_deleted)
|
||||
{
|
||||
if (text_field_editing(NULL))
|
||||
text_field_end(FALSE);
|
||||
password_screen.text[0] = 0;
|
||||
ui_widget_port_go_back(widget);
|
||||
*widget_deleted = TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* "port password edit" (the field's row's A: the typing begins; Enter, its
|
||||
end, joins) */
|
||||
static boolean password_screen_edit(struct widget_instance *row, boolean *widget_deleted)
|
||||
{
|
||||
if (text_field_editing(row))
|
||||
return password_screen_join(row, widget_deleted);
|
||||
text_field_begin_masked(row, password_screen.text, PASSWORD_LENGTH, password_screen_done);
|
||||
csmemcpy(lobby_browser.identifier, game->identifier, sizeof(lobby_browser.identifier));
|
||||
csmemcpy(lobby_browser.name, game->name, sizeof(lobby_browser.name));
|
||||
lobby_browser.joining = TRUE;
|
||||
lobby_browser.ready = FALSE;
|
||||
lobby_browser.join_time = system_milliseconds();
|
||||
lobby_browser.controller = controller;
|
||||
ui_play_audio_feedback_sound(SOUND_FORWARD);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -5162,26 +4950,6 @@ boolean pc_menu_event_function_invoke(
|
||||
{
|
||||
return server_name_edit(widget);
|
||||
}
|
||||
else if (!strcmp(name, "ss edit server password"))
|
||||
{
|
||||
return server_password_edit(widget);
|
||||
}
|
||||
else if (!strcmp(name, "port password init"))
|
||||
{
|
||||
return password_screen_initialize(widget);
|
||||
}
|
||||
else if (!strcmp(name, "port password edit"))
|
||||
{
|
||||
return password_screen_edit(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "port password join"))
|
||||
{
|
||||
return password_screen_join(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "port password back"))
|
||||
{
|
||||
return password_screen_back(widget, widget_deleted);
|
||||
}
|
||||
else if (!strcmp(name, "ss copy invite"))
|
||||
{
|
||||
return invite_copy();
|
||||
@@ -5361,8 +5129,6 @@ void pc_menu_game_data_function_invoke(
|
||||
gametype_list_update(widget);
|
||||
else if (!strcmp(name, "server settings update"))
|
||||
server_settings_update(widget);
|
||||
else if (!strcmp(name, "port password update"))
|
||||
password_screen_update(widget);
|
||||
else if (!strcmp(name, "gamespy screen update"))
|
||||
browser_update(widget);
|
||||
else if (!strcmp(name, "gt edit list update"))
|
||||
|
||||
@@ -361,8 +361,6 @@ static char const *const port_function_names[] =
|
||||
/* (Settings' OK: the Xbox's fails when the profile has no changes, the
|
||||
settings' screens having written theirs to config.toml) */
|
||||
"player profile save changes",
|
||||
/* (the server browser's password screen) */
|
||||
"port password init", "port password edit", "port password join", "port password back",
|
||||
};
|
||||
|
||||
/* the PC version's game data functions that the Xbox's have not, from
|
||||
@@ -380,7 +378,6 @@ static char const *const port_game_data_input_names[] =
|
||||
/* (the gametype editor's: the Xbox's stops the game on the buttons'
|
||||
row; the Xbox's read only player_ui's gametype, not Server Setup's) */
|
||||
"game settings lists text update", "get edit game settings name", "mp edit profile set rule text",
|
||||
"port password update",
|
||||
};
|
||||
|
||||
static struct
|
||||
@@ -1249,14 +1246,6 @@ static struct cache_file_tag_instance *instances_grow(long count, long *first_in
|
||||
|
||||
if (!instances)
|
||||
return NULL;
|
||||
/* (every tag's absolute index fits a tag index's 16 bits, short of
|
||||
NONE's, and the table's size cannot wrap: the map's count was checked
|
||||
as it loaded, cache_files.c, and is again) */
|
||||
if (existing < 0 || count < 0 || existing > UNSIGNED_SHORT_MAX - count)
|
||||
{
|
||||
platform_log("menus: %ld tags and the map's %ld are too many for a tag table", count, existing);
|
||||
return NULL;
|
||||
}
|
||||
grown = allocate((existing + count) * sizeof(*grown));
|
||||
if (!grown)
|
||||
return NULL;
|
||||
|
||||
@@ -29,8 +29,6 @@ that clients need to see is sent from this file:
|
||||
every tick in rotation, those that have moved since the map loaded more
|
||||
often. A scenario group is identified by its index, which is the same on
|
||||
every machine; a device's own group by the device.
|
||||
- Breakable surfaces (glass) the host breaks, each once as it breaks and
|
||||
a few at a time again in rotation (for a late joiner or a lost message).
|
||||
- Named objects (scenery and devices) the scripts create or destroy.
|
||||
network_objects.c already handles units, vehicles, weapons and equipment.
|
||||
Twice a second the host sends which object names currently exist. A
|
||||
@@ -55,9 +53,7 @@ host skips once more than half the machines have voted. The skip itself is
|
||||
the campaign's: the host reverts to the state saved when the cutscene began
|
||||
and the script carries on past it. Clients catch up through the object,
|
||||
device and name syncs. The host keeps its clock running through the revert,
|
||||
because the netcode needs it to only go forward (network_coop_reverted).
|
||||
A failed mission (game_lost, or the scripts' game_revert) reverts the same
|
||||
way, rather than leaving the players stuck in the failure's cutscene.
|
||||
because the netcode needs it to only go forward (network_coop_skip_reverted).
|
||||
|
||||
Objects (scenery and devices) that aren't synced by network_objects.c are
|
||||
found on a client by their scenario name, or, unnamed, by their object
|
||||
@@ -92,15 +88,11 @@ index and tag, since the map placed them at the same index everywhere.
|
||||
#include "models/model_animation_definitions.h"
|
||||
#include "models/model_definitions.h"
|
||||
#include "models/models.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "networking/network_game_manager.h"
|
||||
#include "objects/object_definitions.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/object_types.h"
|
||||
#include "objects/damage.h"
|
||||
#include "objects/scenery.h"
|
||||
#include "physics/breakable_surfaces.h"
|
||||
#include "structures/structure_bsp_definitions.h"
|
||||
#include "physics/collisions.h"
|
||||
#include "rasterizer/rasterizer_cinematics.h"
|
||||
#include "saved games/game_state.h"
|
||||
@@ -192,13 +184,9 @@ enum
|
||||
_coop_event_effect,
|
||||
_coop_event_attach,
|
||||
/* the host skipped the cutscene: a client stops its dialogue */
|
||||
/* the host reverted to its last saved state */
|
||||
_coop_event_reverted,
|
||||
_coop_event_cutscene_skipped,
|
||||
/* a unit opened (value TRUE) or closed: a dropship's doors */
|
||||
_coop_event_unit_open,
|
||||
/* a breakable surface broke: value its index, frame its BSP, reals the
|
||||
damage's epicenter */
|
||||
_coop_event_surface_broken,
|
||||
};
|
||||
|
||||
/* distributed_coop_event.type of an effect: at a cutscene flag (value), or on
|
||||
@@ -282,11 +270,7 @@ struct distributed_coop_presentation
|
||||
/* whether the scripts have set the players' maximum vitality, and to
|
||||
what (network_coop_set_players_vitality) */
|
||||
byte players_vitality_set;
|
||||
/* the cluster the scripts keep active (object_pvs_activate) plus one,
|
||||
0 for none: a cutscene filmed far from every player (b30's Elite at the
|
||||
shaft door) is active on the clients too. Plus one so the zeroed padding
|
||||
of a host without it reads as none. */
|
||||
short activating_cluster;
|
||||
byte pad[2];
|
||||
real players_maximum_body_vitality;
|
||||
real players_maximum_shield_vitality;
|
||||
/* which teams are allies and friends (game_allegiance.c): the scripts
|
||||
@@ -1445,7 +1429,6 @@ static void host_presentation(
|
||||
presentation->input_disabled = (byte)!player_input_enabled();
|
||||
presentation->scripted_shake = (byte)player_effect_port_scripted_active();
|
||||
presentation->players_vitality_set = (byte)players_vitality.set;
|
||||
presentation->activating_cluster = (short)(objects_get_activating_cluster_index() + 1);
|
||||
presentation->players_maximum_body_vitality = players_vitality.maximum_body;
|
||||
presentation->players_maximum_shield_vitality = players_vitality.maximum_shield;
|
||||
game_allegiance_get_teams(presentation->ally_teams, presentation->friendly_teams);
|
||||
@@ -1561,10 +1544,7 @@ static void client_apply_player_effect(
|
||||
switch (event->type)
|
||||
{
|
||||
case _coop_player_effect_translation:
|
||||
/* (the camera's shake, which moves it no further than an observer
|
||||
accepts) */
|
||||
scripted_player_effect_set_translation(PIN(reals[0], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND),
|
||||
PIN(reals[1], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND), PIN(reals[2], -CAMERA_WORLD_BOUND, CAMERA_WORLD_BOUND));
|
||||
scripted_player_effect_set_translation(reals[0], reals[1], reals[2]);
|
||||
break;
|
||||
case _coop_player_effect_rotation:
|
||||
scripted_player_effect_set_rotation(reals[0], reals[1], reals[2]);
|
||||
@@ -1822,17 +1802,9 @@ static void client_apply_event(
|
||||
case _coop_event_attach:
|
||||
client_apply_attach(event);
|
||||
break;
|
||||
case _coop_event_reverted:
|
||||
case _coop_event_cutscene_skipped:
|
||||
client_stop_script_sounds();
|
||||
break;
|
||||
case _coop_event_surface_broken:
|
||||
if (event->frame == global_structure_bsp_index)
|
||||
{
|
||||
real_point3d epicenter = { event->reals[0], event->reals[1], event->reals[2] };
|
||||
|
||||
breakable_surface_port_break(event->value, &epicenter);
|
||||
}
|
||||
break;
|
||||
case _coop_event_unit_open:
|
||||
if (distributed_object_index_valid(event->object_index) && network_objects_client_has(event->object_index) &&
|
||||
object_try_and_get_and_verify_type(event->object_index, _object_mask_unit))
|
||||
@@ -1963,16 +1935,6 @@ boolean network_coop_active(
|
||||
global_scenario && global_scenario->type == _scenario_type_solo && !game_engine_running();
|
||||
}
|
||||
|
||||
boolean network_coop_player_collisions(
|
||||
void)
|
||||
{
|
||||
struct network_game *game;
|
||||
|
||||
if (!network_coop_active() || !(game = network_game_get_game()))
|
||||
return TRUE;
|
||||
return !TEST_FLAG(game->cooperative_flags, _network_game_cooperative_no_player_collisions_bit);
|
||||
}
|
||||
|
||||
boolean network_coop_devices_remote(
|
||||
void)
|
||||
{
|
||||
@@ -2468,51 +2430,6 @@ void network_coop_note_effect(
|
||||
event->value = cutscene_flag_index;
|
||||
}
|
||||
|
||||
void network_coop_note_surface_broken(
|
||||
short breakable_surface_index,
|
||||
real_point3d const *epicenter)
|
||||
{
|
||||
struct distributed_coop_event *event = event_new(_coop_event_surface_broken);
|
||||
|
||||
if (!event)
|
||||
return;
|
||||
event->value = breakable_surface_index;
|
||||
event->frame = global_structure_bsp_index;
|
||||
event->reals[0] = epicenter->x;
|
||||
event->reals[1] = epicenter->y;
|
||||
event->reals[2] = epicenter->z;
|
||||
}
|
||||
|
||||
/* host: a few broken surfaces again each refresh, round them all, for a
|
||||
machine that joined since or lost the message (broken from their middle) */
|
||||
static void host_send_broken_surfaces(
|
||||
void)
|
||||
{
|
||||
enum { SURFACES_PER_REFRESH = 8 };
|
||||
static short cursor;
|
||||
struct structure_bsp *structure_bsp = global_structure_bsp_get();
|
||||
short count = (short)structure_bsp->breakable_surfaces.count;
|
||||
short sent = 0;
|
||||
short step;
|
||||
|
||||
if (!host_resend.refresh || count <= 0)
|
||||
return;
|
||||
for (step = 0; step < count && sent < SURFACES_PER_REFRESH; step++)
|
||||
{
|
||||
short index = (short)((cursor + step) % count);
|
||||
|
||||
if (!breakable_surface_extant(index))
|
||||
{
|
||||
struct structure_breakable_surface *surface = TAG_BLOCK_GET_ELEMENT(&structure_bsp->breakable_surfaces,
|
||||
index, struct structure_breakable_surface);
|
||||
|
||||
network_coop_note_surface_broken(index, &surface->centroid);
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
cursor = (short)((cursor + step) % count);
|
||||
}
|
||||
|
||||
void network_coop_note_object_effect(
|
||||
long effect_definition_index,
|
||||
long object_index,
|
||||
@@ -2548,7 +2465,7 @@ boolean network_coop_vote_skip(
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* The host reverted its game state, clock included. The clock is
|
||||
/* The skip reverted the host's game state, clock included. The clock is
|
||||
put back, since clients and the netcode expect it to only go forward, and
|
||||
the script threads' wake times move by the same amount so they still wake
|
||||
when they would have. The revert also renumbered the input queues from the
|
||||
@@ -2557,7 +2474,7 @@ from the restored one, or no tick would run until they caught up. And the
|
||||
revert is stamped with the restored time, since the script's game_reverted
|
||||
compares the stamp with the clock: otherwise it would play the cinematic
|
||||
it was meant to skip. */
|
||||
void network_coop_reverted(
|
||||
void network_coop_skip_reverted(
|
||||
long now)
|
||||
{
|
||||
long ticks;
|
||||
@@ -2574,15 +2491,8 @@ void network_coop_reverted(
|
||||
}
|
||||
/* (the dropships' riders kept are of the game state reverted from) */
|
||||
coop_enemies_reset();
|
||||
error(_error_silent, "co-op: reverted %ld ticks, clock kept at %ld", ticks, now);
|
||||
event_new(_coop_event_reverted);
|
||||
}
|
||||
|
||||
void network_coop_skip_done(
|
||||
void)
|
||||
{
|
||||
if (!coop_host())
|
||||
return;
|
||||
error(_error_silent, "co-op: cutscene skipped; reverted %ld ticks, clock kept at %ld", ticks, now);
|
||||
event_new(_coop_event_cutscene_skipped);
|
||||
skip_vote_clear();
|
||||
skip_vote.requested = FALSE;
|
||||
skip_vote.cooldown_until = game_time_get() + SKIP_COOLDOWN_TICKS;
|
||||
@@ -2627,7 +2537,6 @@ void network_coop_host_tick(
|
||||
host_send_attachments();
|
||||
host_send_looping_sounds();
|
||||
host_send_hud_state();
|
||||
host_send_broken_surfaces();
|
||||
host_send_events();
|
||||
}
|
||||
|
||||
@@ -2689,16 +2598,7 @@ void network_coop_note_player_structure_bsp(
|
||||
boolean network_coop_player_has_structure_bsp(
|
||||
long player_index)
|
||||
{
|
||||
struct player_datum *player;
|
||||
|
||||
if (!network_coop_active())
|
||||
return TRUE;
|
||||
/* (no player, or none of the tracked: whose BSP is not known, which
|
||||
does not keep their predictions out) */
|
||||
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
|
||||
if (!player || DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return TRUE;
|
||||
return player->local_player_index != NONE ||
|
||||
return !network_coop_active() || player_get(player_index)->local_player_index != NONE ||
|
||||
host_player_structure_bsps[DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index)] == global_structure_bsp_index_get();
|
||||
}
|
||||
|
||||
@@ -3004,7 +2904,6 @@ static void client_presentation_apply(
|
||||
!coop_spectate_watching_rider(0)) || coop_spectate_nothing_to_watch(0));
|
||||
if (coop_presentation.cinematic_started)
|
||||
cinematic_show_letterbox(TEST_FLAG(presentation->flags, _presentation_letterbox_bit));
|
||||
objects_port_set_activating_cluster((short)(presentation->activating_cluster - 1));
|
||||
if (coop_presentation.host_camera)
|
||||
{
|
||||
real_vector3d forward, up;
|
||||
|
||||
@@ -45,9 +45,6 @@ enum
|
||||
/* whether this is a network co-op game (not its lobby, whose menu scene also
|
||||
has no game engine) */
|
||||
boolean network_coop_active(void);
|
||||
/* whether the players collide with each other: FALSE in a co-op game whose
|
||||
host set Server Setup's PLAYER COLLISIONS off (bipeds.c, collisions.c) */
|
||||
boolean network_coop_player_collisions(void);
|
||||
/* whether this machine is a co-op client, whose devices only the host moves */
|
||||
boolean network_coop_devices_remote(void);
|
||||
/* devices.c: a group's devices were set straight to its value */
|
||||
@@ -91,10 +88,6 @@ void network_coop_note_attach(long parent_index, char const *parent_marker_name,
|
||||
char const *child_marker_name);
|
||||
void network_coop_note_detach(long parent_index, long child_index);
|
||||
void network_coop_note_object_effect(long effect_definition_index, long object_index, char const *marker_name);
|
||||
/* breakable_surfaces.c: a breakable surface (glass) broke, from damage at
|
||||
`epicenter`. Each machine's own shots break glass too, but with their own
|
||||
random damage, so the host's breaks are the ones everyone keeps. */
|
||||
void network_coop_note_surface_broken(short breakable_surface_index, real_point3d const *epicenter);
|
||||
void network_coop_note_scenery_animation(long object_index, long animation_graph_index, short animation_index,
|
||||
short frame_index);
|
||||
|
||||
@@ -103,12 +96,9 @@ network_coop_vote_skip returns FALSE outside network co-op, where the
|
||||
cinematic is skipped as usual. */
|
||||
boolean network_coop_skip_offered(void);
|
||||
boolean network_coop_vote_skip(void);
|
||||
/* main.c: after a co-op host reverts to its last saved state (to skip a
|
||||
cinematic, or because the mission failed); now is the game time before the
|
||||
revert */
|
||||
void network_coop_reverted(long now);
|
||||
/* main.c: the skip vote is over once the skip has reverted */
|
||||
void network_coop_skip_done(void);
|
||||
/* main.c: after a co-op host reverts to skip a cinematic; now is the game
|
||||
time before the revert */
|
||||
void network_coop_skip_reverted(long now);
|
||||
/* the vote count to show, if a skippable cinematic is playing */
|
||||
boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted);
|
||||
|
||||
|
||||
@@ -34,10 +34,6 @@ being hit looks and feels like on the clients).
|
||||
- The damage to a unit goes to the machines of the players it concerns
|
||||
(the unit's, its riders', the damage's owner's) and those sent the
|
||||
unit's player this tick, which see it; a killing blow to all.
|
||||
- Scenery and devices that can be destroyed (lights, panels) are the
|
||||
map's, at the same index on every machine. A client reports its hits on
|
||||
them like any other, and every client deals the host's damage to its own
|
||||
copy, which breaks and plays its effects as the host's did.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
@@ -63,8 +59,6 @@ being hit looks and feels like on the clients).
|
||||
void damage_replay_player_effect(long player_index, struct damage_data *damage, real total_damage);
|
||||
void damage_replay_aftermath(long object_index, struct damage_data *damage, unsigned long being_damaged_flags,
|
||||
real shield_damage, real body_damage, real body_damage_multiplier, short body_part);
|
||||
void damage_replay_static(long object_index, struct damage_data *damage, short node_index, short region_index,
|
||||
short material_index);
|
||||
void damage_replay_kill(long object_index, struct damage_data *damage, short node_index, short region_index,
|
||||
short material_index);
|
||||
/* network_distributed.c's */
|
||||
@@ -148,14 +142,8 @@ enum
|
||||
_damage_event_player_effect,
|
||||
_damage_event_aftermath,
|
||||
_damage_event_kill,
|
||||
/* damage to destructible scenery or a device, to every machine */
|
||||
_damage_event_static,
|
||||
};
|
||||
|
||||
/* the objects every machine has from the map, at the same index, which
|
||||
damage can break (static_target) */
|
||||
#define STATIC_TARGET_TYPES (_object_mask_scenery | _object_mask_device)
|
||||
|
||||
/* what a player's damage is, by how the game deals it (the shape of their
|
||||
report of it) */
|
||||
enum
|
||||
@@ -479,8 +467,7 @@ static boolean distributed_damage_to_data(
|
||||
if (!tag_index_is_group(damage->definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return FALSE;
|
||||
damage_data_new(result, damage->definition_index);
|
||||
/* (the flags the game has) */
|
||||
result->flags = damage->flags & (FLAG(NUMBER_OF_DAMAGE_DATA_FLAGS) - 1);
|
||||
result->flags = damage->flags;
|
||||
result->owner_player_index = distributed_player_from_byte(damage->owner_player_index);
|
||||
result->owner_team_index = damage->owner_team_index;
|
||||
result->owner_object_index = distributed_object_index_valid(damage->owner_object_index) &&
|
||||
@@ -491,22 +478,11 @@ static boolean distributed_damage_to_data(
|
||||
result->scale = damage->scale;
|
||||
result->multiplier = damage->multiplier;
|
||||
result->material_effect_scale = damage->material_effect_scale;
|
||||
/* (a material the game has, else none, damage_data_new's: it indexes
|
||||
the materials' modifiers) */
|
||||
if (damage->material_type >= 0 && damage->material_type < NUMBER_OF_MATERIAL_TYPES)
|
||||
result->material_type = damage->material_type;
|
||||
result->material_type = damage->material_type;
|
||||
scenario_location_from_point(&result->location, &result->epicenter);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* whether the object is scenery or a device that damage can destroy */
|
||||
static boolean static_target(
|
||||
long object_index)
|
||||
{
|
||||
return object_try_and_get_and_verify_type(object_index, STATIC_TARGET_TYPES) &&
|
||||
object_get_maximum_body_vitality(object_index, TRUE) > 0.0f;
|
||||
}
|
||||
|
||||
/* ---------- object_cause_damage (damage.c) */
|
||||
|
||||
/* whether the damage is a weapon's own at the unit that fires it (weapons.c:
|
||||
@@ -653,8 +629,7 @@ boolean network_damage_deals(
|
||||
damage_replay_player_effect(unit->unit.player_index, &effect, 0.0f);
|
||||
}
|
||||
/* a hit of this machine's own player's, on the host's object */
|
||||
if (distributed_player_is_local(damage->owner_player_index) &&
|
||||
(network_objects_client_has(object_index) || static_target(object_index)) &&
|
||||
if (distributed_player_is_local(damage->owner_player_index) && network_objects_client_has(object_index) &&
|
||||
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
|
||||
{
|
||||
struct distributed_hit_report *report = &damage_reports[damage_report_count++];
|
||||
@@ -742,33 +717,6 @@ void network_damage_player_effect(
|
||||
|
||||
/* the host: an object damaged, and what that did (object_damage_aftermath
|
||||
done, so that a kill's killer is known) */
|
||||
/* a damage event of this tick for the object, or NULL once the tick's are
|
||||
spent (the last RESERVED_KILL_EVENTS are kept for killing blows) */
|
||||
static struct distributed_damage_event *damage_event_new(
|
||||
byte kind,
|
||||
long object_index,
|
||||
struct damage_data const *damage,
|
||||
short node_index,
|
||||
short region_index,
|
||||
short material_index,
|
||||
boolean kill)
|
||||
{
|
||||
struct distributed_damage_event *event;
|
||||
|
||||
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
|
||||
return NULL;
|
||||
event = &damage_events[damage_event_count++];
|
||||
csmemset(event, 0, sizeof(*event));
|
||||
event->kind = kind;
|
||||
event->player_index = NO_PLAYER;
|
||||
event->object_index = object_index;
|
||||
distributed_damage_from_data(damage, &event->damage);
|
||||
event->node_index = node_index;
|
||||
event->region_index = region_index;
|
||||
event->material_index = material_index;
|
||||
return event;
|
||||
}
|
||||
|
||||
void network_damage_aftermath(
|
||||
long object_index,
|
||||
struct damage_data const *damage,
|
||||
@@ -788,11 +736,6 @@ void network_damage_aftermath(
|
||||
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
if (static_target(object_index))
|
||||
{
|
||||
damage_event_new(_damage_event_static, object_index, damage, node_index, region_index, material_index, FALSE);
|
||||
return;
|
||||
}
|
||||
/* (units only: items and the like the objects' states place) */
|
||||
unit = (struct unit_datum *)object_try_and_get_and_verify_type(object_index, _object_mask_unit);
|
||||
if (!unit)
|
||||
@@ -806,15 +749,22 @@ void network_damage_aftermath(
|
||||
(victim_player_index != NONE ||
|
||||
(TEST_FLAG(_object_mask_biped, unit->object.type) &&
|
||||
!TEST_FLAG(being_damaged_flags, _object_being_damaged_body_destroyed_bit)));
|
||||
event = damage_event_new(_damage_event_aftermath, object_index, damage, node_index, region_index, material_index,
|
||||
kill);
|
||||
if (!event)
|
||||
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
|
||||
return;
|
||||
event = &damage_events[damage_event_count++];
|
||||
csmemset(event, 0, sizeof(*event));
|
||||
event->kind = _damage_event_aftermath;
|
||||
event->player_index = NO_PLAYER;
|
||||
event->object_index = object_index;
|
||||
distributed_damage_from_data(damage, &event->damage);
|
||||
event->being_damaged_flags = being_damaged_flags;
|
||||
event->shield_damage = shield_damage;
|
||||
event->body_damage = body_damage;
|
||||
event->body_damage_multiplier = body_damage_multiplier;
|
||||
event->body_part = body_part;
|
||||
event->node_index = node_index;
|
||||
event->region_index = region_index;
|
||||
event->material_index = material_index;
|
||||
/* a killing blow, a player's with who the host says dealt it */
|
||||
if (kill)
|
||||
{
|
||||
@@ -1769,8 +1719,6 @@ static boolean distributed_report_valid(
|
||||
return FALSE;
|
||||
target = (struct object_datum *)object_try_and_get_and_verify_type(report->object_index,
|
||||
_object_mask_biped | _object_mask_vehicle | _object_mask_weapon | _object_mask_equipment);
|
||||
if (!target && static_target(report->object_index))
|
||||
target = object_get(report->object_index);
|
||||
if (!target || !tag_index_is_group(report->damage.definition_index, DAMAGE_EFFECT_DEFINITION_TAG))
|
||||
return FALSE;
|
||||
/* numbers the game can take */
|
||||
@@ -2212,15 +2160,6 @@ void network_damage_handle_events(
|
||||
players_show_telefragged(victim_player_index);
|
||||
}
|
||||
break;
|
||||
case _damage_event_static:
|
||||
if (distributed_object_index_valid(event->object_index) && static_target(event->object_index) &&
|
||||
distributed_damage_indices_valid(event->object_index, event->node_index, event->region_index,
|
||||
event->material_index))
|
||||
{
|
||||
damage_replay_static(event->object_index, &damage, event->node_index, event->region_index,
|
||||
event->material_index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ void game_engine_read_network_state(byte const *buffer, long size);
|
||||
/* physics.c's (world units a tick, each tick) */
|
||||
extern real global_gravity;
|
||||
/* network_server_manager.c's, cseries_windows.c's, console.c's, p2p.c's */
|
||||
void network_game_server_kick_machine(long machine_index, boolean kept_out);
|
||||
void network_game_server_kick_machine(long machine_index);
|
||||
unsigned long network_game_server_machine_address(long machine_index);
|
||||
char const *network_game_server_machine_hardware_id(long machine_index);
|
||||
void p2p_hardware_id_sanitize(char *destination, int size, const char *source);
|
||||
@@ -588,10 +588,7 @@ static long distributed_received_times[MAXIMUM_SENDERS][NUMBER_OF_DISTRIBUTED_ME
|
||||
/* the host: each client machine's clock, measured (CLIENT_CLOCK_WINDOW_MILLISECONDS):
|
||||
its latest tick, and its tick and the host's time as the window began
|
||||
(NONE: none begun); how many windows in a row it went fast, and whether
|
||||
the last did; and whether, since they began, a message that came over its
|
||||
stream was that far ahead too (a datagram's source address is all that
|
||||
says which machine sent it, and anyone can send one as from another's: a
|
||||
machine is banned only on its stream's word, distributed_note_client_clock) */
|
||||
the last did */
|
||||
/* the host: each client machine's Discord user, as it told it, the text
|
||||
kept only of what is allowed (p2p_discord_sanitize) */
|
||||
static struct distributed_client_identity distributed_client_identities[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
@@ -605,11 +602,7 @@ static struct distributed_client_clock
|
||||
unsigned long window_time;
|
||||
short fast_windows;
|
||||
boolean fast;
|
||||
boolean ahead_on_stream;
|
||||
} distributed_client_clocks[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
/* the host: whether the message being handled came over its machine's
|
||||
stream (network_distributed_handle_stream_message) */
|
||||
static boolean distributed_handling_stream_message;
|
||||
/* a client: the host's latest tick it has had a message of */
|
||||
static long distributed_host_time = NONE;
|
||||
/* the host: each client's round trip, in ticks, and its jitter */
|
||||
@@ -1629,14 +1622,6 @@ static void distributed_correct_own_unit(
|
||||
short local_player_index = player->local_player_index;
|
||||
real_point3d position = state->position;
|
||||
|
||||
/* (co-op: the host has its player in a BSP this machine hasn't loaded
|
||||
yet, the switch still on its way. Moved there, it would fall with no
|
||||
floor under it, so it stays on the floor it has until the BSP loads.) */
|
||||
if (network_coop_active() && scenario_leaf_index_from_point(&state->position) == NONE &&
|
||||
scenario_leaf_index_from_point(&object_get(unit_index)->object.position) != NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (TEST_FLAG(state->flags, _distributed_unit_predicted_bit) &&
|
||||
local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS)
|
||||
{
|
||||
@@ -2104,7 +2089,6 @@ static void distributed_apply_predictions(
|
||||
from a co-op client still loading the host's BSP, whose player
|
||||
falls there with no floor under it) */
|
||||
if (unit_index != NONE && unit_index == state->unit_index &&
|
||||
unit_get(unit_index)->unit.player_index != NONE &&
|
||||
network_coop_player_has_structure_bsp(unit_get(unit_index)->unit.player_index))
|
||||
{
|
||||
distributed_take_prediction(player_index, unit_index, &bound);
|
||||
@@ -3725,8 +3709,6 @@ static void distributed_note_client_clock(
|
||||
if (machine_index < 0 || machine_index >= HALO_PORT_MAXIMUM_NETWORK_MACHINES)
|
||||
return;
|
||||
clock = &distributed_client_clocks[machine_index];
|
||||
if (distributed_handling_stream_message && tick - game_time_get() > CLIENT_CLOCK_AHEAD_TICKS)
|
||||
clock->ahead_on_stream = TRUE;
|
||||
if (clock->window_tick == NONE)
|
||||
{
|
||||
clock->latest_tick = tick;
|
||||
@@ -3748,7 +3730,6 @@ static void distributed_note_client_clock(
|
||||
if (!clock->fast)
|
||||
{
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
else if (++clock->fast_windows == 1)
|
||||
{
|
||||
@@ -3765,7 +3746,7 @@ static void distributed_note_client_clock(
|
||||
char discord_id[DISCORD_ID_SIZE];
|
||||
char discord_name[DISCORD_NAME_SIZE];
|
||||
char discord[DISCORD_ID_SIZE + DISCORD_NAME_SIZE + 16] = "";
|
||||
char reason[96];
|
||||
char reason[64];
|
||||
char text[MAXIMUM_NOTICE_LENGTH];
|
||||
|
||||
distributed_machine_player_names(machine_index, names, sizeof(names));
|
||||
@@ -3775,30 +3756,14 @@ static void distributed_note_client_clock(
|
||||
distributed_client_identities[machine_index].discord_name, 1);
|
||||
if (discord_id[0] || discord_name[0])
|
||||
snprintf(discord, sizeof(discord), " (Discord: %s, %s)", discord_name, discord_id);
|
||||
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)%s", rate,
|
||||
clock->ahead_on_stream ? "" : " (its datagrams only: not banned)");
|
||||
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)", rate);
|
||||
snprintf(text, sizeof(text), "%s%s kicked by the host: their game ran %.2f times as fast (a speed hack)",
|
||||
names, discord, rate);
|
||||
distributed_send_notice(text);
|
||||
/* banned, and kept out, only when its stream said so
|
||||
too; on its datagrams' word alone (which another machine
|
||||
could have sent as from its address), logged and dropped,
|
||||
and it may join again */
|
||||
if (clock->ahead_on_stream)
|
||||
{
|
||||
distributed_log_cheater(machine_index, names, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
char address[32];
|
||||
|
||||
distributed_machine_address_text(machine_index, address, sizeof(address));
|
||||
distributed_write_player_record(CHEATERS_FILE, address, machine_index, names, reason);
|
||||
}
|
||||
distributed_log_cheater(machine_index, names, reason);
|
||||
}
|
||||
network_game_server_kick_machine(machine_index, clock->ahead_on_stream);
|
||||
network_game_server_kick_machine(machine_index);
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
}
|
||||
clock->window_tick = clock->latest_tick;
|
||||
@@ -4136,16 +4101,3 @@ void network_distributed_handle_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host: network_server_message_handler.c) a client machine's message
|
||||
of the distributed kind that came over its stream, which no other machine
|
||||
can send as it: as network_distributed_handle_message */
|
||||
void network_distributed_handle_stream_message(
|
||||
long machine_index,
|
||||
word const *message,
|
||||
word size)
|
||||
{
|
||||
distributed_handling_stream_message = TRUE;
|
||||
network_distributed_handle_message(machine_index, message, size);
|
||||
distributed_handling_stream_message = FALSE;
|
||||
}
|
||||
@@ -60,7 +60,6 @@ same datum index (identifier and all), so that any message can name one:
|
||||
#include "items/weapons.h"
|
||||
#include "items/weapon_definitions.h"
|
||||
#include "items/equipment_definitions.h"
|
||||
#include "camera/observer.h"
|
||||
#include "cutscene/cinematics.h"
|
||||
#include "network_coop.h"
|
||||
#include "network_distributed.h"
|
||||
@@ -245,11 +244,7 @@ struct distributed_object_change
|
||||
byte change;
|
||||
byte flags;
|
||||
byte owner_player_index;
|
||||
/* which bitmap of its shaders it draws with, when not its tag's (0). An
|
||||
AI unit's actor variant sets it after the unit is made
|
||||
(actor_customize_unit): the Elite major's and commander's armor are 1
|
||||
and 2. Builds before it sent 0 here. */
|
||||
byte forced_shader_permutation_index;
|
||||
byte pad;
|
||||
long object_index;
|
||||
long definition_index;
|
||||
short owner_team_index;
|
||||
@@ -1008,7 +1003,6 @@ static void distributed_change_from_object(
|
||||
change->owner_player_index = distributed_player_to_byte(object->object.owner_player_index);
|
||||
change->owner_team_index = object->object.owner_team_index;
|
||||
change->variant_number = object->object.variant_number;
|
||||
change->forced_shader_permutation_index = (byte)PIN(object->object.forced_shader_permutation_index, 0, 0xFF);
|
||||
change->position = object->object.position;
|
||||
change->forward = object->object.forward;
|
||||
change->up = object->object.up;
|
||||
@@ -1298,20 +1292,8 @@ static short distributed_host_object_period(
|
||||
count = objects_host_viewers.player_unit_count;
|
||||
origins = objects_host_viewers.player_unit_origins;
|
||||
}
|
||||
/* in a co-op cutscene every client looks through the host's camera, so
|
||||
what it films is sent as often as what's next to the client's players
|
||||
(b30's sword Elite walking out of the door) */
|
||||
if (network_coop_active() && cinematic_in_progress())
|
||||
{
|
||||
real_point3d const *camera = &observer_get_camera(0)->position;
|
||||
real dx = position->x - camera->x;
|
||||
real dy = position->y - camera->y;
|
||||
real dz = position->z - camera->z;
|
||||
|
||||
nearest = dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
/* nobody is alive anywhere, so nothing needs to be sent often */
|
||||
if (!count && nearest < 0.0f)
|
||||
if (!count)
|
||||
return MAXIMUM_OBJECT_PERIOD_TICKS;
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -2160,8 +2142,6 @@ static void distributed_client_apply_change(
|
||||
struct object_datum *object = object_get(object_index);
|
||||
|
||||
csmemcpy(object->object.base_change_colors, change->change_colors, sizeof(object->object.base_change_colors));
|
||||
if (change->forced_shader_permutation_index)
|
||||
object->object.forced_shader_permutation_index = change->forced_shader_permutation_index;
|
||||
/* (and the colors drawn, which object_new chose from the tag: an AI unit's
|
||||
are its variant's, set after it was made, actors.c) */
|
||||
{
|
||||
|
||||
@@ -3,22 +3,19 @@ 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.
|
||||
weapon aims where the right controller points and fires from the hand, and
|
||||
the left hand holds a grenade and throws it its own way (port/linux/src/
|
||||
vr.c); the input the netcode relays has only where the head looks, so the
|
||||
other machines would show that player aiming and throwing where they look.
|
||||
|
||||
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.
|
||||
its muzzle is from the unit's camera, whether the left hand holds a
|
||||
grenade, and the last throw (from where, which way, how hard) for a second
|
||||
after it. The host keeps them, its own VR players' too, and sends each
|
||||
client those of the players of other machines. A machine drives a VR
|
||||
player of another (players.c, weapons.c, units.c) as long as their hands
|
||||
keep coming, else as before.
|
||||
|
||||
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).
|
||||
|
||||
@@ -16,10 +16,7 @@ also need the tick after the pair it spans: two ticks behind.)
|
||||
Rotations are blended as quaternions (normalised lerp, taking the shorter
|
||||
way round), positions and scales linearly. Anything that moves further than
|
||||
a tick of motion plausibly allows (teleports, respawns, camera cuts) snaps
|
||||
instead of sweeping across the world, and so does a pose whose nodes moved
|
||||
too far for one tick: two snapshots of different poses (a model swapped,
|
||||
another weapon's skeleton, a pose left from seconds before) blended node by
|
||||
node stretch vertices across the screen.
|
||||
instead of sweeping across the world.
|
||||
|
||||
Particles, contrails and other effects already move every frame
|
||||
(game_frame), so they need nothing here.
|
||||
@@ -59,17 +56,6 @@ unsigned long config_changes(void);
|
||||
/* world units (10 feet each) a node may move in one tick before it snaps:
|
||||
well beyond any vehicle, short of any teleport */
|
||||
#define OBJECT_SNAP_DISTANCE 10.0f
|
||||
/* ... a node may move in the object's root node's frame in one tick before
|
||||
the pose is taken for a new one, not blended to: further than any limb or
|
||||
part of a model moves in 33 ms (37 m/s), short of the game changing a pose
|
||||
at once (an actor waking from dormancy, a model swapped), which blended
|
||||
sweeps the vertices through poses it never had. (In the root's frame, so
|
||||
that the whole object turning moves nothing: measured in the world, a fast
|
||||
turn swung the head and weapon far enough to snap, and characters moved
|
||||
like robots.) */
|
||||
#define NODE_SNAP_DISTANCE 0.4f
|
||||
/* ... a first-person node may move relative to the camera */
|
||||
#define FIRST_PERSON_SNAP_DISTANCE 0.25f
|
||||
/* a correction's difference left drawn after each tick (of 1) */
|
||||
#define CORRECTION_DECAY 0.6f
|
||||
/* ... and small enough to be none */
|
||||
@@ -325,6 +311,16 @@ static void matrix_blend_rotations(
|
||||
result->up.k = lerp(a->up.k, b->up.k, t);
|
||||
}
|
||||
|
||||
/* a matrix a fraction t of the way from a to b */
|
||||
static void matrix_blend(real_matrix4x3 const *a, real_matrix4x3 const *b, real t, real_matrix4x3 *result)
|
||||
{
|
||||
struct interpolation_rotation rotation_a, rotation_b;
|
||||
|
||||
rotation_from_matrix(a, &rotation_a);
|
||||
rotation_from_matrix(b, &rotation_b);
|
||||
matrix_blend_rotations(a, b, &rotation_a, &rotation_b, t, result);
|
||||
}
|
||||
|
||||
/* the vector's parts' sum, large enough to be a correction (so written that
|
||||
one not a number is none) */
|
||||
static boolean correction_significant(real_vector3d const *correction)
|
||||
@@ -534,22 +530,14 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
|
||||
real_matrix4x3 const *latest = record->nodes + record->latest * record->node_capacity;
|
||||
real_matrix4x3 *blended = record->nodes + 2 * record->node_capacity;
|
||||
short node_index;
|
||||
|
||||
/* (so written that a position not a number snaps) */
|
||||
boolean snap = !(distance_squared(&previous[0].position, &latest[0].position) <=
|
||||
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE);
|
||||
|
||||
/* a node moved further in the root's frame than a tick allows: the
|
||||
two snapshots are different poses, not one moving (so written that
|
||||
a position not a number snaps) */
|
||||
for (node_index = 1; !snap && node_index < record->node_count; node_index++)
|
||||
if (!(distance_squared(&previous[0].position, &latest[0].position) <=
|
||||
OBJECT_SNAP_DISTANCE * OBJECT_SNAP_DISTANCE))
|
||||
{
|
||||
real_point3d previous_local, latest_local;
|
||||
|
||||
matrix4x3_inverse_transform_point(&previous[0], &previous[node_index].position, &previous_local);
|
||||
matrix4x3_inverse_transform_point(&latest[0], &latest[node_index].position, &latest_local);
|
||||
snap = !(distance_squared(&previous_local, &latest_local) <= NODE_SNAP_DISTANCE * NODE_SNAP_DISTANCE);
|
||||
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
|
||||
}
|
||||
if (!snap)
|
||||
else
|
||||
{
|
||||
struct interpolation_rotation *previous_rotations =
|
||||
record->rotations + (record->latest ^ 1) * record->node_capacity;
|
||||
@@ -578,8 +566,6 @@ real_matrix4x3 *render_interpolation_object_node_matrices(long object_index)
|
||||
&latest_rotations[node_index], interpolation_fraction, &blended[node_index]);
|
||||
}
|
||||
}
|
||||
if (snap)
|
||||
memcpy(blended, latest, record->node_count * sizeof(real_matrix4x3));
|
||||
if (correction_significant(&record->correction) || correction_significant(&record->correction_pending))
|
||||
{
|
||||
real_vector3d drawn;
|
||||
@@ -818,8 +804,6 @@ void render_interpolation_first_person(
|
||||
struct interpolated_first_person *first_person;
|
||||
real_matrix4x3 camera_matrix;
|
||||
real_matrix4x3 inverse_camera;
|
||||
struct interpolation_rotation previous_rotations[MAXIMUM_INTERPOLATED_NODES];
|
||||
struct interpolation_rotation latest_rotations[MAXIMUM_INTERPOLATED_NODES];
|
||||
short node_index;
|
||||
|
||||
if (!interpolation_rendering ||
|
||||
@@ -833,11 +817,8 @@ void render_interpolation_first_person(
|
||||
matrix4x3_inverse(&camera_matrix, &inverse_camera);
|
||||
if (first_person->tick != interpolation_tick)
|
||||
{
|
||||
/* the pose drawn last, if that was at the end of the tick just
|
||||
before: not one left from before the view was last away from first
|
||||
person (zoomed, in a vehicle, dead, in a cinematic), seconds old */
|
||||
first_person->has_previous = first_person->tick == interpolation_tick - 1 &&
|
||||
first_person->node_count == node_count;
|
||||
/* the pose drawn last, at the end of the previous tick */
|
||||
first_person->has_previous = first_person->node_count == node_count;
|
||||
memcpy(first_person->previous, first_person->latest, sizeof(first_person->previous));
|
||||
first_person->tick = interpolation_tick;
|
||||
}
|
||||
@@ -846,31 +827,13 @@ void render_interpolation_first_person(
|
||||
first_person->node_count = node_count;
|
||||
if (!first_person->has_previous)
|
||||
return;
|
||||
/* a node that jumped further in the camera's frame than a tick allows:
|
||||
the last pose was another weapon's skeleton (of as many nodes), not this
|
||||
one moving (so written that a position not a number snaps) */
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
real_matrix4x3 const *previous = &first_person->previous[node_index];
|
||||
real_matrix4x3 const *latest = &first_person->latest[node_index];
|
||||
|
||||
if (!(distance_squared(&previous->position, &latest->position) <=
|
||||
FIRST_PERSON_SNAP_DISTANCE * FIRST_PERSON_SNAP_DISTANCE))
|
||||
{
|
||||
return;
|
||||
}
|
||||
rotation_from_matrix(previous, &previous_rotations[node_index]);
|
||||
rotation_from_matrix(latest, &latest_rotations[node_index]);
|
||||
}
|
||||
for (node_index = 0; node_index < node_count; node_index++)
|
||||
{
|
||||
real_matrix4x3 blended;
|
||||
|
||||
matrix_blend_rotations(
|
||||
matrix_blend(
|
||||
&first_person->previous[node_index],
|
||||
&first_person->latest[node_index],
|
||||
&previous_rotations[node_index],
|
||||
&latest_rotations[node_index],
|
||||
interpolation_fraction,
|
||||
&blended);
|
||||
matrix4x3_multiply(&camera_matrix, &blended, &node_matrices[node_index]);
|
||||
|
||||
@@ -1,875 +0,0 @@
|
||||
/*
|
||||
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 */
|
||||
static 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 */
|
||||
static 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
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
|
||||
/* 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
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
VR_GRAPHICS.C
|
||||
|
||||
The VR graphics presets (vr.graphics, the VR options' "Graphics" row): which
|
||||
of the game's costlier effects are drawn, for headsets with little time to
|
||||
spare (the Steam Frame spends most of its frame in its GL driver: fewer
|
||||
draws help it most; the Quest 3 holds 90 Hz with all of them).
|
||||
|
||||
- "high", the default: everything, as before;
|
||||
- "medium": no object shadows, environment reflections or lens flares;
|
||||
- "low": nor specular light, bump maps, grass and other detail objects,
|
||||
the screen's fog layer, contrails, weather or camouflage's second pass;
|
||||
- "max": "high", the models lit for each pixel and the shadows drawn at
|
||||
512x512 (d3d8_gl.c asks halo_vr_graphics).
|
||||
|
||||
Each effect is a switch the game has already: the rasterizer's console
|
||||
variables (rasterizer_debug_options) and render.c's effect switches, after
|
||||
HaloCE-Quest-VR's vr_graphics.c (CC0). Scripts and the console set some of
|
||||
them (render_effects turns particles off in some cutscenes): a switch is
|
||||
drawn only while both the game and the preset want it. A change the game
|
||||
made since the last frame is taken as its wish, kept and combined with the
|
||||
preset's. Playing in another's game, a client draws grass, lens flares and
|
||||
the screen's fog as the host's rules have it (cheats.c), whatever the
|
||||
preset; the reductions it does keep there, cheats.c leaves alone
|
||||
(vr_graphics_reduces).
|
||||
|
||||
What each costs: shadows (each object's map, blur and projection: draws),
|
||||
specular light and reflections (passes of the level's geometry: draws and
|
||||
fill), detail objects (many small draws), lens flares (their visibility
|
||||
tests), contrails and weather (particles), bump maps, the fog layer and
|
||||
camouflage's pass (the GPU's fill).
|
||||
*/
|
||||
|
||||
#ifdef HALO_ANDROID
|
||||
|
||||
#include "cseries.h"
|
||||
#include "rasterizer/rasterizer_console_vars.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "halo_vr.h"
|
||||
|
||||
/* source/render/render.c, render_objects.c; source/effects/
|
||||
weather_particle_systems.c */
|
||||
extern boolean render_shadows;
|
||||
extern boolean render_contrails_enabled;
|
||||
extern boolean render_weather_particle_systems_enabled;
|
||||
extern boolean weather;
|
||||
|
||||
void platform_log(char const *format, ...);
|
||||
|
||||
/* an effect: whether each preset draws it (low, medium; high and max draw
|
||||
everything), whether a client in another's game draws it anyway, and the
|
||||
game's switches it governs */
|
||||
struct vr_graphics_effect
|
||||
{
|
||||
char const *name;
|
||||
boolean preset[2];
|
||||
boolean host_rules;
|
||||
boolean *switches[2];
|
||||
};
|
||||
|
||||
static struct vr_graphics_effect vr_graphics_effects[] =
|
||||
{
|
||||
{ "shadows", { FALSE, FALSE }, FALSE,
|
||||
{ &render_shadows, &rasterizer_debug_options.draw_environment_shadows } },
|
||||
{ "specular", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.draw_environment_specular_lights,
|
||||
&rasterizer_debug_options.draw_environment_specular_lightmaps } },
|
||||
{ "reflections", { FALSE, FALSE }, FALSE,
|
||||
{ &rasterizer_debug_options.draw_environment_reflections, NULL } },
|
||||
{ "bump maps", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.bump_mapping_enabled, NULL } },
|
||||
{ "detail objects", { FALSE, TRUE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_detail_objects, NULL } },
|
||||
{ "lens flares", { FALSE, FALSE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_lens_flares, NULL } },
|
||||
{ "screen fog", { FALSE, TRUE }, TRUE,
|
||||
{ &rasterizer_debug_options.draw_environment_fog_screen, NULL } },
|
||||
{ "contrails", { FALSE, TRUE }, FALSE,
|
||||
{ &render_contrails_enabled, NULL } },
|
||||
{ "weather", { FALSE, TRUE }, FALSE,
|
||||
{ &weather, &render_weather_particle_systems_enabled } },
|
||||
{ "camouflage pass", { FALSE, TRUE }, FALSE,
|
||||
{ &rasterizer_debug_options.active_camouflage_multipass_enabled, NULL } },
|
||||
};
|
||||
|
||||
#define NUMBER_OF_VR_GRAPHICS_EFFECTS (sizeof(vr_graphics_effects) / sizeof(vr_graphics_effects[0]))
|
||||
#define VR_GRAPHICS_SWITCHES (sizeof(vr_graphics_effects[0].switches) / sizeof(vr_graphics_effects[0].switches[0]))
|
||||
|
||||
static struct
|
||||
{
|
||||
boolean started;
|
||||
int preset;
|
||||
boolean client;
|
||||
/* whether the preset allows each effect; what the game wants of each
|
||||
switch, and what was written last frame */
|
||||
boolean allowed[NUMBER_OF_VR_GRAPHICS_EFFECTS];
|
||||
boolean wanted[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
|
||||
boolean written[NUMBER_OF_VR_GRAPHICS_EFFECTS][VR_GRAPHICS_SWITCHES];
|
||||
} vr_graphics;
|
||||
|
||||
static void vr_graphics_choose(
|
||||
int preset,
|
||||
boolean client)
|
||||
{
|
||||
static char const *const names[] = { "low", "medium", "high", "max" };
|
||||
unsigned int index;
|
||||
|
||||
vr_graphics.preset = preset;
|
||||
vr_graphics.client = client;
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
struct vr_graphics_effect const *effect = &vr_graphics_effects[index];
|
||||
|
||||
vr_graphics.allowed[index] = preset >= HALO_VR_GRAPHICS_HIGH || effect->preset[preset] ||
|
||||
(client && effect->host_rules);
|
||||
}
|
||||
platform_log("VR: graphics %s%s", names[preset], client ? " (in another's game)" : "");
|
||||
}
|
||||
|
||||
/* each frame, before it is drawn (render.c) */
|
||||
void vr_graphics_update(
|
||||
void)
|
||||
{
|
||||
int preset = halo_vr_graphics();
|
||||
boolean client = network_game_distributed_client();
|
||||
unsigned int index, which;
|
||||
|
||||
if (!vr_graphics.started)
|
||||
{
|
||||
/* the game's switches as it starts are its wishes */
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
vr_graphics.wanted[index][which] = *vr_graphics_effects[index].switches[which];
|
||||
vr_graphics.written[index][which] = *vr_graphics_effects[index].switches[which];
|
||||
}
|
||||
}
|
||||
vr_graphics.started = TRUE;
|
||||
vr_graphics_choose(preset, client);
|
||||
}
|
||||
else if (preset != vr_graphics.preset || client != vr_graphics.client)
|
||||
{
|
||||
vr_graphics_choose(preset, client);
|
||||
}
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
boolean *game_switch = vr_graphics_effects[index].switches[which];
|
||||
boolean value;
|
||||
|
||||
/* (the game, a script, the console changed it since: its wish) */
|
||||
if (*game_switch != vr_graphics.written[index][which])
|
||||
vr_graphics.wanted[index][which] = *game_switch;
|
||||
value = vr_graphics.wanted[index][which] && vr_graphics.allowed[index];
|
||||
*game_switch = value;
|
||||
vr_graphics.written[index][which] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* whether the preset keeps this switch of the game's off now (cheats.c, a
|
||||
client's rules: those it may keep off) */
|
||||
boolean vr_graphics_reduces(
|
||||
boolean const *game_switch)
|
||||
{
|
||||
unsigned int index, which;
|
||||
|
||||
if (!vr_graphics.started)
|
||||
return FALSE;
|
||||
for (index = 0; index < NUMBER_OF_VR_GRAPHICS_EFFECTS; index++)
|
||||
{
|
||||
for (which = 0; which < VR_GRAPHICS_SWITCHES && vr_graphics_effects[index].switches[which]; which++)
|
||||
{
|
||||
if (vr_graphics_effects[index].switches[which] == game_switch)
|
||||
return !vr_graphics.allowed[index];
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -41,17 +41,8 @@ the display's shape, or 640 (port/linux/src/d3d8_gl.c) */
|
||||
long halo_screen_width(void);
|
||||
/* takes up a new width between frames (F11); returns the width */
|
||||
long halo_screen_commit(void);
|
||||
/* the shadow maps' pixels for each of their 128 texels each way, a power of
|
||||
two (display.shadow_resolution) */
|
||||
long halo_shadow_map_scale(void);
|
||||
/* while TRUE, drawing shifts right to center 640-column layouts */
|
||||
void halo_screen_ui_offset(unsigned char centered);
|
||||
/* names a model lighting vertex program, whose draws can be lit for each
|
||||
pixel (port/linux/src/d3d8_gl.c, display.per_pixel_lighting) */
|
||||
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);
|
||||
/* the mouse in the menus (source/interface/ui_widget.c) */
|
||||
#include "halo_ui_pointer.h"
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
|
||||
in its game's advertisement's reserved bytes (network_server_message_handler.c),
|
||||
which hosts built before there was a version send as zeros: version 0.
|
||||
Raise it with any change to what the machines send each other. */
|
||||
#define HALO_PORT_NETWORK_VERSION 20
|
||||
#define HALO_PORT_NETWORK_VERSION 16
|
||||
/* ... the advertisement's reserved bytes: the version (a little-endian word),
|
||||
then flags */
|
||||
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
|
||||
|
||||
@@ -88,14 +88,13 @@ 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 weapon in the right hand (vr.aim_with = "controller"; each returns 0,
|
||||
and the game does as before, when the hand is not tracked):
|
||||
- the 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);
|
||||
in the hand;
|
||||
- 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]);
|
||||
@@ -105,75 +104,35 @@ int halo_vr_weapon_origin_offset(short local_player_index, float offset[3]);
|
||||
int halo_vr_weapon_origin(short local_player_index, float origin[3]);
|
||||
int halo_vr_weapon_camera(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
int halo_vr_laser(short local_player_index, float start[3], float direction[3]);
|
||||
/* Model space: the first-person model (first_person_weapons.c) is always
|
||||
posed as the right hand's. Left-handed, it is posed in a mirror image of
|
||||
the room and drawn mirrored back (its triangles' winding turned over); the
|
||||
mirror: a point on it (world units) and its normal, or 0 when it is not
|
||||
mirrored. What first_person_weapons.c and vr.c exchange about the model
|
||||
is in model space: the weapon camera, the free hand, the foregrip, the
|
||||
fingers (0 the model's left hand, the free one), the barrel and the laser
|
||||
along it. */
|
||||
int halo_vr_first_person_mirror(short local_player_index, float point[3], float normal[3]);
|
||||
/* the laser for this frame, along the barrel (first_person_weapons.c: while
|
||||
an animation turns the weapon, as in a reload; model space), and the laser
|
||||
at the ready, where the controller aims (in the room) */
|
||||
an animation turns the weapon, as in a reload), and the laser at the ready,
|
||||
where the controller aims */
|
||||
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
|
||||
- the body: the head's position (world units) and flat heading, from
|
||||
which the shoulders hang;
|
||||
- the left hand's pose;
|
||||
- the left hand on the weapon: given where the model's left hand holds it
|
||||
(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's forward, right and up, whether the left 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]);
|
||||
int halo_vr_body_frame(short local_player_index, float head[3], float forward[3]);
|
||||
int halo_vr_left_hand(short local_player_index, float position[3], float forward[3], float up[3]);
|
||||
int halo_vr_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) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_BODY_ARMS = 0,
|
||||
HALO_VR_BODY_HANDS,
|
||||
HALO_VR_BODY_OFF,
|
||||
};
|
||||
int halo_vr_body_mode(void);
|
||||
/* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */
|
||||
int halo_vr_melee(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
|
||||
/* The grenade (units.c): whether the left 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 */
|
||||
/* where the held grenade is drawn (render_objects.c): in the left 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
|
||||
@@ -204,9 +163,9 @@ void halo_vr_capture_names(void);
|
||||
void halo_vr_set_scope_raise(float metres);
|
||||
void halo_vr_capture_scope(float left, float top, float right, float bottom);
|
||||
/* where the weapon's gun node is as drawn, at the ready (world units;
|
||||
first_person_weapons.c, each frame; 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 */
|
||||
first_person_weapons.c, each frame): shots, the crosshair and the laser
|
||||
start there; and the direction its barrel points in the weapon camera's
|
||||
frame (forward, left, up), which the camera is turned to aim */
|
||||
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 */
|
||||
@@ -222,8 +181,7 @@ 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
|
||||
right stick, else the right controller aims (HALO_VR_SEAT_AIM). And each
|
||||
frame (render.c) the heading as drawn, between ticks. */
|
||||
enum
|
||||
{
|
||||
@@ -249,59 +207,6 @@ int halo_vr_wheel_held(void);
|
||||
/* the controllers' layout (HALO_XR_LAYOUT_*: 0 the Quest's, 1 the Steam
|
||||
Frame's), for what the game tells the player */
|
||||
int halo_vr_controller_layout(void);
|
||||
/* the hand that holds the weapon (HALO_XR_HAND_*: 1 the right, 0 the left,
|
||||
vr.handedness) */
|
||||
int halo_vr_weapon_hand(void);
|
||||
/* The game's actions on the controllers' buttons (vr.button_*; the VR
|
||||
options' BUTTONS page): each action on one of seven buttons, named as
|
||||
right-handed (A and B on the right controller, X and Y, the Quest's, on
|
||||
the left; the move stick's click, the turning stick's, the weapon hand's
|
||||
grip); left-handed with vr.mirror_controls, their mirror images. The
|
||||
actions are numbered by the button each has by default. The triggers, the
|
||||
free hand's grip, the menu button and the Steam Frame's bumpers, D-pad and
|
||||
View button keep theirs; the menus keep the buttons as they are. */
|
||||
enum
|
||||
{
|
||||
HALO_VR_INPUT_A = 0,
|
||||
HALO_VR_INPUT_B,
|
||||
HALO_VR_INPUT_X,
|
||||
HALO_VR_INPUT_Y,
|
||||
HALO_VR_INPUT_MOVE_STICK,
|
||||
HALO_VR_INPUT_TURN_STICK,
|
||||
HALO_VR_INPUT_GRIP,
|
||||
HALO_VR_INPUT_COUNT,
|
||||
|
||||
HALO_VR_ACTION_JUMP = HALO_VR_INPUT_A,
|
||||
HALO_VR_ACTION_LIGHT = HALO_VR_INPUT_B, /* a tap the flashlight; held, switch grenades */
|
||||
HALO_VR_ACTION_ACTION = HALO_VR_INPUT_X, /* action, reload */
|
||||
HALO_VR_ACTION_WEAPON = HALO_VR_INPUT_Y, /* switch weapon */
|
||||
HALO_VR_ACTION_CROUCH = HALO_VR_INPUT_MOVE_STICK,
|
||||
HALO_VR_ACTION_ZOOM = HALO_VR_INPUT_TURN_STICK,
|
||||
HALO_VR_ACTION_MELEE = HALO_VR_INPUT_GRIP,
|
||||
HALO_VR_ACTION_COUNT = HALO_VR_INPUT_COUNT,
|
||||
};
|
||||
/* an action's setting ("vr.button_jump"), a button's value in it ("a"),
|
||||
and the button an action is on, as set */
|
||||
const char *halo_vr_action_setting(int action);
|
||||
const char *halo_vr_input_value(int input);
|
||||
int halo_vr_action_input(int action);
|
||||
/* the graphics preset (vr.graphics; HALO_VR_GRAPHICS_HIGH outside VR): which
|
||||
effects are drawn (port/linux/game/vr_graphics.c), and with "max" the
|
||||
models lit for each pixel and larger shadows (d3d8_gl.c) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_GRAPHICS_LOW = 0,
|
||||
HALO_VR_GRAPHICS_MEDIUM,
|
||||
HALO_VR_GRAPHICS_HIGH,
|
||||
HALO_VR_GRAPHICS_MAX,
|
||||
};
|
||||
int halo_vr_graphics(void);
|
||||
/* the VR options: the refresh rates the headset offers (Hz, at most maximum;
|
||||
0 when it lets none be chosen, as SteamVR), and the one it runs at (0:
|
||||
unknown; vr.refresh_rate asks for one); and vr.resolution_scale as the app
|
||||
started (it takes a restart) */
|
||||
int halo_vr_refresh_rates(float *rates, int maximum, float *current);
|
||||
float halo_vr_started_resolution_scale(void);
|
||||
/* whether a seated player's view stays in first person (director.c) */
|
||||
int halo_vr_vehicle_first_person(void);
|
||||
/* VR test aid (debug.vehicle_spawn): both sticks held in for a moment asks
|
||||
@@ -331,7 +236,7 @@ coming (each returns 0 otherwise, and for this machine's own players, and
|
||||
the game does as before):
|
||||
- where the weapon aims, on foot;
|
||||
- where its shots start (world units), from the unit's camera;
|
||||
- whether the free hand holds the grenade;
|
||||
- whether the left 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]);
|
||||
@@ -357,23 +262,6 @@ enum
|
||||
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, and the
|
||||
pose taken as built already this frame (counted) */
|
||||
enum
|
||||
{
|
||||
HALO_VR_TIMING_FIRST_PERSON,
|
||||
HALO_VR_TIMING_ARMS,
|
||||
HALO_VR_TIMING_FIRST_PERSON_KEPT,
|
||||
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);
|
||||
|
||||
@@ -86,10 +86,6 @@ this list to generate the guest's entry points */
|
||||
X(glCheckFramebufferStatus) \
|
||||
X(glBlitFramebuffer) \
|
||||
X(glDrawBuffers) \
|
||||
X(glGenRenderbuffers) \
|
||||
X(glBindRenderbuffer) \
|
||||
X(glRenderbufferStorageMultisample) \
|
||||
X(glFramebufferRenderbuffer) \
|
||||
X(glReadBuffer) \
|
||||
X(glInvalidateFramebuffer) \
|
||||
X(glGenBuffers) \
|
||||
@@ -199,10 +195,6 @@ this list to generate the guest's entry points */
|
||||
X(glCheckFramebufferStatus) \
|
||||
X(glBlitFramebuffer) \
|
||||
X(glDrawBuffers) \
|
||||
X(glGenRenderbuffers) \
|
||||
X(glBindRenderbuffer) \
|
||||
X(glRenderbufferStorageMultisample) \
|
||||
X(glFramebufferRenderbuffer) \
|
||||
X(glGenBuffers) \
|
||||
X(glDeleteBuffers) \
|
||||
X(glBindBuffer) \
|
||||
@@ -217,11 +209,6 @@ this list to generate the guest's entry points */
|
||||
X(glDisableVertexAttribArray) \
|
||||
X(glVertexAttribPointer) \
|
||||
X(glVertexAttribIPointer) \
|
||||
X(glVertexAttribFormat) \
|
||||
X(glVertexAttribIFormat) \
|
||||
X(glVertexAttribBinding) \
|
||||
X(glBindVertexBuffer) \
|
||||
X(glGetQueryBufferObjectuiv) \
|
||||
X(glVertexAttrib4fv) \
|
||||
X(glVertexAttribI4ui) \
|
||||
X(glDrawArrays) \
|
||||
@@ -319,10 +306,6 @@ pointers, sees the declarations without these aliases */
|
||||
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
|
||||
#define glBlitFramebuffer halo_glBlitFramebuffer
|
||||
#define glDrawBuffers halo_glDrawBuffers
|
||||
#define glGenRenderbuffers halo_glGenRenderbuffers
|
||||
#define glBindRenderbuffer halo_glBindRenderbuffer
|
||||
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
|
||||
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
|
||||
#define glReadBuffer halo_glReadBuffer
|
||||
#define glInvalidateFramebuffer halo_glInvalidateFramebuffer
|
||||
#define glGenBuffers halo_glGenBuffers
|
||||
@@ -430,10 +413,6 @@ pointers, sees the declarations without these aliases */
|
||||
#define glCheckFramebufferStatus halo_glCheckFramebufferStatus
|
||||
#define glBlitFramebuffer halo_glBlitFramebuffer
|
||||
#define glDrawBuffers halo_glDrawBuffers
|
||||
#define glGenRenderbuffers halo_glGenRenderbuffers
|
||||
#define glBindRenderbuffer halo_glBindRenderbuffer
|
||||
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
|
||||
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
|
||||
#define glGenBuffers halo_glGenBuffers
|
||||
#define glDeleteBuffers halo_glDeleteBuffers
|
||||
#define glBindBuffer halo_glBindBuffer
|
||||
@@ -448,11 +427,6 @@ pointers, sees the declarations without these aliases */
|
||||
#define glDisableVertexAttribArray halo_glDisableVertexAttribArray
|
||||
#define glVertexAttribPointer halo_glVertexAttribPointer
|
||||
#define glVertexAttribIPointer halo_glVertexAttribIPointer
|
||||
#define glVertexAttribFormat halo_glVertexAttribFormat
|
||||
#define glVertexAttribIFormat halo_glVertexAttribIFormat
|
||||
#define glVertexAttribBinding halo_glVertexAttribBinding
|
||||
#define glBindVertexBuffer halo_glBindVertexBuffer
|
||||
#define glGetQueryBufferObjectuiv halo_glGetQueryBufferObjectuiv
|
||||
#define glVertexAttrib4fv halo_glVertexAttrib4fv
|
||||
#define glVertexAttribI4ui halo_glVertexAttribI4ui
|
||||
#define glDrawArrays halo_glDrawArrays
|
||||
|
||||
@@ -125,9 +125,7 @@ HALO_COMMON(hs_global_data, 4); /* struct data_array *hs_global_data */
|
||||
HALO_COMMON(hs_thread_data, 4); /* struct data_array *hs_thread_data */
|
||||
HALO_COMMON(hud_msg_def, 4); /* struct hud_messaging_parameters_definition *hud_msg_def */
|
||||
HALO_COMMON(interrupt_result, 24); /* HRESULT interrupt_result */
|
||||
/* object_lights.c owns light_cluster_partition. On COFF a weak fallback
|
||||
can override its tentative COMMON definition; let the owner allocate the
|
||||
native struct cluster_partition, including its reference-cache fields. */
|
||||
HALO_COMMON(light_cluster_partition, 12); /* struct cluster_partition light_cluster_partition */
|
||||
HALO_COMMON(light_data, 4); /* struct data_array *light_data */
|
||||
HALO_COMMON(local_player_index_for_draw_string_and_hack_in_icons, 4); /* short local_player_index_for_draw_string_and_hack_in_icons */
|
||||
HALO_COMMON(looping_sound_data, 4); /* struct data_array *looping_sound_data */
|
||||
|
||||
@@ -518,59 +518,6 @@ static const char *comparison_operator(unsigned long function)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- per-pixel model lighting
|
||||
|
||||
The game lights its models for each vertex: its model lighting programs
|
||||
(nv2a_vsh.c nv2a_vertex_shader_lighting) sum the ambient light, two distant
|
||||
lights and two point lights into the diffuse color v0. Halo's models have
|
||||
few vertices, so the light across a curved surface comes out in facets, and
|
||||
a point light that passes close lights only the vertices it reaches.
|
||||
model_lighting computes the same sum for each pixel, from the normal and
|
||||
world position the vertex program hands on and the same constants
|
||||
(XGPU_MODEL_LIGHT_COUNT): the same falloff, facing, cone and translucency
|
||||
terms, clamped where the programs clamp them. The diffuse alpha, which the
|
||||
programs write apart (0, or planar fog's), is kept. */
|
||||
|
||||
static void model_lighting(struct xgpu_text *text, BOOL point_lights)
|
||||
{
|
||||
int light;
|
||||
|
||||
xgpu_text_append(text,
|
||||
"uniform vec4 model_lights[%d];\n"
|
||||
"in vec4 xWorldNormal;\n"
|
||||
"%s"
|
||||
"vec3 model_lighting()\n"
|
||||
"{\n"
|
||||
/* the normal's direction between the vertices, at the length the
|
||||
vertices' normals have, as the programs light by it unnormalized
|
||||
(shorter than 1 on some models: the first-person weapons) */
|
||||
"\tvec3 n = xWorldNormal.xyz * (xWorldNormal.w * inversesqrt(max(dot(xWorldNormal.xyz, xWorldNormal.xyz), 1.0e-24)));\n"
|
||||
/* the ambient light, the first distant light, which also lights the
|
||||
back by the translucency, and the second */
|
||||
"\tfloat facing = dot(n, -model_lights[7].xyz);\n"
|
||||
"\tvec3 light = model_lights[11].xyz + max(max(facing, -facing * model_lights[0].z), 0.0) * model_lights[8].xyz +\n"
|
||||
"\t\tmax(dot(n, -model_lights[9].xyz), 0.0) * model_lights[10].xyz;\n",
|
||||
XGPU_MODEL_LIGHT_COUNT, point_lights ? "in vec3 xWorldPosition;\n" : "");
|
||||
/* each point light: its position and 1 / radius squared, its cone's axis
|
||||
and falloff scale, its color and falloff offset */
|
||||
for (light = 0; point_lights && light < 2; light++)
|
||||
{
|
||||
int first = 1 + light * 3;
|
||||
|
||||
xgpu_text_append(text,
|
||||
"\t{\n"
|
||||
"\t\tvec3 to_light = model_lights[%d].xyz - xWorldPosition;\n"
|
||||
"\t\tfloat distance_squared = dot(to_light, to_light);\n"
|
||||
"\t\tvec3 l = to_light * inversesqrt(max(distance_squared, 1.0e-12));\n"
|
||||
"\t\tlight += max(1.0 - distance_squared * model_lights[%d].w, 0.0) * clamp(dot(l, n), 0.0, 1.0) *\n"
|
||||
"\t\t\tclamp(dot(l, -model_lights[%d].xyz) * model_lights[%d].w + model_lights[%d].w, 0.0, 1.0) *\n"
|
||||
"\t\t\tmodel_lights[%d].xyz;\n"
|
||||
"\t}\n",
|
||||
first, first, first + 1, first + 1, first + 2, first + 2);
|
||||
}
|
||||
xgpu_text_append(text, "\treturn clamp(light, 0.0, 1.0);\n}\n");
|
||||
}
|
||||
|
||||
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
{
|
||||
const DWORD *state = key->combiner_state;
|
||||
@@ -609,8 +556,6 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
XGPU_PIXEL_UNIFORMS);
|
||||
for (stage = 0; stage < 4; stage++)
|
||||
xgpu_text_append(&text, "uniform %s tex%d;\n", sampler_declaration(key->sampler_type[stage]), stage);
|
||||
if (key->per_pixel_lighting)
|
||||
model_lighting(&text, key->per_pixel_lighting == 2);
|
||||
xgpu_text_append(&text,
|
||||
"float signed_byte(float x)\n"
|
||||
"{\n"
|
||||
@@ -627,8 +572,6 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
"\tvec4 v1 = xD1;\n"
|
||||
"\tvec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0), t3 = vec4(0.0);\n"
|
||||
"\tfloat dot0 = 0.0, dot1 = 0.0, dot2 = 0.0, dot3 = 0.0;\n");
|
||||
if (key->per_pixel_lighting)
|
||||
xgpu_text_append(&text, "\tv0.rgb = model_lighting();\n");
|
||||
|
||||
for (stage = 0; stage < 4; stage++)
|
||||
texture_stage(&text, key, stage);
|
||||
@@ -702,27 +645,9 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
|
||||
const char *comparison = comparison_operator(key->alpha_test_function);
|
||||
|
||||
if (!comparison)
|
||||
{
|
||||
xgpu_text_append(&text, "\tdiscard;\n");
|
||||
}
|
||||
else if (*comparison && key->alpha_test_samples && comparison[0] != '=' && comparison[0] != '!')
|
||||
{
|
||||
/* multisampled: the samples covered as alpha passes the reference
|
||||
over the pixel (its change across the pixel from fwidth), the rest
|
||||
left as they are; alpha itself is kept, the game keeping values of
|
||||
its own in destination alpha */
|
||||
xgpu_text_append(&text,
|
||||
"\tfloat test_alpha = clamp(result.a, 0.0, 1.0) * 255.0;\n"
|
||||
"\tfloat test_coverage = clamp(%s(test_alpha - alpha_reference) / max(fwidth(test_alpha), 1.0) + 0.5, 0.0, 1.0);\n"
|
||||
"\tint test_samples = int(test_coverage * %d.0 + 0.5);\n"
|
||||
"\tif (test_samples == 0) discard;\n"
|
||||
"\tgl_SampleMask[0] = (1 << test_samples) - 1;\n",
|
||||
comparison[0] == '<' ? "-" : "", (int)key->alpha_test_samples);
|
||||
}
|
||||
else if (*comparison)
|
||||
{
|
||||
xgpu_text_append(&text, "\tif (!(floor(clamp(result.a, 0.0, 1.0) * 255.0 + 0.5) %s alpha_reference)) discard;\n", comparison);
|
||||
}
|
||||
}
|
||||
if (*config_string("debug.gpu_debug_expression"))
|
||||
xgpu_text_append(&text, "\tresult = vec4(vec3(%s), 1.0);\n", config_string("debug.gpu_debug_expression"));
|
||||
|
||||
@@ -7,7 +7,7 @@ GLSL.
|
||||
Each instruction is four little-endian words. Word 1 to 3 hold a MAC
|
||||
(vector) operation and an ILU (scalar) operation that execute in parallel
|
||||
on the same three operands A, B and C, and the destinations of both; the
|
||||
bit positions of every field are spelled out in operand_fields() and in
|
||||
bit positions of every field are spelled out in operand() and in
|
||||
nv2a_vertex_shader_to_glsl(). When both units run, the ILU result goes to
|
||||
temporary r1, whatever the instruction's temporary register field says.
|
||||
|
||||
@@ -76,55 +76,43 @@ static void write_mask(unsigned long mask, char *out)
|
||||
out[count] = 0;
|
||||
}
|
||||
|
||||
struct operand_fields
|
||||
{
|
||||
unsigned long negate, swizzle[4], index, mux;
|
||||
};
|
||||
|
||||
static void operand_fields(const DWORD *instruction, char which, struct operand_fields *fields)
|
||||
{
|
||||
switch (which)
|
||||
{
|
||||
case 'A':
|
||||
fields->negate = field(instruction, 1, 8, 1);
|
||||
fields->swizzle[0] = field(instruction, 1, 6, 2);
|
||||
fields->swizzle[1] = field(instruction, 1, 4, 2);
|
||||
fields->swizzle[2] = field(instruction, 1, 2, 2);
|
||||
fields->swizzle[3] = field(instruction, 1, 0, 2);
|
||||
fields->index = field(instruction, 2, 28, 4);
|
||||
fields->mux = field(instruction, 2, 26, 2);
|
||||
break;
|
||||
case 'B':
|
||||
fields->negate = field(instruction, 2, 25, 1);
|
||||
fields->swizzle[0] = field(instruction, 2, 23, 2);
|
||||
fields->swizzle[1] = field(instruction, 2, 21, 2);
|
||||
fields->swizzle[2] = field(instruction, 2, 19, 2);
|
||||
fields->swizzle[3] = field(instruction, 2, 17, 2);
|
||||
fields->index = field(instruction, 2, 13, 4);
|
||||
fields->mux = field(instruction, 2, 11, 2);
|
||||
break;
|
||||
default:
|
||||
fields->negate = field(instruction, 2, 10, 1);
|
||||
fields->swizzle[0] = field(instruction, 2, 8, 2);
|
||||
fields->swizzle[1] = field(instruction, 2, 6, 2);
|
||||
fields->swizzle[2] = field(instruction, 2, 4, 2);
|
||||
fields->swizzle[3] = field(instruction, 2, 2, 2);
|
||||
fields->index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
|
||||
fields->mux = field(instruction, 3, 28, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void operand(struct xgpu_text *text, const DWORD *instruction, char which, int relative)
|
||||
{
|
||||
static const char swizzle_names[] = "xyzw";
|
||||
struct operand_fields fields;
|
||||
unsigned long *swizzle = fields.swizzle, index, mux;
|
||||
unsigned long negate, swizzle[4], index, mux;
|
||||
|
||||
operand_fields(instruction, which, &fields);
|
||||
index = fields.index;
|
||||
mux = fields.mux;
|
||||
xgpu_text_append(text, "%s", fields.negate ? "-" : "");
|
||||
switch (which)
|
||||
{
|
||||
case 'A':
|
||||
negate = field(instruction, 1, 8, 1);
|
||||
swizzle[0] = field(instruction, 1, 6, 2);
|
||||
swizzle[1] = field(instruction, 1, 4, 2);
|
||||
swizzle[2] = field(instruction, 1, 2, 2);
|
||||
swizzle[3] = field(instruction, 1, 0, 2);
|
||||
index = field(instruction, 2, 28, 4);
|
||||
mux = field(instruction, 2, 26, 2);
|
||||
break;
|
||||
case 'B':
|
||||
negate = field(instruction, 2, 25, 1);
|
||||
swizzle[0] = field(instruction, 2, 23, 2);
|
||||
swizzle[1] = field(instruction, 2, 21, 2);
|
||||
swizzle[2] = field(instruction, 2, 19, 2);
|
||||
swizzle[3] = field(instruction, 2, 17, 2);
|
||||
index = field(instruction, 2, 13, 4);
|
||||
mux = field(instruction, 2, 11, 2);
|
||||
break;
|
||||
default:
|
||||
negate = field(instruction, 2, 10, 1);
|
||||
swizzle[0] = field(instruction, 2, 8, 2);
|
||||
swizzle[1] = field(instruction, 2, 6, 2);
|
||||
swizzle[2] = field(instruction, 2, 4, 2);
|
||||
swizzle[3] = field(instruction, 2, 2, 2);
|
||||
index = (field(instruction, 2, 0, 2) << 2) | field(instruction, 3, 30, 2);
|
||||
mux = field(instruction, 3, 28, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
xgpu_text_append(text, "%s", negate ? "-" : "");
|
||||
switch (mux)
|
||||
{
|
||||
case _mux_temporary:
|
||||
@@ -151,183 +139,6 @@ static void operand(struct xgpu_text *text, const DWORD *instruction, char which
|
||||
swizzle_names[swizzle[0]], swizzle_names[swizzle[1]], swizzle_names[swizzle[2]], swizzle_names[swizzle[3]]);
|
||||
}
|
||||
|
||||
/* ---------- model lighting
|
||||
|
||||
The game's model lighting programs (rasterizer_xbox_vertex_shaders_data.inc
|
||||
9, 10, 17 and 27) sum into the diffuse color oD0.xyz the ambient light
|
||||
c[-69], two distant lights (direction c[-73] and c[-71], color c[-72] and
|
||||
c[-70]; the first also lights the back by the translucency c[-82].z) and, in
|
||||
10 and 17, two point lights (c[-79] to c[-77] and c[-76] to c[-74]: position
|
||||
and 1 / radius squared, the cone's axis and falloff scale, the color and
|
||||
falloff offset). The normal and world position they light by are
|
||||
temporaries that the programs reuse (10 and 17 overwrite both after the
|
||||
sum). In 9, 10 and 17 the normal is the skinned normal turned by c[-84].w
|
||||
for the back faces' pass, r0 (written by instruction 17) and the position
|
||||
the skinned position, r10 (by 12); in 27, of a single node, the normal is
|
||||
r3 (by 5). They are taken as the first instruction that lights by them
|
||||
reads them: in 9 the normal before instruction 19, in 10 and 17 the normal
|
||||
before 24 and the position before 19, in 27 the normal before 7. A program
|
||||
is taken only where its lighting is the sum nv2a_psh.c computes for each
|
||||
pixel (the diffuse color written once, by the sum; both distant lights by
|
||||
one normal; the translucency; each point light by one position, its cone
|
||||
by its axis reversed) and the registers keep the same normal and position
|
||||
through it. Nothing else of the programs goes into oD0.xyz; oD0.w is 0, or
|
||||
in 17 the planar fog's. */
|
||||
|
||||
static BOOL xyz_unswizzled(const struct operand_fields *operand)
|
||||
{
|
||||
return operand->swizzle[0] == 0 && operand->swizzle[1] == 1 && operand->swizzle[2] == 2;
|
||||
}
|
||||
|
||||
/* the operand is c[reg] (as the game numbers them: -73), its xyz unswizzled */
|
||||
static BOOL light_constant(const DWORD *instruction, const struct operand_fields *operand, int reg, BOOL negated)
|
||||
{
|
||||
return operand->mux == _mux_constant && !field(instruction, 3, 1, 1) &&
|
||||
field(instruction, 1, 13, 8) == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + reg) &&
|
||||
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
|
||||
}
|
||||
|
||||
/* the operand is a temporary (not the position output), its xyz unswizzled */
|
||||
static BOOL light_temporary(const struct operand_fields *operand, BOOL negated)
|
||||
{
|
||||
return operand->mux == _mux_temporary && operand->index < 12 &&
|
||||
operand->negate == (negated ? 1UL : 0UL) && xyz_unswizzled(operand);
|
||||
}
|
||||
|
||||
/* any of x, y and z of temporary reg written by the instructions in [first, last) */
|
||||
static BOOL temporary_written(const DWORD *instructions, unsigned long first, unsigned long last, unsigned long reg)
|
||||
{
|
||||
unsigned long index;
|
||||
|
||||
for (index = first; index < last; index++)
|
||||
{
|
||||
const DWORD *instruction = instructions + index * 4;
|
||||
unsigned long mac = field(instruction, 1, 21, 4);
|
||||
unsigned long ilu = field(instruction, 1, 25, 3);
|
||||
unsigned long temporary = field(instruction, 3, 20, 4);
|
||||
|
||||
if (mac != _mac_nop && mac != _mac_arl && temporary == reg && (field(instruction, 3, 24, 4) & 0xe))
|
||||
return TRUE;
|
||||
/* (the ILU writes r1 when both units run) */
|
||||
if (ilu != _ilu_nop && (mac != _mac_nop ? 1 : temporary) == reg && (field(instruction, 3, 16, 4) & 0xe))
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
|
||||
struct nv2a_vertex_lighting *lighting)
|
||||
{
|
||||
unsigned long count, index, diffuse;
|
||||
unsigned long distant[2], distant_register[2] = { 0, 0 }, point[2], point_register[2] = { 0, 0 };
|
||||
BOOL cone[2] = { FALSE, FALSE }, translucency = FALSE, point_lights = FALSE;
|
||||
int light;
|
||||
|
||||
/* to the instruction that ends the program */
|
||||
for (count = 0; count < instruction_count;)
|
||||
{
|
||||
if (field(instructions + count++ * 4, 3, 0, 1))
|
||||
break;
|
||||
}
|
||||
diffuse = distant[0] = distant[1] = point[0] = point[1] = count;
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
const DWORD *instruction = instructions + index * 4;
|
||||
unsigned long mac = field(instruction, 1, 21, 4);
|
||||
unsigned long output_mask = field(instruction, 3, 12, 4);
|
||||
unsigned long constant = field(instruction, 1, 13, 8);
|
||||
struct operand_fields a, b, c;
|
||||
|
||||
operand_fields(instruction, 'A', &a);
|
||||
operand_fields(instruction, 'B', &b);
|
||||
operand_fields(instruction, 'C', &c);
|
||||
/* the diffuse color's x, y and z, written once and last by the sum:
|
||||
the second distant light's term times its color, plus the rest */
|
||||
if ((output_mask & 0xe) && field(instruction, 3, 11, 1) && field(instruction, 3, 3, 8) == 3)
|
||||
{
|
||||
if (diffuse != count || (output_mask & 0xe) != 0xe || field(instruction, 3, 2, 1) ||
|
||||
mac != _mac_mad || !light_constant(instruction, &b, -70, FALSE) || c.mux != _mux_temporary)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
diffuse = index;
|
||||
}
|
||||
for (light = 0; light < 2; light++)
|
||||
{
|
||||
/* each distant light's facing: the normal's dot product with the
|
||||
light's direction reversed */
|
||||
if (mac == _mac_dp3 && light_constant(instruction, &b, light ? -71 : -73, TRUE))
|
||||
{
|
||||
if (distant[light] != count || !light_temporary(&a, FALSE))
|
||||
return FALSE;
|
||||
distant[light] = index;
|
||||
distant_register[light] = a.index;
|
||||
}
|
||||
/* each point light's direction, from the world position */
|
||||
if (mac == _mac_add && light_constant(instruction, &a, light ? -76 : -79, FALSE) &&
|
||||
light_temporary(&c, TRUE))
|
||||
{
|
||||
if (point[light] != count)
|
||||
return FALSE;
|
||||
point[light] = index;
|
||||
point_register[light] = c.index;
|
||||
}
|
||||
/* and its cone: the direction's dot product with the light's axis
|
||||
reversed */
|
||||
if (mac == _mac_dp3 && b.mux == _mux_constant && !field(instruction, 3, 1, 1) &&
|
||||
constant == (unsigned long)(XGPU_VERTEX_CONSTANT_BIAS + (light ? -75 : -78)))
|
||||
{
|
||||
if (!light_constant(instruction, &b, light ? -75 : -78, TRUE))
|
||||
return FALSE;
|
||||
cone[light] = TRUE;
|
||||
}
|
||||
}
|
||||
/* the back lit by the first distant light: its facing reversed, times
|
||||
the translucency */
|
||||
if (mac == _mac_mul && a.mux == _mux_temporary && a.negate && b.mux == _mux_constant &&
|
||||
!field(instruction, 3, 1, 1) && constant == XGPU_VERTEX_CONSTANT_BIAS - 82 && b.swizzle[0] == 2)
|
||||
{
|
||||
translucency = TRUE;
|
||||
}
|
||||
if ((a.mux == _mux_constant || b.mux == _mux_constant || c.mux == _mux_constant) &&
|
||||
!field(instruction, 3, 1, 1) && constant >= XGPU_VERTEX_CONSTANT_BIAS - 79 &&
|
||||
constant <= XGPU_VERTEX_CONSTANT_BIAS - 74)
|
||||
{
|
||||
point_lights = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if (diffuse == count || distant[0] >= diffuse || distant[1] >= diffuse ||
|
||||
distant_register[0] != distant_register[1] || !translucency)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
lighting->lights = 1;
|
||||
lighting->normal_register = distant_register[0];
|
||||
lighting->normal_instruction = distant[0] < distant[1] ? distant[0] : distant[1];
|
||||
if (temporary_written(instructions, lighting->normal_instruction, diffuse, lighting->normal_register))
|
||||
return FALSE;
|
||||
lighting->position_instruction = lighting->position_register = 0;
|
||||
if (point_lights)
|
||||
{
|
||||
unsigned long last = point[0] > point[1] ? point[0] : point[1];
|
||||
|
||||
if (point[0] >= diffuse || point[1] >= diffuse || point_register[0] != point_register[1] ||
|
||||
!cone[0] || !cone[1])
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
lighting->lights = 2;
|
||||
lighting->position_register = point_register[0];
|
||||
lighting->position_instruction = point[0] < point[1] ? point[0] : point[1];
|
||||
if (temporary_written(instructions, lighting->position_instruction, last, lighting->position_register))
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- translation */
|
||||
|
||||
static const char shader_prologue[] =
|
||||
#ifdef HALO_ANDROID
|
||||
/* the #version line comes first, from the context's capabilities */
|
||||
@@ -384,7 +195,7 @@ 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, int multiview)
|
||||
unsigned long packed_attribute_mask, int multiview)
|
||||
{
|
||||
struct xgpu_text text = { 0 };
|
||||
unsigned long index;
|
||||
@@ -410,11 +221,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
(void)multiview;
|
||||
#endif
|
||||
xgpu_text_append(&text, "%s", shader_prologue);
|
||||
/* (the pixel shader's model_lighting; the normal's length in w) */
|
||||
if (lighting)
|
||||
xgpu_text_append(&text, "out vec4 xWorldNormal;\n");
|
||||
if (lighting && lighting->lights == 2)
|
||||
xgpu_text_append(&text, "out vec3 xWorldPosition;\n");
|
||||
for (index = 0; index < XGPU_VERTEX_ATTRIBUTE_COUNT; index++)
|
||||
{
|
||||
if (packed_attribute_mask & (1UL << index))
|
||||
@@ -461,14 +267,6 @@ char *nv2a_vertex_shader_to_glsl(const DWORD *instructions, unsigned long instru
|
||||
char mask[5];
|
||||
|
||||
xgpu_text_append(&text, "\t/* %lu */\n", index);
|
||||
/* the lighting's normal and position, before the program reuses them */
|
||||
if (lighting && index == lighting->normal_instruction)
|
||||
{
|
||||
xgpu_text_append(&text, "\txWorldNormal = vec4(r%lu.xyz, length(r%lu.xyz));\n",
|
||||
lighting->normal_register, lighting->normal_register);
|
||||
}
|
||||
if (lighting && lighting->lights == 2 && index == lighting->position_instruction)
|
||||
xgpu_text_append(&text, "\txWorldPosition = r%lu.xyz;\n", lighting->position_register);
|
||||
xgpu_text_append(&text, "\tA = "); operand(&text, instruction, 'A', relative); xgpu_text_append(&text, ";\n");
|
||||
xgpu_text_append(&text, "\tB = "); operand(&text, instruction, 'B', relative); xgpu_text_append(&text, ";\n");
|
||||
xgpu_text_append(&text, "\tC = "); operand(&text, instruction, 'C', relative); xgpu_text_append(&text, ";\n");
|
||||
|
||||
@@ -2421,13 +2421,7 @@ int p2p_public_games(struct p2p_public_game *games, int maximum)
|
||||
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++];
|
||||
struct p2p_public_game *game = &games[count++];
|
||||
|
||||
memset(game, 0, sizeof(*game));
|
||||
snprintf(game->name, sizeof(game->name), "%s", listings[index].name);
|
||||
@@ -2539,8 +2533,7 @@ void p2p_new_invite_if_listed(void)
|
||||
if (!p2p.has_token || !p2p.token_listed)
|
||||
return;
|
||||
make_invite();
|
||||
platform_log("Internet play: a new invite, so that the one listed lets no one in (private, or a new password): %s",
|
||||
p2p.invite);
|
||||
platform_log("Internet play: the game is private now, with a new invite: %s", p2p.invite);
|
||||
if (p2p.hosting)
|
||||
{
|
||||
p2p_signal_host(p2p.token);
|
||||
|
||||
@@ -149,12 +149,6 @@ in everyone's server browser: Create Game's PUBLIC; a private game is
|
||||
reached only by its invite. Going private makes a new invite, if the old one
|
||||
was listed */
|
||||
void p2p_set_hosting_public(int public);
|
||||
/* the password of the game hosted for the internet (Server Setup's
|
||||
PASSWORD; NULL or empty: none): its listing lets only those who know it
|
||||
join from the server browser (its invite link still joins it). Setting or
|
||||
changing it makes a new invite, if the old one was listed. It takes a few
|
||||
milliseconds (the password's key) */
|
||||
void p2p_set_hosting_password(const char *password);
|
||||
/* the hosted game's details as listed (printable ASCII is kept; NULL leaves
|
||||
one as it was): the game's server calls it as they change (calling it with
|
||||
the same again costs little) */
|
||||
@@ -173,16 +167,10 @@ enum
|
||||
/* 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,
|
||||
P2P_LISTING_KEY_HASH_SIZE = 16,
|
||||
P2P_LISTING_SEALED_TOKEN_SIZE = 56,
|
||||
};
|
||||
|
||||
struct p2p_listing
|
||||
{
|
||||
/* (empty for a locked one until p2p_listing_unlock opens it) */
|
||||
char invite[P2P_LISTING_INVITE_SIZE];
|
||||
/* the host's (its XNADDR's abEnet once reached) */
|
||||
unsigned char identifier[6];
|
||||
@@ -195,20 +183,12 @@ struct p2p_listing
|
||||
unsigned char failed;
|
||||
/* milliseconds, -1 if not known */
|
||||
short ping;
|
||||
/* it has a password: its invite is sealed with it (p2p_listing_unlock) */
|
||||
unsigned char locked;
|
||||
unsigned char key_hash[P2P_LISTING_KEY_HASH_SIZE];
|
||||
unsigned char signing_key[P2P_LISTING_SIGNING_KEY_SIZE];
|
||||
unsigned char sealed_token[P2P_LISTING_SEALED_TOKEN_SIZE];
|
||||
};
|
||||
/* a copy of the public games found, in the order shown (the most players
|
||||
first; then those not failed, the open ones, by name); returns their count */
|
||||
int p2p_lobby_games(struct p2p_listing *games, int maximum_count);
|
||||
/* a game the browser could not join: kept, marked failed, for this run */
|
||||
void p2p_lobby_mark_failed(const unsigned char *identifier);
|
||||
/* a locked game's invite (into its invite), if the password is its own:
|
||||
FALSE if not; TRUE for a game with none. It takes a few milliseconds */
|
||||
int p2p_listing_unlock(struct p2p_listing *listing, const char *password);
|
||||
|
||||
/* the sizes of a Discord user's id and name as kept (with their end), and
|
||||
the text kept of either as told: only the characters allowed (digits in an
|
||||
|
||||
@@ -20,7 +20,6 @@ tunnel's packets are sealed with keys only its two machines have.
|
||||
#include "monocypher.h"
|
||||
#include "monocypher-ed25519.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---------- SHA-256 */
|
||||
@@ -650,50 +649,6 @@ int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519
|
||||
return !p2p_equal(product, zero, P2P_KEY_SIZE);
|
||||
}
|
||||
|
||||
/* ---------- passwords (a password-protected public game's listing holds
|
||||
its invite's token sealed with the password's key: p2p_lobby.c) */
|
||||
|
||||
/* (Argon2id of 4 MiB and three passes: tens of milliseconds, once for the
|
||||
host and once a guess for a joiner) */
|
||||
#define PASSWORD_KEY_BLOCKS 4096
|
||||
#define PASSWORD_KEY_PASSES 3
|
||||
|
||||
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key)
|
||||
{
|
||||
crypto_argon2_config config = { CRYPTO_ARGON2_ID, PASSWORD_KEY_BLOCKS, PASSWORD_KEY_PASSES, 1 };
|
||||
crypto_argon2_inputs inputs;
|
||||
void *work_area = malloc((size_t)PASSWORD_KEY_BLOCKS * 1024);
|
||||
|
||||
inputs.pass = (const unsigned char *)password;
|
||||
inputs.pass_size = (unsigned int)strlen(password);
|
||||
inputs.salt = salt;
|
||||
inputs.salt_size = P2P_KEY_SIZE;
|
||||
if (!work_area)
|
||||
{
|
||||
/* (no key anyone could guess: nothing opens with it) */
|
||||
posix_random_bytes(key, P2P_PASSWORD_KEY_SIZE);
|
||||
return;
|
||||
}
|
||||
crypto_argon2(key, P2P_PASSWORD_KEY_SIZE, work_area, config, inputs, crypto_argon2_no_extras);
|
||||
crypto_wipe(work_area, (size_t)PASSWORD_KEY_BLOCKS * 1024);
|
||||
free(work_area);
|
||||
}
|
||||
|
||||
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
|
||||
unsigned char *sealed)
|
||||
{
|
||||
/* (a nonce of its own each time: 24 random bytes never repeat) */
|
||||
posix_random_bytes(sealed, 24);
|
||||
crypto_aead_lock(sealed + 24 + 16, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, token, P2P_TOKEN_SIZE);
|
||||
}
|
||||
|
||||
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
|
||||
unsigned char *token)
|
||||
{
|
||||
return crypto_aead_unlock(token, sealed + 24, key, sealed, signing_key, P2P_KEY_SIZE, sealed + 24 + 16,
|
||||
P2P_TOKEN_SIZE) == 0;
|
||||
}
|
||||
|
||||
int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int size,
|
||||
const unsigned char *signature)
|
||||
{
|
||||
|
||||
@@ -246,26 +246,6 @@ int p2p_ed25519_verify(const unsigned char *public_key, const void *message, int
|
||||
/* the X25519 public key of an Ed25519 one; 0 if it has a small order */
|
||||
int p2p_ed25519_to_x25519(const unsigned char *public_key, unsigned char *x25519_public);
|
||||
|
||||
enum
|
||||
{
|
||||
/* a password-protected listing's token, sealed (p2p_seal_token): its
|
||||
nonce (24), its tag (16), then the token sealed */
|
||||
P2P_PASSWORD_KEY_SIZE = 32,
|
||||
P2P_SEALED_TOKEN_SIZE = 24 + 16 + 16,
|
||||
};
|
||||
|
||||
/* the key of a password (Argon2id: P2P_PASSWORD_KEY_SIZE bytes), for the
|
||||
host whose Ed25519 key salt is (P2P_KEY_SIZE bytes): it takes a few
|
||||
milliseconds, so that guessing passwords at a listing takes long */
|
||||
void p2p_password_key(const char *password, const unsigned char *salt, unsigned char *key);
|
||||
/* a token (P2P_TOKEN_SIZE bytes) sealed with a password's key, bound to the
|
||||
host's Ed25519 key (P2P_SEALED_TOKEN_SIZE bytes); and opened: 0 if the key
|
||||
is not the one it was sealed with (a wrong password), or it was altered */
|
||||
void p2p_seal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *token,
|
||||
unsigned char *sealed);
|
||||
int p2p_unseal_token(const unsigned char *key, const unsigned char *signing_key, const unsigned char *sealed,
|
||||
unsigned char *token);
|
||||
|
||||
/* ---------- p2p_lobby.c: public games' listings */
|
||||
|
||||
/* the p2p thread's pass: the token of the game hosted for the internet
|
||||
|
||||
@@ -12,11 +12,9 @@ the host can list its invite, alter its listing, or list another's under
|
||||
false details:
|
||||
|
||||
"HL", format 1, the lobby version (2: HALO_PORT_NETWORK_VERSION; browsers
|
||||
hide others), flags (open, under way, teams, closed, password), sequence
|
||||
(4: newest wins), Unix time (4), the Ed25519 key (32), the invite's token
|
||||
(16; a password's game's sealed with the password's key, 56:
|
||||
p2p_seal_token; a tombstone's zero), players, most players, the
|
||||
gametype's engine (1 each), the game's name,
|
||||
hide others), flags (open, under way, teams, closed), sequence (4: newest
|
||||
wins), Unix time (4), the Ed25519 key (32), the invite's token (16),
|
||||
players, most players, the gametype's engine (1 each), the game's name,
|
||||
map and gametype (a length byte, then up to 32, 32 and 24 characters of
|
||||
printable ASCII), a stamp (8, reserved: zero), and the signature (64) of
|
||||
"hceu-lobby-1" and all before it.
|
||||
@@ -40,14 +38,6 @@ one deletes or keeps is no authority, so
|
||||
A host that stops publishes a tombstone, then clears its slot. Going private
|
||||
makes a new invite (p2p.c), so a listing seen before lets no one in.
|
||||
|
||||
A game with a password (Server Setup's PASSWORD: p2p_set_hosting_password)
|
||||
is listed with its token sealed with the password's key (Argon2id of the
|
||||
password, salted with the host's key: p2p_password_key), so that only who
|
||||
knows the password can join it from the server browser
|
||||
(p2p_listing_unlock); its invite link, which holds the token, still joins it
|
||||
as any invite does. Setting or changing the password makes a new invite, as
|
||||
going private does.
|
||||
|
||||
Checking signatures takes work, and anyone can send listings: at most
|
||||
VERIFY_BUDGET milliseconds of it each pass of the p2p thread, from a queue
|
||||
of MAXIMUM_QUEUED; the rest is dropped (hosts publish again).
|
||||
@@ -72,11 +62,11 @@ enum
|
||||
_listing_has_teams = 4,
|
||||
/* a tombstone: the host stopped */
|
||||
_listing_closed = 8,
|
||||
/* its token sealed with a password's key */
|
||||
/* (reserved) */
|
||||
_listing_password = 16,
|
||||
STAMP_SIZE = 8,
|
||||
/* the signed part's end: everything but the signature */
|
||||
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_SEALED_TOKEN_SIZE + 3 +
|
||||
MAXIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 +
|
||||
(1 + P2P_LISTING_NAME_SIZE) + (1 + P2P_LISTING_MAP_SIZE) + (1 + P2P_LISTING_GAMETYPE_SIZE) + STAMP_SIZE +
|
||||
P2P_SIGNATURE_SIZE,
|
||||
MINIMUM_LISTING_SIZE = 2 + 1 + 2 + 1 + 4 + 4 + P2P_KEY_SIZE + P2P_TOKEN_SIZE + 3 + 3 + STAMP_SIZE +
|
||||
@@ -98,10 +88,6 @@ enum
|
||||
|
||||
static const char signature_label[] = "hceu-lobby-1";
|
||||
|
||||
/* (p2p.h's sizes of a locked listing's are p2p_internal.h's) */
|
||||
typedef char check_listing_sizes[P2P_LISTING_SIGNING_KEY_SIZE == P2P_KEY_SIZE &&
|
||||
P2P_LISTING_KEY_HASH_SIZE == P2P_KEY_HASH_SIZE && P2P_LISTING_SEALED_TOKEN_SIZE == P2P_SEALED_TOKEN_SIZE ? 1 : -1];
|
||||
|
||||
/* a listing, read */
|
||||
struct listing
|
||||
{
|
||||
@@ -111,8 +97,6 @@ struct listing
|
||||
unsigned long time;
|
||||
unsigned char key[P2P_KEY_SIZE];
|
||||
unsigned char token[P2P_TOKEN_SIZE];
|
||||
/* (_listing_password: its token, sealed) */
|
||||
unsigned char sealed_token[P2P_SEALED_TOKEN_SIZE];
|
||||
int player_count, maximum_player_count, engine_type;
|
||||
char name[P2P_LISTING_NAME_SIZE + 1];
|
||||
char map[P2P_LISTING_MAP_SIZE + 1];
|
||||
@@ -159,9 +143,6 @@ static struct
|
||||
int engine_type;
|
||||
int flags;
|
||||
int player_count, maximum_player_count;
|
||||
/* the password's key (p2p_set_hosting_password), if it has one */
|
||||
int has_password;
|
||||
unsigned char password_key[P2P_PASSWORD_KEY_SIZE];
|
||||
/* listed: with this token; its listing as last published */
|
||||
int listed;
|
||||
unsigned char token[P2P_TOKEN_SIZE];
|
||||
@@ -259,8 +240,6 @@ static int listing_make(unsigned char *bytes, int flags)
|
||||
unsigned char data[sizeof(signature_label) - 1 + MAXIMUM_LISTING_SIZE];
|
||||
int size = 0;
|
||||
|
||||
if (lobby.has_password && !(flags & _listing_closed))
|
||||
flags |= _listing_password;
|
||||
bytes[size++] = 'H';
|
||||
bytes[size++] = 'L';
|
||||
bytes[size++] = LISTING_FORMAT;
|
||||
@@ -273,20 +252,8 @@ static int listing_make(unsigned char *bytes, int flags)
|
||||
size += 4;
|
||||
memcpy(bytes + size, p2p_signing_key(), P2P_KEY_SIZE);
|
||||
size += P2P_KEY_SIZE;
|
||||
if (flags & _listing_password)
|
||||
{
|
||||
p2p_seal_token(lobby.password_key, p2p_signing_key(), lobby.token, bytes + size);
|
||||
size += P2P_SEALED_TOKEN_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* (a tombstone's none: it may end a game with a password) */
|
||||
if (flags & _listing_closed)
|
||||
memset(bytes + size, 0, P2P_TOKEN_SIZE);
|
||||
else
|
||||
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
|
||||
size += P2P_TOKEN_SIZE;
|
||||
}
|
||||
memcpy(bytes + size, lobby.token, P2P_TOKEN_SIZE);
|
||||
size += P2P_TOKEN_SIZE;
|
||||
bytes[size++] = (unsigned char)(lobby.player_count > 255 ? 255 : lobby.player_count);
|
||||
bytes[size++] = (unsigned char)(lobby.maximum_player_count > 255 ? 255 : lobby.maximum_player_count);
|
||||
bytes[size++] = (unsigned char)lobby.engine_type;
|
||||
@@ -335,20 +302,8 @@ static int listing_read(const unsigned char *bytes, int size, struct listing *li
|
||||
offset += 4;
|
||||
memcpy(listing->key, bytes + offset, P2P_KEY_SIZE);
|
||||
offset += P2P_KEY_SIZE;
|
||||
if (listing->flags & _listing_password)
|
||||
{
|
||||
/* (its token sealed, which is longer: the counts after it too) */
|
||||
if (offset + P2P_SEALED_TOKEN_SIZE + 3 > size)
|
||||
return 0;
|
||||
memset(listing->token, 0, P2P_TOKEN_SIZE);
|
||||
memcpy(listing->sealed_token, bytes + offset, P2P_SEALED_TOKEN_SIZE);
|
||||
offset += P2P_SEALED_TOKEN_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
|
||||
offset += P2P_TOKEN_SIZE;
|
||||
}
|
||||
memcpy(listing->token, bytes + offset, P2P_TOKEN_SIZE);
|
||||
offset += P2P_TOKEN_SIZE;
|
||||
listing->player_count = bytes[offset++];
|
||||
listing->maximum_player_count = bytes[offset++];
|
||||
listing->engine_type = bytes[offset++];
|
||||
@@ -614,22 +569,10 @@ static void listing_take(const struct queued *queued, const struct listing *list
|
||||
memcpy(game->payload, queued->payload, (size_t)queued->size);
|
||||
game->payload_size = queued->size;
|
||||
shown = &game->listing;
|
||||
shown->locked = (listing->flags & _listing_password) != 0;
|
||||
if (shown->locked)
|
||||
{
|
||||
/* (its invite once the password opens its token: p2p_listing_unlock) */
|
||||
shown->invite[0] = 0;
|
||||
memcpy(shown->key_hash, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(shown->signing_key, listing->key, P2P_KEY_SIZE);
|
||||
memcpy(shown->sealed_token, listing->sealed_token, P2P_SEALED_TOKEN_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
|
||||
}
|
||||
memcpy(bytes, queued->key_hash, P2P_KEY_HASH_SIZE);
|
||||
memcpy(bytes + P2P_KEY_HASH_SIZE, listing->token, P2P_TOKEN_SIZE);
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(shown->invite, sizeof(shown->invite), "halo://join/%s", text);
|
||||
p2p_identifier_from_hash(queued->key_hash, shown->identifier);
|
||||
memcpy(shown->name, listing->name, sizeof(shown->name));
|
||||
memcpy(shown->map, listing->map, sizeof(shown->map));
|
||||
@@ -726,33 +669,6 @@ void p2p_set_hosting_public(int public)
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
}
|
||||
|
||||
void p2p_set_hosting_password(const char *password)
|
||||
{
|
||||
unsigned char key[P2P_PASSWORD_KEY_SIZE];
|
||||
int has_password = password && *password;
|
||||
|
||||
/* (the key takes a while: not under the lock) */
|
||||
if (has_password)
|
||||
p2p_password_key(password, p2p_signing_key(), key);
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
if (has_password != lobby.has_password ||
|
||||
(has_password && !p2p_equal(key, lobby.password_key, P2P_PASSWORD_KEY_SIZE)))
|
||||
{
|
||||
/* (a new invite, if one was listed: who saw it, with no password or
|
||||
another, cannot join with it) */
|
||||
p2p_new_invite_if_listed();
|
||||
lobby.has_password = has_password;
|
||||
if (has_password)
|
||||
memcpy(lobby.password_key, key, sizeof(key));
|
||||
else
|
||||
memset(lobby.password_key, 0, sizeof(lobby.password_key));
|
||||
if (lobby.listed)
|
||||
republish_soon(0);
|
||||
}
|
||||
pthread_mutex_unlock(&p2p_lock);
|
||||
memset(key, 0, sizeof(key));
|
||||
}
|
||||
|
||||
void p2p_set_game_listing(const char *name, const char *map, const char *gametype, int engine_type, int open,
|
||||
int in_progress, int has_teams)
|
||||
{
|
||||
@@ -849,28 +765,6 @@ int p2p_lobby_games(struct p2p_listing *games, int maximum_count)
|
||||
return count;
|
||||
}
|
||||
|
||||
int p2p_listing_unlock(struct p2p_listing *listing, const char *password)
|
||||
{
|
||||
unsigned char key[P2P_PASSWORD_KEY_SIZE];
|
||||
unsigned char bytes[P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE];
|
||||
char text[2 * (P2P_KEY_HASH_SIZE + P2P_TOKEN_SIZE) + 1];
|
||||
int opened;
|
||||
|
||||
if (!listing->locked)
|
||||
return 1;
|
||||
p2p_password_key(password ? password : "", listing->signing_key, key);
|
||||
memcpy(bytes, listing->key_hash, P2P_KEY_HASH_SIZE);
|
||||
opened = p2p_unseal_token(key, listing->signing_key, listing->sealed_token, bytes + P2P_KEY_HASH_SIZE);
|
||||
if (opened)
|
||||
{
|
||||
p2p_hex(bytes, sizeof(bytes), text);
|
||||
snprintf(listing->invite, sizeof(listing->invite), "halo://join/%s", text);
|
||||
}
|
||||
memset(key, 0, sizeof(key));
|
||||
memset(bytes, 0, sizeof(bytes));
|
||||
return opened;
|
||||
}
|
||||
|
||||
void p2p_lobby_mark_failed(const unsigned char *identifier)
|
||||
{
|
||||
pthread_mutex_lock(&p2p_lock);
|
||||
|
||||
@@ -104,9 +104,8 @@ enum
|
||||
PROOF_SIZE = NONCE_SIZE + TAG_SIZE,
|
||||
BUFFER_SIZE = 4096,
|
||||
MAXIMUM_MESSAGE_SIZE = 512,
|
||||
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, 268 with a
|
||||
password's sealed token, and some) */
|
||||
MAXIMUM_LISTING_SIZE = 320,
|
||||
/* a listing's most (p2p_lobby.c's MAXIMUM_LISTING_SIZE, and some) */
|
||||
MAXIMUM_LISTING_SIZE = 256,
|
||||
/* the publishes awaiting acknowledgement on a broker */
|
||||
MAXIMUM_IN_FLIGHT = 4,
|
||||
/* the topics a broker is subscribed to (_topic_*) */
|
||||
|
||||
@@ -16,7 +16,6 @@ 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>
|
||||
@@ -97,12 +96,6 @@ static const struct config_setting config_settings[] =
|
||||
{ "display.max_fps", _config_integer, "0", "HALO_MAX_FPS", _environment_value, _platform_desktop,
|
||||
"With vsync off, the most frames a second: 0 for twice the display's\n"
|
||||
"refresh rate, -1 for no limit (which can hang some Intel graphics)." },
|
||||
{ "display.anti_aliasing", _config_string, "\"off\"", "HALO_ANTI_ALIASING", _environment_value, _platform_all,
|
||||
"Smoothing of jagged edges, which the Xbox did not have: \"off\"; \"fxaa\"\n"
|
||||
"or \"smaa\" smooth the 3D view once it is drawn (the HUD and menus stay\n"
|
||||
"sharp); \"ssaa2x\" draws at twice the resolution each way (four times\n"
|
||||
"the work); \"msaa2x\", \"msaa4x\" or \"msaa8x\" draw with that many samples\n"
|
||||
"a pixel. Android has \"fxaa\" for \"smaa\", and no \"ssaa2x\"." },
|
||||
{ "display.interpolation", _config_boolean, "true", "HALO_INTERPOLATION", _environment_value, _platform_all,
|
||||
"Draw a frame for every display refresh, blending between the game's 30\n"
|
||||
"ticks a second; false keeps the original 30 frames a second." },
|
||||
@@ -118,10 +111,6 @@ static const struct config_setting config_settings[] =
|
||||
"Draw the menus' and HUD's text with the fonts in port/assets/fonts\n"
|
||||
"(Overpass) at the resolution the game draws at, and the menus' titles\n"
|
||||
"from port/assets/titles; false draws the maps' bitmap fonts and titles." },
|
||||
{ "display.shadow_resolution", _config_integer, "128", "HALO_SHADOW_RESOLUTION", _environment_value,
|
||||
_platform_all,
|
||||
"The size the objects' shadows are drawn at, in pixels each way: 128 as\n"
|
||||
"on the Xbox, or 256, 512 or 1024 for smoother edges, as soft." },
|
||||
{ "display.menus", _config_string, "\"pc\"", "HALO_MENUS", _environment_value, _platform_all,
|
||||
"The menus: \"pc\" for the PC version's main menu (port/assets/menus,\n"
|
||||
"and a menus folder here for your own), \"xbox\" for the Xbox's." },
|
||||
@@ -146,12 +135,6 @@ static const struct config_setting config_settings[] =
|
||||
_environment_value, _platform_all,
|
||||
"The scoreboard panel's colour: \"red, green, blue, alpha\", each 0 to 255\n"
|
||||
"(alpha 0 is see-through, 255 solid)." },
|
||||
{ "display.per_pixel_lighting", _config_boolean, "false", "HALO_PER_PIXEL_LIGHTING", _environment_value,
|
||||
_platform_all,
|
||||
"Light the models (characters, weapons, vehicles, scenery) for each\n"
|
||||
"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." },
|
||||
|
||||
{ "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"
|
||||
@@ -165,13 +148,6 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_android,
|
||||
"The display refresh rate to ask the headset for, in Hz (72, 90, 120);\n"
|
||||
"it picks the nearest it has at or below. 0 for the highest." },
|
||||
{ "vr.graphics", _config_string, "\"high\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
|
||||
"Which of the costlier effects are drawn: \"high\" all of them; \"medium\"\n"
|
||||
"no object shadows, environment reflections or lens flares; \"low\" nor\n"
|
||||
"specular light, bump maps, grass, the screen's fog, contrails, weather\n"
|
||||
"or camouflage's second pass (a client in another's game keeps grass,\n"
|
||||
"lens flares and fog); \"max\" all of them, the models lit for each\n"
|
||||
"pixel and shadows at 512x512." },
|
||||
{ "vr.positional_tracking", _config_boolean, "true", "HALO_VR_POSITIONAL", _environment_value, _platform_android,
|
||||
"Moving your head moves the view (leaning, ducking); false only turns it.\n"
|
||||
"The view recentres when play starts or resumes after a cinematic, and\n"
|
||||
@@ -188,41 +164,8 @@ static const struct config_setting config_settings[] =
|
||||
"Log the frame rate and where the frame time goes every five seconds\n"
|
||||
"(adb logcat -s halo)." },
|
||||
{ "vr.aim_with", _config_string, "\"controller\"", "HALO_VR_AIM_WITH", _environment_value, _platform_android,
|
||||
"\"controller\": the weapon is in your hand (vr.handedness) and aims where\n"
|
||||
"it points, while you move and look with your head; \"head\": you aim\n"
|
||||
"where you look." },
|
||||
{ "vr.handedness", _config_string, "\"right\"", "HALO_VR_HANDEDNESS", _environment_value, _platform_android,
|
||||
"The hand that holds the weapon: \"right\", or \"left\", which mirrors the\n"
|
||||
"weapon and the arms, throws grenades and grips the foregrip with the\n"
|
||||
"right hand, and fires with the left trigger." },
|
||||
{ "vr.mirror_controls", _config_boolean, "true", "HALO_VR_MIRROR_CONTROLS", _environment_value,
|
||||
_platform_android,
|
||||
"Left-handed, the sticks and buttons swap sides too: the right stick moves\n"
|
||||
"and the left one turns, X jumps and A reloads (on the Steam Frame the\n"
|
||||
"D-pad takes A, B, X and Y as their mirror image); false keeps them as\n"
|
||||
"right-handed. The menus keep their buttons either way." },
|
||||
{ "vr.button_jump", _config_string, "\"a\"", "HALO_VR_BUTTON_JUMP", _environment_value, _platform_android,
|
||||
"The button each of the game's actions is on (the VR options' BUTTONS\n"
|
||||
"page swaps them): \"a\", \"b\", \"x\", \"y\" (A and B on the right\n"
|
||||
"controller, X and Y the Quest's on the left; the Steam Frame has all\n"
|
||||
"four on the right), \"move_stick\" and \"turn_stick\" (their clicks),\n"
|
||||
"\"grip\" (the weapon hand's), named as right-handed: left-handed with\n"
|
||||
"mirror_controls, their mirror images. Jump," },
|
||||
{ "vr.button_light", _config_string, "\"b\"", "HALO_VR_BUTTON_LIGHT", _environment_value, _platform_android,
|
||||
"the flashlight (held: switch grenades)," },
|
||||
{ "vr.button_action", _config_string, "\"x\"", "HALO_VR_BUTTON_ACTION", _environment_value, _platform_android,
|
||||
"action and reload," },
|
||||
{ "vr.button_weapon", _config_string, "\"y\"", "HALO_VR_BUTTON_WEAPON", _environment_value, _platform_android,
|
||||
"switch weapon," },
|
||||
{ "vr.button_crouch", _config_string, "\"move_stick\"", "HALO_VR_BUTTON_CROUCH", _environment_value,
|
||||
_platform_android,
|
||||
"crouch," },
|
||||
{ "vr.button_zoom", _config_string, "\"turn_stick\"", "HALO_VR_BUTTON_ZOOM", _environment_value,
|
||||
_platform_android,
|
||||
"zoom" },
|
||||
{ "vr.button_melee", _config_string, "\"grip\"", "HALO_VR_BUTTON_MELEE", _environment_value, _platform_android,
|
||||
"and melee. The triggers, the free hand's grip and the menu button keep\n"
|
||||
"theirs, as do the menus." },
|
||||
"\"controller\": the weapon is in your right hand and aims where it points,\n"
|
||||
"while you move and look with your head; \"head\": you aim where you look." },
|
||||
{ "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,
|
||||
@@ -237,33 +180,21 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.weapon_down", _config_real, "0.14", "HALO_VR_WEAPON_DOWN", _environment_value, _platform_android,
|
||||
"See weapon_forward." },
|
||||
{ "vr.two_hand_reach", _config_real, "0.2", "HALO_VR_TWO_HAND_REACH", _environment_value, _platform_android,
|
||||
"How near, in metres, the free hand must be to the weapon's foregrip for\n"
|
||||
"How near, in metres, the left 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; \"hands\", the hands\n"
|
||||
"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." },
|
||||
"melee attack (the right 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." },
|
||||
"A grenade is held while the left trigger is, and thrown as it lets go:\n"
|
||||
"along the hand's throw, when the hand moved faster than this (metres per\n"
|
||||
"second; full strength at 4), else as the game throws it. 0 turns the\n"
|
||||
"hand's throw off." },
|
||||
{ "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)." },
|
||||
"The size the grenade held in the left 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),", },
|
||||
"Where the held grenade is drawn, in metres from the left controller's\n"
|
||||
"grip: towards the palm (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,
|
||||
@@ -326,13 +257,6 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.comfort_vignette", _config_real, "0.7", "HALO_VR_COMFORT_VIGNETTE", _environment_value, _platform_android,
|
||||
"How far the view darkens from its edges while a vehicle turns you\n"
|
||||
"(0 never, 1 to a small window)." },
|
||||
{ "vr.foot_vignette", _config_string, "\"off\"", "HALO_VR_FOOT_VIGNETTE", _environment_value, _platform_android,
|
||||
"On foot, the view darkens from its edges too: \"turning\" while the stick\n"
|
||||
"turns you smoothly (smooth_turn), \"moving\" while a stick turns or moves\n"
|
||||
"you, as far as it is pushed; \"off\" never." },
|
||||
{ "vr.foot_vignette_strength", _config_real, "0.7", "HALO_VR_FOOT_VIGNETTE_STRENGTH", _environment_value,
|
||||
_platform_android,
|
||||
"How far it darkens then (as comfort_vignette)." },
|
||||
{ "vr.motion_tracker_on_wrist", _config_boolean, "true", "HALO_VR_MOTION_TRACKER_WRIST", _environment_value,
|
||||
_platform_android,
|
||||
"Show the motion tracker on the back of the left wrist, like a watch (turn\n"
|
||||
@@ -353,11 +277,9 @@ static const struct config_setting config_settings[] =
|
||||
{ "vr.smooth_turn", _config_boolean, "false", "HALO_VR_SMOOTH_TURN", _environment_value, _platform_android,
|
||||
"The right stick turns smoothly; false turns in steps (snap_turn_degrees)." },
|
||||
{ "vr.snap_turn_degrees", _config_real, "30.0", "HALO_VR_SNAP_TURN", _environment_value, _platform_android,
|
||||
"How far a flick of the right stick turns, in degrees (the VR options step\n"
|
||||
"through 10 to 45 by 5, 60, 75 and 90)." },
|
||||
"How far a flick of the right stick turns." },
|
||||
{ "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)." },
|
||||
"Degrees per second the right stick turns at, fully pushed, with smooth_turn." },
|
||||
{ "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)." },
|
||||
@@ -379,10 +301,6 @@ static const struct config_setting config_settings[] =
|
||||
{ "audio.effects_volume", _config_real, "1.0", "HALO_EFFECTS_VOLUME", _environment_value, _platform_all,
|
||||
"The volume of every other sound (effects and speech), 0.0 to 1.0 (of\n"
|
||||
"audio.volume)." },
|
||||
{ "audio.reverb", _config_boolean, "true", "HALO_REVERB", _environment_value, _platform_all,
|
||||
"Reverberate the world's sounds as the place the player is in does (the\n"
|
||||
"maps' sound environments, as the Xbox's I3DL2 reverb did); false keeps\n"
|
||||
"them dry." },
|
||||
|
||||
{ "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop,
|
||||
"How far the view turns for the mouse's movement." },
|
||||
@@ -507,12 +425,6 @@ static const struct config_setting config_settings[] =
|
||||
"\"on\", \"shields_only\" or \"explosives_only\" (Server Setup's FRIENDLY\n"
|
||||
"FIRE in co-op, which writes its choice here). Their AI allies they\n"
|
||||
"always can, as in the campaign." },
|
||||
{ "network.coop_player_collisions", _config_boolean, "true", "HALO_NET_COOP_PLAYER_COLLISIONS", _environment_value,
|
||||
_platform_all,
|
||||
"Whether the players of an online co-op game bump into each other;\n"
|
||||
"false, they walk through each other (the AI's characters they still\n"
|
||||
"bump into). Server Setup's PLAYER COLLISIONS in co-op writes its\n"
|
||||
"choice here." },
|
||||
{ "network.coop_enemies_mode", _config_string, "\"per_player\"", "HALO_NET_COOP_ENEMIES_MODE", _environment_value,
|
||||
_platform_all,
|
||||
"Online co-op's extra enemies: \"none\", \"per_player\" (each squad of\n"
|
||||
@@ -600,17 +512,14 @@ static const struct config_setting config_settings[] =
|
||||
{ "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.gl_environment", _config_string, "\"\"", "HALO_GL_ENVIRONMENT", _environment_value, _platform_android,
|
||||
"Variables for the GL driver, \"NAME=value NAME2=value\", set before it starts\n"
|
||||
"(the Steam Frame's Mesa: ZINK_DEBUG, TU_DEBUG, ...), for trying. Read when\n"
|
||||
"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_options", _config_boolean, "false", "HALO_DEBUG_VR_OPTIONS", _environment_value, _platform_android,
|
||||
"Off the headset (the emulator), show the VR options panel on the main\n"
|
||||
"and pause menus all the same, for checking its pages." },
|
||||
{ "debug.mirror_first_person", _config_boolean, "false", "HALO_MIRROR_FIRST_PERSON", _environment_value,
|
||||
_platform_android,
|
||||
"Off the headset (flat), the first-person weapon drawn mirrored, as the\n"
|
||||
"left hand's is in VR: a test aid for what reads mirrored there. Read once." },
|
||||
{ "debug.test_input", _config_string, "\"\"", "HALO_TEST_INPUT", _environment_value, _platform_all,
|
||||
"\"bot:<seed>\" plays controller 1 with a scripted pattern (automated\n"
|
||||
"network tests); \"look:<seed>\" stands still, only turning and looking\n"
|
||||
@@ -752,24 +661,13 @@ static char *config_read_file(const char *path, size_t *size)
|
||||
static int config_write_file(const char *path, const char *text)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
/* 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;
|
||||
FILE *file = fopen(path, "wb");
|
||||
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);
|
||||
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;
|
||||
return fclose(file) == 0 && written;
|
||||
#else
|
||||
return SDL_SaveFile(path, text, strlen(text));
|
||||
#endif
|
||||
|
||||
@@ -656,10 +656,6 @@ struct texture_entry
|
||||
unsigned long last_used_frame;
|
||||
/* the high-res HUD texture drawn in its place (hud_hires.h), or -1 */
|
||||
long override;
|
||||
/* the newest generation of its pages (memory_watch_generation) as of the
|
||||
memory watch serial read before it was found: the same while no watched
|
||||
page has been written since (0: never found) */
|
||||
unsigned long watched_serial, watched_generation;
|
||||
};
|
||||
|
||||
#define TEXTURE_BUCKET_COUNT 4096
|
||||
@@ -672,7 +668,7 @@ static struct texture_entry *texture_buckets[TEXTURE_BUCKET_COUNT];
|
||||
texture that is not palettized is remembered with the memory watch serial it
|
||||
started at: while no watched page has been written since, and no texture
|
||||
has been dropped, the same lookup finds the same current texture. */
|
||||
#define RECENT_TEXTURE_COUNT 512
|
||||
#define RECENT_TEXTURE_COUNT 64
|
||||
|
||||
static struct
|
||||
{
|
||||
@@ -684,14 +680,9 @@ static struct
|
||||
static unsigned long texture_drop_serial = 1;
|
||||
static unsigned long texture_frame = 0;
|
||||
|
||||
/* (every bit of the three mixed into the top ones: textures are aligned,
|
||||
and few sizes and formats are common) */
|
||||
static unsigned long bucket_index(DWORD data, DWORD format_word, DWORD size_word)
|
||||
{
|
||||
unsigned long hash = (unsigned long)data * 2654435761UL ^ (unsigned long)format_word * 2246822519UL ^
|
||||
(unsigned long)size_word * 3266489917UL;
|
||||
|
||||
return ((hash & 0xffffffffUL) >> 20) % TEXTURE_BUCKET_COUNT;
|
||||
return ((data >> 7) ^ (format_word * 2654435761UL) ^ size_word) % TEXTURE_BUCKET_COUNT;
|
||||
}
|
||||
|
||||
/* palettized textures are cached per palette contents: the game rewrites
|
||||
@@ -766,33 +757,25 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
|
||||
unsigned long recent = bucket_index(data, format_word, size_word) % RECENT_TEXTURE_COUNT;
|
||||
unsigned long watch_serial = memory_watch_serial();
|
||||
|
||||
/* (a texture that is not palettized has the one entry, until one is
|
||||
dropped: remembered, a page written since only means checking it) */
|
||||
entry = NULL;
|
||||
if (!palettized && recent_textures[recent].entry && recent_textures[recent].data == data &&
|
||||
recent_textures[recent].format_word == format_word && recent_textures[recent].size_word == size_word &&
|
||||
recent_textures[recent].watch_serial == watch_serial &&
|
||||
recent_textures[recent].drop_serial == texture_drop_serial)
|
||||
{
|
||||
entry = recent_textures[recent].entry;
|
||||
if (recent_textures[recent].watch_serial == watch_serial)
|
||||
{
|
||||
entry->last_used_frame = texture_frame;
|
||||
return texture_entry_result(entry, target, description);
|
||||
}
|
||||
entry->last_used_frame = texture_frame;
|
||||
return texture_entry_result(entry, target, description);
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
for (entry = *bucket; entry; entry = entry->next)
|
||||
{
|
||||
for (entry = *bucket; entry; entry = entry->next)
|
||||
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
|
||||
{
|
||||
if (entry->data == data && entry->format_word == format_word && entry->size_word == size_word)
|
||||
{
|
||||
if (entry->palette_hash == hash)
|
||||
break;
|
||||
variant_count++;
|
||||
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
|
||||
oldest_variant = entry;
|
||||
}
|
||||
if (entry->palette_hash == hash)
|
||||
break;
|
||||
variant_count++;
|
||||
if (!oldest_variant || entry->last_used_frame < oldest_variant->last_used_frame)
|
||||
oldest_variant = entry;
|
||||
}
|
||||
}
|
||||
if (!entry && variant_count >= MAXIMUM_PALETTE_VARIANTS)
|
||||
@@ -823,17 +806,7 @@ GLuint xgpu_texture_get(const DWORD *resource, const D3DCOLOR *palette, GLenum *
|
||||
|
||||
if (no_cache < 0)
|
||||
no_cache = config_boolean("debug.texture_no_cache");
|
||||
/* (its pages' newest generation, found again only once a watched page
|
||||
has been written: a large texture's pages, scanned for every draw that
|
||||
bound it, were much of a frame with many objects) */
|
||||
if (entry->watched_serial && entry->watched_serial == watch_serial)
|
||||
generation = entry->watched_generation;
|
||||
else
|
||||
{
|
||||
generation = memory_watch_generation(entry->address, entry->size);
|
||||
entry->watched_serial = watch_serial;
|
||||
entry->watched_generation = generation;
|
||||
}
|
||||
generation = memory_watch_generation(entry->address, entry->size);
|
||||
if (!entry->generation || generation > entry->generation || no_cache)
|
||||
{
|
||||
/* protect first, so a write racing with the upload is noticed */
|
||||
|
||||
@@ -53,11 +53,6 @@ must call this afterwards. */
|
||||
|
||||
void xgpu_gl_state_invalidate(void);
|
||||
|
||||
/* a compiled shader, or a linked program of two, or 0 with the log
|
||||
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);
|
||||
|
||||
/* ---------- generated source text */
|
||||
|
||||
struct xgpu_text
|
||||
@@ -76,34 +71,14 @@ void xgpu_text_append(struct xgpu_text *text, const char *format, ...) __attribu
|
||||
/* D3D constant register -96 is hardware register 0 */
|
||||
#define XGPU_VERTEX_CONSTANT_BIAS 96
|
||||
|
||||
/* where one of the game's model lighting programs (d3d8_gl.c
|
||||
halo_vertex_shader_lighting) has the normal, and the world position, that it
|
||||
lights the diffuse color by: the temporary register that holds each before
|
||||
the instruction given */
|
||||
struct nv2a_vertex_lighting
|
||||
{
|
||||
/* 1 by the ambient and distant lights, 2 by the point lights too */
|
||||
int lights;
|
||||
unsigned long normal_instruction, normal_register;
|
||||
/* (with the point lights only) */
|
||||
unsigned long position_instruction, position_register;
|
||||
};
|
||||
|
||||
/* finds them in a model lighting program, checking that it lights its
|
||||
diffuse color as nv2a_psh.c does for each pixel; FALSE if it does not */
|
||||
BOOL nv2a_vertex_shader_lighting(const DWORD *instructions, unsigned long instruction_count,
|
||||
struct nv2a_vertex_lighting *lighting);
|
||||
|
||||
/* GLSL for an NV2A vertex program (the instruction words after the program
|
||||
header). Attributes whose bit is set in packed_attribute_mask are fed as
|
||||
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. */
|
||||
NORMPACKED3 32-bit integers and unpacked in the shader. 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, int multiview);
|
||||
unsigned long packed_attribute_mask, int multiview);
|
||||
|
||||
/* ---------- pixel shaders */
|
||||
|
||||
@@ -139,15 +114,6 @@ struct nv2a_pixel_shader_key
|
||||
behind it only where it covers it (the Xbox's point-sampled meters stop
|
||||
at their texels' edges; filtered ones have a fringe of faint texels) */
|
||||
unsigned char coverage_alpha;
|
||||
/* a model lighting program's draw lit for each pixel
|
||||
(display.per_pixel_lighting): nv2a_vertex_lighting's lights, or 0 for
|
||||
the diffuse color the vertex shader computed */
|
||||
unsigned char per_pixel_lighting;
|
||||
/* drawn into a multisampled target (display.anti_aliasing's
|
||||
multisampling), its samples a pixel: the alpha test covers samples in
|
||||
proportion to how far alpha is past the reference, not all of the pixel
|
||||
or none of it, so that cut-out edges (foliage, grates) are smoothed too */
|
||||
unsigned char alpha_test_samples;
|
||||
};
|
||||
|
||||
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key);
|
||||
@@ -174,13 +140,6 @@ C0/C1 of each stage and the final combiner, and texture constants */
|
||||
"uniform vec4 texture_scale[4];\n" \
|
||||
XGPU_PIXEL_UNIFORMS_ES
|
||||
|
||||
/* the vertex constants the per-pixel model lighting reads, in a uniform of
|
||||
their own (the vertex shader's 192 would pass OpenGL ES's least fragment
|
||||
uniform space): [0] c[-82] (the translucency in z), [1] to [11] c[-79] to
|
||||
c[-69] (rasterizer_set_model_lighting's two point lights, two distant
|
||||
lights and the ambient light) */
|
||||
#define XGPU_MODEL_LIGHT_COUNT 12
|
||||
|
||||
/* ---------- textures */
|
||||
|
||||
struct xgpu_texture_description
|
||||
@@ -219,33 +178,9 @@ struct xgpu_render_target
|
||||
targets when the game draws at the display's resolution (d3d8_gl.c) */
|
||||
float scale[2];
|
||||
unsigned long gl_width, gl_height;
|
||||
/* with multisampling, the multisampled renderbuffer draws go to, its
|
||||
samples a pixel (0 when it has none), and whether it has been drawn into
|
||||
since the texture last had its pixels (d3d8_gl.c,
|
||||
render_target_multisample) */
|
||||
GLuint multisample;
|
||||
int samples;
|
||||
BOOL unresolved;
|
||||
};
|
||||
|
||||
/* the GL texture holding a render target with this physical address, or 0 */
|
||||
struct xgpu_render_target *xgpu_render_target_find(unsigned long data);
|
||||
|
||||
/* ---------- anti-aliasing
|
||||
|
||||
display.anti_aliasing's passes (xgpu_post.c): FXAA or SMAA antialias each
|
||||
window's 3D view in place before the HUD and menus are drawn over it, so
|
||||
that their text stays sharp. Supersampling and multisampling are the
|
||||
device's (d3d8_gl.c). */
|
||||
|
||||
/* the programs and textures of FXAA, or of SMAA, made now; FALSE if they
|
||||
cannot be (once FALSE, it stays so) */
|
||||
BOOL xgpu_post_prepare(BOOL smaa);
|
||||
|
||||
/* FXAA, or SMAA, on the corners x0, y0 to x1, y1 (from row 0) of a render
|
||||
target's framebuffer, width by height, GL_RGBA8; FALSE if its programs do
|
||||
not build */
|
||||
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
|
||||
const GLint corners[4]);
|
||||
|
||||
#endif
|
||||
@@ -1,524 +0,0 @@
|
||||
/*
|
||||
XGPU_POST.C
|
||||
|
||||
The post-process passes of display.anti_aliasing (xgpu.h): FXAA and SMAA,
|
||||
run on a render target in place, over the rectangle of a window's 3D view.
|
||||
Each pass draws one triangle covering the rectangle (its corners from
|
||||
gl_VertexID, with no vertex data) and finds its texels from gl_FragCoord: a
|
||||
target's rows are the picture's from the top, as the passes' own textures'
|
||||
are, so no coordinate is turned over. The target's pixels are copied first,
|
||||
so that no pass reads what it writes, and only colour is written back: the
|
||||
game keeps values of its own in destination alpha. The programs are built
|
||||
when the setting is chosen (xgpu_post_prepare), not in the middle of a frame.
|
||||
|
||||
FXAA is written here, after Timothy Lottes' description: the direction of an
|
||||
edge from the luma around a pixel, the edge's ends found by stepping along
|
||||
it, and the pixel blended across it by where it lies between them, or by how
|
||||
much it stands out from its neighbours. SMAA is port/third_party/smaa's,
|
||||
compiled as GLSL at its HIGH preset: edges from luma, blending weights from
|
||||
its lookup textures, then the blend. Android has FXAA only (d3d8_gl.c's
|
||||
anti_aliasing_values).
|
||||
*/
|
||||
|
||||
#include "xgpu.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
#include "memory/zlib/zlib.h"
|
||||
|
||||
/* port/third_party/smaa, embedded by tools/embed_assets.py: the shader's
|
||||
text (ending in a NUL), and the lookup textures' bytes compressed with
|
||||
zlib; each size 0 where the build had no such file */
|
||||
extern const unsigned int xgpu_smaa_shader[];
|
||||
extern const unsigned long xgpu_smaa_shader_size;
|
||||
extern const unsigned int xgpu_smaa_area_texture[];
|
||||
extern const unsigned long xgpu_smaa_area_texture_size;
|
||||
extern const unsigned int xgpu_smaa_search_texture[];
|
||||
extern const unsigned long xgpu_smaa_search_texture_size;
|
||||
#endif
|
||||
|
||||
enum
|
||||
{
|
||||
_post_program_fxaa,
|
||||
_post_program_smaa_edges,
|
||||
_post_program_smaa_weights,
|
||||
_post_program_smaa_blend,
|
||||
NUMBER_OF_POST_PROGRAMS
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
GLuint vertex_shader;
|
||||
GLuint programs[NUMBER_OF_POST_PROGRAMS];
|
||||
/* each program's metrics uniform, and FXAA's bounds */
|
||||
GLint metrics[NUMBER_OF_POST_PROGRAMS];
|
||||
GLint bounds;
|
||||
/* FXAA's, and SMAA's, programs or textures could not be made */
|
||||
BOOL failed[2];
|
||||
/* the size of the textures below */
|
||||
unsigned long width, height;
|
||||
/* the target's pixels before the pass, and SMAA's edges and blending
|
||||
weights */
|
||||
GLuint color, edges, weights;
|
||||
GLuint color_framebuffer, edges_framebuffer, weights_framebuffer;
|
||||
/* SMAA's lookup textures */
|
||||
GLuint area, search;
|
||||
/* linear and clamped, as FXAA and SMAA sample */
|
||||
GLuint sampler;
|
||||
/* no attributes: the triangle comes from gl_VertexID */
|
||||
GLuint vertex_array;
|
||||
} post;
|
||||
|
||||
/* ---------- programs */
|
||||
|
||||
static void shader_header(struct xgpu_text *text)
|
||||
{
|
||||
#ifdef HALO_ANDROID
|
||||
xgpu_text_append(text,
|
||||
"#version %s\n"
|
||||
"precision highp float;\n"
|
||||
"precision highp int;\n"
|
||||
"precision highp sampler2D;\n",
|
||||
xgpu_capabilities.shading_language);
|
||||
#else
|
||||
xgpu_text_append(text, "#version 450 core\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
/* one triangle over the whole viewport */
|
||||
static const char vertex_source[] =
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tgl_Position = vec4(float((gl_VertexID & 1) << 2) - 1.0, float((gl_VertexID & 2) << 1) - 1.0, 0.0, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
static const char fxaa_source[] =
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"/* 1 / width, 1 / height, width, height */\n"
|
||||
"uniform vec4 metrics;\n"
|
||||
"/* the rectangle's outermost texel centres, as texture coordinates */\n"
|
||||
"uniform vec4 bounds;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"const float EDGE_THRESHOLD = 0.125;\n"
|
||||
"const float EDGE_THRESHOLD_MINIMUM = 0.0312;\n"
|
||||
"const float SUBPIXEL_QUALITY = 0.75;\n"
|
||||
"const int STEPS = 12;\n"
|
||||
"const float STEP_LENGTHS[12] = float[12](1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 2.0, 2.0, 2.0, 4.0, 8.0);\n"
|
||||
"\n"
|
||||
"vec3 color_at(vec2 position)\n"
|
||||
"{\n"
|
||||
"\treturn textureLod(color_texture, clamp(position, bounds.xy, bounds.zw), 0.0).rgb;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"float luma_at(vec2 position)\n"
|
||||
"{\n"
|
||||
"\treturn dot(color_at(position), vec3(0.299, 0.587, 0.114));\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 position = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tfloat luma = luma_at(position);\n"
|
||||
"\tfloat up = luma_at(position + vec2(0.0, -metrics.y));\n"
|
||||
"\tfloat down = luma_at(position + vec2(0.0, metrics.y));\n"
|
||||
"\tfloat left = luma_at(position + vec2(-metrics.x, 0.0));\n"
|
||||
"\tfloat right = luma_at(position + vec2(metrics.x, 0.0));\n"
|
||||
"\tfloat highest = max(luma, max(max(up, down), max(left, right)));\n"
|
||||
"\tfloat range = highest - min(luma, min(min(up, down), min(left, right)));\n"
|
||||
"\n"
|
||||
"\t/* no edge: the target has the pixel already */\n"
|
||||
"\tif (range < max(EDGE_THRESHOLD_MINIMUM, highest * EDGE_THRESHOLD))\n"
|
||||
"\t\tdiscard;\n"
|
||||
"\tfloat up_left = luma_at(position + vec2(-metrics.x, -metrics.y));\n"
|
||||
"\tfloat up_right = luma_at(position + vec2(metrics.x, -metrics.y));\n"
|
||||
"\tfloat down_left = luma_at(position + vec2(-metrics.x, metrics.y));\n"
|
||||
"\tfloat down_right = luma_at(position + vec2(metrics.x, metrics.y));\n"
|
||||
"\n"
|
||||
"\t/* how much the pixel stands out from its neighbourhood (a feature\n"
|
||||
"\tthinner than a pixel) */\n"
|
||||
"\tfloat average = (2.0 * (up + down + left + right) + up_left + up_right + down_left + down_right) / 12.0;\n"
|
||||
"\tfloat subpixel = smoothstep(0.0, 1.0, clamp(abs(average - luma) / range, 0.0, 1.0));\n"
|
||||
"\tsubpixel = subpixel * subpixel * SUBPIXEL_QUALITY;\n"
|
||||
"\n"
|
||||
"\t/* an edge along the rows where the luma changes more from row to row\n"
|
||||
"\tthan from column to column; across it, the side it changes more to */\n"
|
||||
"\tbool horizontal = abs(up_left + down_left - 2.0 * left) + 2.0 * abs(up + down - 2.0 * luma) +\n"
|
||||
"\t\tabs(up_right + down_right - 2.0 * right) >=\n"
|
||||
"\t\tabs(up_left + up_right - 2.0 * up) + 2.0 * abs(left + right - 2.0 * luma) +\n"
|
||||
"\t\tabs(down_left + down_right - 2.0 * down);\n"
|
||||
"\tfloat before = horizontal ? up : left;\n"
|
||||
"\tfloat after = horizontal ? down : right;\n"
|
||||
"\tbool before_steeper = abs(before - luma) >= abs(after - luma);\n"
|
||||
"\tfloat gradient = 0.25 * max(abs(before - luma), abs(after - luma));\n"
|
||||
"\tfloat edge_luma = 0.5 * ((before_steeper ? before : after) + luma);\n"
|
||||
"\tvec2 across = (horizontal ? vec2(0.0, metrics.y) : vec2(metrics.x, 0.0)) * (before_steeper ? -1.0 : 1.0);\n"
|
||||
"\tvec2 along = horizontal ? vec2(metrics.x, 0.0) : vec2(0.0, metrics.y);\n"
|
||||
"\n"
|
||||
"\t/* along the edge, half a pixel across, to where its luma ends either way */\n"
|
||||
"\tvec2 end_before = position + 0.5 * across - along;\n"
|
||||
"\tvec2 end_after = position + 0.5 * across + along;\n"
|
||||
"\tfloat luma_before = luma_at(end_before) - edge_luma;\n"
|
||||
"\tfloat luma_after = luma_at(end_after) - edge_luma;\n"
|
||||
"\tbool reached_before = abs(luma_before) >= gradient;\n"
|
||||
"\tbool reached_after = abs(luma_after) >= gradient;\n"
|
||||
"\tfor (int index = 1; index < STEPS && !(reached_before && reached_after); index++)\n"
|
||||
"\t{\n"
|
||||
"\t\tif (!reached_before)\n"
|
||||
"\t\t{\n"
|
||||
"\t\t\tend_before -= along * STEP_LENGTHS[index];\n"
|
||||
"\t\t\tluma_before = luma_at(end_before) - edge_luma;\n"
|
||||
"\t\t\treached_before = abs(luma_before) >= gradient;\n"
|
||||
"\t\t}\n"
|
||||
"\t\tif (!reached_after)\n"
|
||||
"\t\t{\n"
|
||||
"\t\t\tend_after += along * STEP_LENGTHS[index];\n"
|
||||
"\t\t\tluma_after = luma_at(end_after) - edge_luma;\n"
|
||||
"\t\t\treached_after = abs(luma_after) >= gradient;\n"
|
||||
"\t\t}\n"
|
||||
"\t}\n"
|
||||
"\n"
|
||||
"\t/* blended across the edge the more, the nearer the pixel is to an end;\n"
|
||||
"\tonly where the luma at that end changes the way the pixel's does */\n"
|
||||
"\tfloat distance_before = horizontal ? position.x - end_before.x : position.y - end_before.y;\n"
|
||||
"\tfloat distance_after = horizontal ? end_after.x - position.x : end_after.y - position.y;\n"
|
||||
"\tfloat pixel_offset = 0.5 - min(distance_before, distance_after) / (distance_before + distance_after);\n"
|
||||
"\tbool varies = ((distance_before < distance_after ? luma_before : luma_after) < 0.0) != (luma < edge_luma);\n"
|
||||
"\tresult = vec4(color_at(position + max(varies ? pixel_offset : 0.0, subpixel) * across), 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
/* the main functions of SMAA's passes, after SMAA.hlsl; its vertex
|
||||
functions' offsets are found for each pixel */
|
||||
static const char *const smaa_mains[] =
|
||||
{
|
||||
/* edges */
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec4 offset[3];\n"
|
||||
"\tSMAAEdgeDetectionVS(texcoord, offset);\n"
|
||||
"\tresult = vec4(SMAALumaEdgeDetectionPS(texcoord, offset, color_texture), 0.0, 0.0);\n"
|
||||
"}\n",
|
||||
/* weights */
|
||||
"uniform sampler2D edges_texture;\n"
|
||||
"uniform sampler2D area_texture;\n"
|
||||
"uniform sampler2D search_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec2 pixcoord;\n"
|
||||
"\tvec4 offset[3];\n"
|
||||
"\tSMAABlendingWeightCalculationVS(texcoord, pixcoord, offset);\n"
|
||||
"\tresult = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, edges_texture, area_texture,\n"
|
||||
"\t\tsearch_texture, vec4(0.0));\n"
|
||||
"}\n",
|
||||
/* blend */
|
||||
"uniform sampler2D color_texture;\n"
|
||||
"uniform sampler2D weights_texture;\n"
|
||||
"out vec4 result;\n"
|
||||
"\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
"\tvec2 texcoord = gl_FragCoord.xy * metrics.xy;\n"
|
||||
"\tvec4 offset;\n"
|
||||
"\tSMAANeighborhoodBlendingVS(texcoord, offset);\n"
|
||||
"\tresult = SMAANeighborhoodBlendingPS(texcoord, offset, color_texture, weights_texture);\n"
|
||||
"}\n",
|
||||
};
|
||||
#endif
|
||||
|
||||
/* a program, its samplers given their texture units; 0 if it does not
|
||||
build */
|
||||
static GLuint program_build(int which)
|
||||
{
|
||||
static const char *const names[NUMBER_OF_POST_PROGRAMS] =
|
||||
{
|
||||
"FXAA", "SMAA edge detection", "SMAA blending weight", "SMAA neighborhood blending"
|
||||
};
|
||||
static const struct
|
||||
{
|
||||
const char *name;
|
||||
GLint unit;
|
||||
} units[] =
|
||||
{
|
||||
{ "color_texture", 0 }, { "edges_texture", 0 }, { "area_texture", 1 },
|
||||
{ "search_texture", 2 }, { "weights_texture", 1 },
|
||||
};
|
||||
struct xgpu_text text = { 0 };
|
||||
GLuint fragment_shader, program;
|
||||
unsigned long index;
|
||||
|
||||
if (!post.vertex_shader)
|
||||
{
|
||||
shader_header(&text);
|
||||
xgpu_text_append(&text, "%s", vertex_source);
|
||||
post.vertex_shader = xgpu_compile_shader(GL_VERTEX_SHADER, text.buffer, "post-process");
|
||||
text.length = 0;
|
||||
}
|
||||
shader_header(&text);
|
||||
if (which == _post_program_fxaa)
|
||||
{
|
||||
xgpu_text_append(&text, "%s", fxaa_source);
|
||||
}
|
||||
#ifndef HALO_ANDROID
|
||||
else if (xgpu_smaa_shader_size)
|
||||
{
|
||||
xgpu_text_append(&text,
|
||||
"#define SMAA_GLSL_4 1\n"
|
||||
"uniform vec4 metrics;\n"
|
||||
"#define SMAA_RT_METRICS metrics\n"
|
||||
"#define SMAA_PRESET_HIGH 1\n"
|
||||
"%s\n%s",
|
||||
(const char *)xgpu_smaa_shader, smaa_mains[which - _post_program_smaa_edges]);
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
platform_log("anti-aliasing: the build has no SMAA");
|
||||
free(text.buffer);
|
||||
return 0;
|
||||
}
|
||||
fragment_shader = post.vertex_shader ?
|
||||
xgpu_compile_shader(GL_FRAGMENT_SHADER, text.buffer, names[which]) :
|
||||
0;
|
||||
free(text.buffer);
|
||||
if (!fragment_shader)
|
||||
return 0;
|
||||
program = xgpu_link_program(post.vertex_shader, fragment_shader, names[which]);
|
||||
glDeleteShader(fragment_shader);
|
||||
if (!program)
|
||||
return 0;
|
||||
glUseProgram(program);
|
||||
/* (a name the program does not have is location -1, which is ignored) */
|
||||
for (index = 0; index < sizeof(units) / sizeof(units[0]); index++)
|
||||
glUniform1i(glGetUniformLocation(program, units[index].name), units[index].unit);
|
||||
post.metrics[which] = glGetUniformLocation(program, "metrics");
|
||||
if (which == _post_program_fxaa)
|
||||
post.bounds = glGetUniformLocation(program, "bounds");
|
||||
return program;
|
||||
}
|
||||
|
||||
/* ---------- textures */
|
||||
|
||||
static GLuint texture_new(GLenum format, unsigned long width, unsigned long height, GLenum data_format,
|
||||
const void *data)
|
||||
{
|
||||
GLuint texture;
|
||||
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, (GLint)format, (GLsizei)width, (GLsizei)height, 0, data_format,
|
||||
GL_UNSIGNED_BYTE, data);
|
||||
return texture;
|
||||
}
|
||||
|
||||
static GLuint framebuffer_new(GLuint texture)
|
||||
{
|
||||
GLuint framebuffer;
|
||||
GLenum draw_buffer = GL_COLOR_ATTACHMENT0;
|
||||
|
||||
glGenFramebuffers(1, &framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
glDrawBuffers(1, &draw_buffer);
|
||||
return framebuffer;
|
||||
}
|
||||
|
||||
/* the passes' textures, of the target's size (SMAA's only for SMAA); color
|
||||
is the target's format, so that the copy is exact */
|
||||
static void textures_fit(BOOL smaa, unsigned long width, unsigned long height)
|
||||
{
|
||||
if (post.width != width || post.height != height)
|
||||
{
|
||||
GLuint textures[3] = { post.color, post.edges, post.weights };
|
||||
GLuint framebuffers[3] = { post.color_framebuffer, post.edges_framebuffer, post.weights_framebuffer };
|
||||
|
||||
glDeleteFramebuffers(3, framebuffers);
|
||||
glDeleteTextures(3, textures);
|
||||
post.color = post.edges = post.weights = 0;
|
||||
post.width = width;
|
||||
post.height = height;
|
||||
post.color = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
|
||||
post.color_framebuffer = framebuffer_new(post.color);
|
||||
}
|
||||
if (smaa && !post.edges)
|
||||
{
|
||||
post.edges = texture_new(GL_RG8, width, height, GL_RG, NULL);
|
||||
post.weights = texture_new(GL_RGBA8, width, height, GL_RGBA, NULL);
|
||||
post.edges_framebuffer = framebuffer_new(post.edges);
|
||||
post.weights_framebuffer = framebuffer_new(post.weights);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
/* a lookup texture of SMAA's from its zlib stream; 0 if it does not
|
||||
inflate */
|
||||
static GLuint lookup_texture(const unsigned int *stream, unsigned long stream_size, GLenum format,
|
||||
GLenum data_format, unsigned long width, unsigned long height, unsigned long texel_size)
|
||||
{
|
||||
uLongf size = (uLongf)(width * height * texel_size);
|
||||
unsigned char *texels = malloc(size);
|
||||
GLuint texture = 0;
|
||||
int result;
|
||||
|
||||
if (!texels)
|
||||
return 0;
|
||||
result = uncompress(texels, &size, (const Bytef *)stream, (uLong)stream_size);
|
||||
/* (the game's zlib is 1.1, which can stop short of saying the stream
|
||||
has ended: the size says whether it is whole) */
|
||||
if ((result == Z_OK || result == Z_BUF_ERROR) && size == (uLongf)(width * height * texel_size))
|
||||
{
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
texture = texture_new(format, width, height, data_format, texels);
|
||||
}
|
||||
free(texels);
|
||||
return texture;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---------- the passes */
|
||||
|
||||
static void texture_bind(GLuint unit, GLuint texture)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + unit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindSampler(unit, post.sampler);
|
||||
}
|
||||
|
||||
/* a program's triangle drawn into a framebuffer over the rectangle;
|
||||
color_only keeps the framebuffer's alpha */
|
||||
static void pass_draw(int which, GLuint framebuffer, const GLint corners[4], BOOL color_only)
|
||||
{
|
||||
float metrics[4];
|
||||
|
||||
metrics[0] = 1.0f / (float)post.width;
|
||||
metrics[1] = 1.0f / (float)post.height;
|
||||
metrics[2] = (float)post.width;
|
||||
metrics[3] = (float)post.height;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glViewport(corners[0], corners[1], corners[2] - corners[0], corners[3] - corners[1]);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, color_only ? GL_FALSE : GL_TRUE);
|
||||
glUseProgram(post.programs[which]);
|
||||
glUniform4fv(post.metrics[which], 1, metrics);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
/* an intermediate emptied, all of it, so that no edge of another window's
|
||||
pass is found: SMAA's passes write only where there is an edge */
|
||||
static void intermediate_clear(GLuint framebuffer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
BOOL xgpu_post_prepare(BOOL smaa)
|
||||
{
|
||||
int first = smaa ? _post_program_smaa_edges : _post_program_fxaa;
|
||||
int last = smaa ? _post_program_smaa_blend : _post_program_fxaa;
|
||||
int which;
|
||||
|
||||
if (post.failed[smaa != FALSE])
|
||||
return FALSE;
|
||||
for (which = first; which <= last; which++)
|
||||
{
|
||||
if (!post.programs[which] && !(post.programs[which] = program_build(which)))
|
||||
{
|
||||
post.failed[smaa != FALSE] = TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#ifndef HALO_ANDROID
|
||||
if (smaa && !post.area)
|
||||
{
|
||||
post.area = lookup_texture(xgpu_smaa_area_texture, xgpu_smaa_area_texture_size, GL_RG8, GL_RG,
|
||||
160, 560, 2);
|
||||
post.search = lookup_texture(xgpu_smaa_search_texture, xgpu_smaa_search_texture_size, GL_R8, GL_RED,
|
||||
64, 16, 1);
|
||||
if (!post.area || !post.search)
|
||||
{
|
||||
platform_log("anti-aliasing: cannot inflate SMAA's lookup textures");
|
||||
post.failed[1] = TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!post.sampler)
|
||||
{
|
||||
glGenSamplers(1, &post.sampler);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(post.sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glGenVertexArrays(1, &post.vertex_array);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL xgpu_post_anti_alias(BOOL smaa, GLuint framebuffer, unsigned long width, unsigned long height,
|
||||
const GLint corners[4])
|
||||
{
|
||||
/* (made already, as the setting was chosen) */
|
||||
if (!xgpu_post_prepare(smaa))
|
||||
return FALSE;
|
||||
textures_fit(smaa, width, height);
|
||||
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_POLYGON_OFFSET_FILL);
|
||||
#ifndef HALO_ANDROID
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
#endif
|
||||
glBindVertexArray(post.vertex_array);
|
||||
|
||||
/* the target's pixels, read while the target is drawn into */
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, post.color_framebuffer);
|
||||
glBlitFramebuffer(corners[0], corners[1], corners[2], corners[3], corners[0], corners[1], corners[2], corners[3],
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
|
||||
if (!smaa)
|
||||
{
|
||||
float bounds[4];
|
||||
|
||||
/* (split screen: no texel of the next window is read) */
|
||||
bounds[0] = ((float)corners[0] + 0.5f) / (float)width;
|
||||
bounds[1] = ((float)corners[1] + 0.5f) / (float)height;
|
||||
bounds[2] = ((float)corners[2] - 0.5f) / (float)width;
|
||||
bounds[3] = ((float)corners[3] - 0.5f) / (float)height;
|
||||
texture_bind(0, post.color);
|
||||
glUseProgram(post.programs[_post_program_fxaa]);
|
||||
glUniform4fv(post.bounds, 1, bounds);
|
||||
pass_draw(_post_program_fxaa, framebuffer, corners, TRUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
intermediate_clear(post.edges_framebuffer);
|
||||
texture_bind(0, post.color);
|
||||
pass_draw(_post_program_smaa_edges, post.edges_framebuffer, corners, FALSE);
|
||||
|
||||
intermediate_clear(post.weights_framebuffer);
|
||||
texture_bind(0, post.edges);
|
||||
texture_bind(1, post.area);
|
||||
texture_bind(2, post.search);
|
||||
pass_draw(_post_program_smaa_weights, post.weights_framebuffer, corners, FALSE);
|
||||
|
||||
texture_bind(0, post.color);
|
||||
texture_bind(1, post.weights);
|
||||
pass_draw(_post_program_smaa_blend, framebuffer, corners, TRUE);
|
||||
}
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -851,19 +851,6 @@ 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"
|
||||
@@ -942,8 +929,9 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
|
||||
}
|
||||
merge_button(pad, XINPUT_GAMEPAD_A, (right->buttons & HALO_XR_BUTTON_A) != 0);
|
||||
merge_button(pad, XINPUT_GAMEPAD_X, (left->buttons & HALO_XR_BUTTON_A) != 0);
|
||||
/* (the grips holding a Warthog's wheel are not a blow: vr.c) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_B, (right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 || halo_vr_melee());
|
||||
/* (the grips holding a Warthog's wheel are not a blow) */
|
||||
merge_button(pad, XINPUT_GAMEPAD_B,
|
||||
((right->buttons & HALO_XR_BUTTON_SQUEEZE) != 0 && !halo_vr_wheel_held()) || halo_vr_melee());
|
||||
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) */
|
||||
@@ -1050,15 +1038,12 @@ DWORD WINAPI XInputSetState(HANDLE device, PXINPUT_FEEDBACK feedback)
|
||||
if (port < 0)
|
||||
return ERROR_DEVICE_NOT_CONNECTED;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: the heavy (left) motor in the free hand, the light one in the
|
||||
weapon's (left-handed, the other way round), as the game refreshes them
|
||||
every frame */
|
||||
/* VR: the heavy (left) motor in the left hand, the light one in the
|
||||
right, 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);
|
||||
host_xr_haptic(HALO_XR_HAND_LEFT, feedback->Rumble.wLeftMotorSpeed / 65535.0f, 0.1f);
|
||||
host_xr_haptic(HALO_XR_HAND_RIGHT, feedback->Rumble.wRightMotorSpeed / 65535.0f, 0.1f);
|
||||
}
|
||||
#endif
|
||||
count = sdl_gamepads(gamepads);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com)
|
||||
Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com)
|
||||
Copyright (C) 2013 Belen Masia (bmasia@unizar.es)
|
||||
Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com)
|
||||
Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software. As clarification, there is no
|
||||
requirement that the copyright notice and permission be included in binary
|
||||
distributions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,20 +0,0 @@
|
||||
# SMAA
|
||||
|
||||
Enhanced Subpixel Morphological Antialiasing, by Jorge Jimenez, Jose I.
|
||||
Echevarria, Belen Masia, Fernando Navarro and Diego Gutierrez, MIT licensed
|
||||
(see `LICENSE`).
|
||||
|
||||
Upstream: https://github.com/iryoku/smaa, commit
|
||||
71c806a838bdd7d517df19192a20f0c61b3ca29d. `SMAA.hlsl` is copied
|
||||
unchanged. `area_tex.zlib` and `search_tex.zlib` are the bytes of
|
||||
`Textures/AreaTex.h` (160x560, two channels) and `Textures/SearchTex.h`
|
||||
(64x16, one channel), compressed with zlib:
|
||||
|
||||
python -c "import re, sys, zlib; t = open(sys.argv[1]).read(); \
|
||||
open(sys.argv[2], 'wb').write(zlib.compress(bytes(int(h, 16) for h in \
|
||||
re.findall(r'0x([0-9a-fA-F]{2})', t[t.index('{'):])), 9))" AreaTex.h area_tex.zlib
|
||||
|
||||
`display.anti_aliasing = "smaa"` antialiases the 3D view with it, compiled
|
||||
as GLSL (`port/linux/src/xgpu_post.c`); the files are embedded in the Linux
|
||||
and Windows platform layers by `tools/embed_assets.py`. Android has FXAA in
|
||||
its place.
|
||||
@@ -2082,13 +2082,6 @@ boolean ai_enemies_attacking_player(
|
||||
return ai_enemies_endanger_player(TRUE, NONE);
|
||||
}
|
||||
|
||||
/* port: whether enemies can see this player's unit */
|
||||
boolean ai_port_enemies_can_see_unit(
|
||||
long unit_index)
|
||||
{
|
||||
return ai_enemies_endanger_player(FALSE, unit_index);
|
||||
}
|
||||
|
||||
/* port: whether enemies are attacking this player's unit */
|
||||
boolean ai_port_enemies_attacking_unit(
|
||||
long unit_index)
|
||||
|
||||
@@ -143,9 +143,6 @@ boolean ai_consider_major_upgrade(
|
||||
real chance);
|
||||
|
||||
/* port: network co-op's respawn (players.c) */
|
||||
/* port: whether enemies can see this player's unit */
|
||||
boolean ai_port_enemies_can_see_unit(
|
||||
long unit_index);
|
||||
boolean ai_port_enemies_attacking_unit(
|
||||
long unit_index);
|
||||
boolean ai_enemies_attacking_player(
|
||||
|
||||
@@ -3255,8 +3255,7 @@ void ai_scripting_attach_unit(
|
||||
squad_index,
|
||||
struct squad_definition);
|
||||
|
||||
/* port: an entry the palette has (a map's index), as the others here */
|
||||
if (VALID_INDEX(squad_definition->actor_palette_index, scenario->ai_actor_palette.count))
|
||||
if (squad_definition->actor_palette_index != NONE)
|
||||
{
|
||||
struct tag_reference *actor_palette_entry = TAG_BLOCK_GET_ELEMENT(
|
||||
&scenario->ai_actor_palette,
|
||||
|
||||
@@ -3006,9 +3006,9 @@ static void encounter_post_combat(
|
||||
return;
|
||||
}
|
||||
|
||||
/* port: extra_number, counting from 1, places one of network co-op's extra
|
||||
enemies (coop_enemies.c), spread around the squad's starting locations in
|
||||
turn; 0 places the squad's own actor, as on the Xbox */
|
||||
/* port: extra_number, from 1, places one of network co-op's extra enemies
|
||||
(coop_enemies.c): at the squad's starting locations in turn, each the
|
||||
place a ring about its own (0: the squad's own actor, as the Xbox's) */
|
||||
static boolean encounter_place_actor(
|
||||
long encounter_index,
|
||||
short squad_index,
|
||||
@@ -3033,59 +3033,16 @@ static boolean encounter_place_actor(
|
||||
short actor_palette_index = squad_definition->actor_palette_index;
|
||||
struct scenario *scenario = global_scenario_get();
|
||||
|
||||
/* port: an extra enemy goes on free ground around its starting
|
||||
location, else around the squad's others in turn */
|
||||
/* port: an extra enemy a ring about the starting location */
|
||||
if (extra_number > 0)
|
||||
{
|
||||
/* (what was found taken around the starting locations while this
|
||||
squad's extra enemies are placed, not tried again for the rest of
|
||||
them: placing actors only takes room, and each enemy would try
|
||||
every taken place again, and every place of a full location) */
|
||||
static struct
|
||||
spread_location = *starting_location;
|
||||
if (coop_enemies_spread_position(&starting_location->position,
|
||||
(short)((extra_number - 1) / squad_definition->starting_locations.count + 1),
|
||||
&spread_location.position))
|
||||
{
|
||||
long encounter_index;
|
||||
long time;
|
||||
short squad_index;
|
||||
unsigned long full;
|
||||
long taken[32][BIT_VECTOR_SIZE_IN_LONGS(COOP_ENEMIES_SPREAD_SPOTS)];
|
||||
} full_locations = { NONE, NONE, NONE, 0 };
|
||||
short count = squad_definition->starting_locations.count;
|
||||
short tried;
|
||||
|
||||
if (extra_number == 1 || full_locations.encounter_index != encounter_index ||
|
||||
full_locations.squad_index != squad_index || full_locations.time != game_time_get())
|
||||
{
|
||||
full_locations.encounter_index = encounter_index;
|
||||
full_locations.squad_index = squad_index;
|
||||
full_locations.time = game_time_get();
|
||||
full_locations.full = 0;
|
||||
csmemset(full_locations.taken, 0, sizeof(full_locations.taken));
|
||||
starting_location = &spread_location;
|
||||
}
|
||||
for (tried = 0; tried < count; tried++)
|
||||
{
|
||||
short candidate_index = (short)((starting_location_index + tried) % count);
|
||||
struct actor_starting_location *candidate = TAG_BLOCK_GET_ELEMENT(&squad_definition->starting_locations,
|
||||
candidate_index, struct actor_starting_location);
|
||||
|
||||
if (candidate_index < 32 && TEST_FLAG(full_locations.full, candidate_index))
|
||||
continue;
|
||||
spread_location = *candidate;
|
||||
if (coop_enemies_spread_position(&candidate->position, extra_number,
|
||||
candidate_index < 32 ? full_locations.taken[candidate_index] : NULL, &spread_location.position))
|
||||
break;
|
||||
if (candidate_index < 32)
|
||||
SET_FLAG(full_locations.full, candidate_index, TRUE);
|
||||
}
|
||||
/* (with no room anywhere, as extra enemies were placed before:
|
||||
on rings around the squad's starting location in turn, else on
|
||||
it) */
|
||||
if (tried == count)
|
||||
{
|
||||
spread_location = *starting_location;
|
||||
coop_enemies_fallback_position(&starting_location->position,
|
||||
(short)((extra_number - 1) / MAX(count, 1) + 1), &spread_location.position);
|
||||
}
|
||||
starting_location = &spread_location;
|
||||
}
|
||||
|
||||
if (starting_location->actor_variant_index != NONE)
|
||||
|
||||
@@ -137,16 +137,6 @@ symbols in this file:
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
/* port: a vertex or index buffer in a cache file (a D3DResource: Common,
|
||||
Data, Lock), as cache_files_disable_writes counts them */
|
||||
CACHE_FILE_BUFFER_SIZE = 12,
|
||||
/* port: what cache_file_read rounds a read's size up to a multiple of
|
||||
(cache_files_windows.c) */
|
||||
CACHE_FILE_SECTOR_SIZE = 512,
|
||||
};
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
#define STRUCTURE_BSP_TAG 'sbsp'
|
||||
@@ -231,18 +221,6 @@ typedef char verify_cache_file_header_size[
|
||||
|
||||
static struct cache_file_tag_instance *cache_get_tag_instance(
|
||||
long tag_index);
|
||||
static boolean cache_file_region_contains(
|
||||
void const *region,
|
||||
unsigned long region_size,
|
||||
void const *address,
|
||||
long count,
|
||||
long element_size);
|
||||
static boolean cache_file_tag_header_verify(
|
||||
struct cache_file_tag_header *tag_header,
|
||||
long tag_data_size,
|
||||
char const *scenario_name);
|
||||
static boolean cache_file_structure_bsp_reference_verify(
|
||||
struct scenario_structure_bsp_reference *reference);
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
@@ -292,156 +270,6 @@ static struct cache_file_tag_instance *cache_get_tag_instance(
|
||||
return tag_instance;
|
||||
}
|
||||
|
||||
/* port: whether count elements of element_size bytes at address all lie in
|
||||
the region_size bytes at region (no elements always do): a map's pointers
|
||||
and counts are checked so before anything follows them */
|
||||
static boolean cache_file_region_contains(
|
||||
void const *region,
|
||||
unsigned long region_size,
|
||||
void const *address,
|
||||
long count,
|
||||
long element_size)
|
||||
{
|
||||
unsigned long offset = (unsigned long)address - (unsigned long)region;
|
||||
|
||||
if (count == 0)
|
||||
return TRUE;
|
||||
|
||||
return count > 0 &&
|
||||
(unsigned long)address >= (unsigned long)region &&
|
||||
offset <= region_size &&
|
||||
(unsigned long)count <= (region_size - offset) / (unsigned long)element_size;
|
||||
}
|
||||
|
||||
/* port: whether the tag header of the tags just read (tag_data_size bytes
|
||||
at the tag cache's base) can be trusted, as everything after trusts it:
|
||||
its tag table and vertex and index buffers lie in the tag data, its tags'
|
||||
count fits a tag index's absolute index, and it names a scenario tag */
|
||||
static boolean cache_file_tag_header_verify(
|
||||
struct cache_file_tag_header *tag_header,
|
||||
long tag_data_size,
|
||||
char const *scenario_name)
|
||||
{
|
||||
char const *problem = NULL;
|
||||
|
||||
if (tag_header->signature != CACHE_FILE_TAG_HEADER_SIGNATURE)
|
||||
{
|
||||
problem = "signature";
|
||||
}
|
||||
else if (tag_header->tag_count <= 0 || tag_header->tag_count > UNSIGNED_SHORT_MAX)
|
||||
{
|
||||
problem = "tag count";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->tag_instances,
|
||||
tag_header->tag_count,
|
||||
sizeof(struct cache_file_tag_instance)))
|
||||
{
|
||||
problem = "tag table";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->vertex_buffers,
|
||||
tag_header->vertex_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
problem = "vertex buffers";
|
||||
}
|
||||
else if (!cache_file_region_contains(
|
||||
tag_header,
|
||||
tag_data_size,
|
||||
tag_header->index_buffers,
|
||||
tag_header->index_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
problem = "index buffers";
|
||||
}
|
||||
else
|
||||
{
|
||||
long scenario_absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(tag_header->scenario_tag_index);
|
||||
|
||||
if (scenario_absolute_index >= tag_header->tag_count ||
|
||||
tag_header->tag_instances[scenario_absolute_index].tag_index != tag_header->scenario_tag_index ||
|
||||
tag_header->tag_instances[scenario_absolute_index].group_tag != SCENARIO_TAG)
|
||||
{
|
||||
problem = "scenario tag";
|
||||
}
|
||||
}
|
||||
|
||||
if (problem)
|
||||
{
|
||||
error(_error_silent, "the cache file '%s' is damaged: its tag header's %s is wrong", scenario_name, problem);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* port: whether a structure bsp reference (the scenario's) may be loaded:
|
||||
its bytes lie in the map and fit the tag cache after the tag data, where
|
||||
they are read to (rounded up to whole sectors, as the read is), and it
|
||||
names a structure bsp tag */
|
||||
static boolean cache_file_structure_bsp_reference_verify(
|
||||
struct scenario_structure_bsp_reference *reference)
|
||||
{
|
||||
byte *tag_cache_base_address = physical_memory_get_tag_cache_base_address();
|
||||
long tag_data_size = cache_file_globals.header.tag_data_size;
|
||||
long absolute_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(reference->structure_bsp.index);
|
||||
long read_size;
|
||||
|
||||
if (reference->file_offset < 0 ||
|
||||
reference->file_size < (long)sizeof(struct cache_file_structure_bsp_header) ||
|
||||
reference->file_size > TAG_CACHE_SIZE ||
|
||||
reference->file_offset > cache_file_globals.header.file_length - reference->file_size)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: %08x bytes at %08x, in %08x bytes",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
cache_file_globals.header.file_length);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
read_size = (reference->file_size + CACHE_FILE_SECTOR_SIZE - 1) & ~(CACHE_FILE_SECTOR_SIZE - 1);
|
||||
if (!cache_file_region_contains(
|
||||
tag_cache_base_address + tag_data_size,
|
||||
TAG_CACHE_SIZE - tag_data_size,
|
||||
reference->base_address,
|
||||
read_size,
|
||||
1))
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: its %08x bytes at %08x would load to %08lx, outside the tag cache",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
(unsigned long)reference->base_address);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (reference->structure_bsp.index == NONE ||
|
||||
absolute_index >= cache_file_globals.tag_header->tag_count ||
|
||||
global_tag_instances[absolute_index].tag_index != reference->structure_bsp.index ||
|
||||
global_tag_instances[absolute_index].group_tag != STRUCTURE_BSP_TAG)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: %08x is not a structure bsp tag",
|
||||
reference->structure_bsp.index);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
char const *cache_files_map_directory(
|
||||
@@ -616,9 +444,7 @@ long tag_loaded(
|
||||
long group_tag,
|
||||
char const *name)
|
||||
{
|
||||
/* port: a long, as the tags' count is (a short wrapped on a count past
|
||||
0x7FFF, and the walk never ended) */
|
||||
long absolute_index;
|
||||
short absolute_index;
|
||||
long result = NONE;
|
||||
|
||||
if (cache_file_globals.tags_loaded)
|
||||
@@ -825,27 +651,6 @@ boolean cache_file_header_verify(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* port: the map holds at least its header, and its tag data lies in it
|
||||
and fits the tag cache, which it is read into whole (a size rounded up
|
||||
to whole sectors still fits, the cache being whole sectors). Checked
|
||||
without overflow: the offset and size are each checked first */
|
||||
if (header->file_length < (long)sizeof(struct cache_file_header) ||
|
||||
header->tag_data_offset < 0 ||
|
||||
header->tag_data_size < 0 ||
|
||||
header->tag_data_size > TAG_CACHE_SIZE ||
|
||||
header->tag_data_offset > header->file_length - header->tag_data_size)
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"the cache file '%s' is damaged: %08x bytes of tag data at %08x, in %08x bytes",
|
||||
scenario_name,
|
||||
header->tag_data_size,
|
||||
header->tag_data_offset,
|
||||
header->file_length);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -1041,26 +846,6 @@ long scenario_tags_load(
|
||||
SwitchToThread();
|
||||
}
|
||||
|
||||
/* port: tags that did not all read, or whose header cannot be
|
||||
trusted, are not loaded: the map is refused, as one whose header
|
||||
is wrong is, and closed for the next to open */
|
||||
if (read_complete != TRUE)
|
||||
{
|
||||
error(_error_silent, "the cache file '%s' could not be read", scenario_name);
|
||||
cache_file_close();
|
||||
|
||||
return NONE;
|
||||
}
|
||||
if (!cache_file_tag_header_verify(
|
||||
tag_cache_base_address,
|
||||
cache_file_globals.header.tag_data_size,
|
||||
scenario_name))
|
||||
{
|
||||
cache_file_close();
|
||||
|
||||
return NONE;
|
||||
}
|
||||
|
||||
cache_file_globals.tag_header = tag_cache_base_address;
|
||||
match_vassert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files.c",
|
||||
@@ -1100,11 +885,6 @@ long scenario_tags_load(
|
||||
}
|
||||
result = cache_file_globals.tag_header->scenario_tag_index;
|
||||
}
|
||||
/* port: a map refused is closed for the next to open */
|
||||
else
|
||||
{
|
||||
cache_file_close();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1115,18 +895,6 @@ boolean scenario_structure_bsp_load(
|
||||
{
|
||||
struct cache_file_tag_instance *tag_instance;
|
||||
byte *tag_cache_base_address;
|
||||
/* port: the bsp's header, once read and checked */
|
||||
struct cache_file_structure_bsp_header *structure_bsp_header;
|
||||
|
||||
/* port: the tag data's size was checked as the map loaded
|
||||
(cache_file_header_verify); the bsp's reference is the map's, and is
|
||||
checked before anything is read where it says */
|
||||
if (cache_file_globals.header.tag_data_size < 0 ||
|
||||
cache_file_globals.header.tag_data_size > TAG_CACHE_SIZE ||
|
||||
!cache_file_structure_bsp_reference_verify(reference))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
tag_cache_base_address = physical_memory_get_tag_cache_base_address();
|
||||
csmemset(
|
||||
@@ -1151,43 +919,9 @@ boolean scenario_structure_bsp_load(
|
||||
sound_idle();
|
||||
}
|
||||
}
|
||||
|
||||
/* port: a bsp that did not all read, or whose header's pointers
|
||||
leave what was read, is not loaded */
|
||||
structure_bsp_header = reference->base_address;
|
||||
if (read_complete != TRUE ||
|
||||
structure_bsp_header->signature != CACHE_FILE_STRUCTURE_BSP_HEADER_SIGNATURE ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->base_address,
|
||||
1,
|
||||
1) ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->vertex_buffers,
|
||||
structure_bsp_header->vertex_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE) ||
|
||||
!cache_file_region_contains(
|
||||
reference->base_address,
|
||||
reference->file_size,
|
||||
structure_bsp_header->index_buffers,
|
||||
structure_bsp_header->index_buffer_count,
|
||||
CACHE_FILE_BUFFER_SIZE))
|
||||
{
|
||||
error(
|
||||
_error_silent,
|
||||
"a structure bsp is damaged: its %08x bytes at %08x %s",
|
||||
reference->file_size,
|
||||
reference->file_offset,
|
||||
read_complete != TRUE ? "could not be read" : "have a wrong header");
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
cache_file_globals.structure_bsp_header = structure_bsp_header;
|
||||
cache_file_globals.structure_bsp_header = reference->base_address;
|
||||
match_assert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files.c",
|
||||
0xE0,
|
||||
|
||||
@@ -22,14 +22,6 @@ enum cached_map_file_precache_state
|
||||
NUMBER_OF_CACHED_MAP_FILE_PRECACHE_STATES,
|
||||
};
|
||||
|
||||
/* port: what a read's completion flag is set to when the read failed or came
|
||||
up short (cache_file_read): not FALSE, so whatever waits on it stops
|
||||
waiting, but not TRUE either, so a caller that checks can tell */
|
||||
enum
|
||||
{
|
||||
_cache_file_read_failed = 2,
|
||||
};
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
/* ---------- structures */
|
||||
@@ -45,10 +37,8 @@ struct cache_file_structure_bsp_header;
|
||||
struct tag_iterator
|
||||
{
|
||||
byte reserved0[4];
|
||||
/* port: a long, as the tags' count is (a short wrapped on a count past
|
||||
0x7FFF, and the walk never ended); the struct keeps its size */
|
||||
long absolute_index;
|
||||
byte reserved8[8];
|
||||
short absolute_index;
|
||||
byte reserved6[10];
|
||||
long group_tag;
|
||||
};
|
||||
|
||||
|
||||
@@ -755,14 +755,6 @@ static void cache_copy_initialize_file_data(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
|
||||
964,
|
||||
self->read_bytes_left>=sizeof(self->header));
|
||||
/* port: a map that did not open, or is too short to hold a header, is a
|
||||
bad file, not copied (the assertion goes on in a release build, and an
|
||||
unopened file's size is -1, which it compared unsigned) */
|
||||
if (self->source_file == INVALID_HANDLE_VALUE ||
|
||||
self->read_bytes_left < (long)sizeof(self->header))
|
||||
{
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
}
|
||||
|
||||
csmemset(self->overlapped_in_use_flags, 0, sizeof(self->overlapped_in_use_flags));
|
||||
csmemset(self->overlapped_completed_flags, 0, sizeof(self->overlapped_completed_flags));
|
||||
@@ -1618,25 +1610,6 @@ static void cache_copy_run_decompression(
|
||||
if (self->write_requests_pending > 1)
|
||||
decompressor_timer_stop(_decompressor_timer_zlib_during_write_file);
|
||||
|
||||
/* port: a stream that ends before the size the header gives the map
|
||||
is a bad file: the rest of the cache file would be taken for it */
|
||||
if (zlib_result == Z_STREAM_END &&
|
||||
zlib_stream->total_out != (uLong)(self->header.size - sizeof(self->header)))
|
||||
{
|
||||
match_vassert(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_decompress_windows.c",
|
||||
1248,
|
||||
FALSE,
|
||||
csprintf(
|
||||
decompressor_globals.message,
|
||||
"decompression ended after %lu of %ld bytes",
|
||||
(unsigned long)zlib_stream->total_out,
|
||||
self->header.size - (long)sizeof(self->header)));
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (zlib_result == Z_OK || zlib_result == Z_STREAM_END)
|
||||
{
|
||||
if (!zlib_stream->avail_in)
|
||||
@@ -1688,22 +1661,14 @@ static unsigned long __stdcall simple_cache_copy_thread(
|
||||
cache_copy_initialize_read_buffers(self);
|
||||
cache_copy_initialize_file_data(self);
|
||||
|
||||
/* port: nor is a bad file (cache_copy_initialize_file_data) read */
|
||||
if (!cache_copy_stop_requested() && !(self->flags & ALL_COPY_FAILURE_FLAGS))
|
||||
if (!cache_copy_stop_requested())
|
||||
{
|
||||
decompressor_timer_start(_decompressor_timer_setup);
|
||||
cache_copy_initialize_read_data(self);
|
||||
cache_copy_initialize_zlib(self);
|
||||
decompressor_timer_stop(_decompressor_timer_setup);
|
||||
|
||||
/* port: a header that is not a map's is a bad file, so the copy
|
||||
fails (it ended as if it had worked, and the map was precached
|
||||
again, or, of no size, seemed never to end) */
|
||||
if (!cache_file_header_verify(&self->header, "cache decompressed", TRUE))
|
||||
{
|
||||
cache_copy_set_flag(_copy_bad_file_bit);
|
||||
}
|
||||
else
|
||||
if (cache_file_header_verify(&self->header, "cache decompressed", TRUE))
|
||||
{
|
||||
boolean keep_going = TRUE;
|
||||
|
||||
|
||||
@@ -427,9 +427,7 @@ static struct cache_file_runtime_globals cache_file_globals;
|
||||
void tags_header_register_vertex_and_index_buffers(
|
||||
struct cache_file_tag_header *header)
|
||||
{
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -452,9 +450,7 @@ void tags_header_register_vertex_and_index_buffers(
|
||||
void tags_header_deregister_vertex_and_index_buffers(
|
||||
struct cache_file_tag_header *header)
|
||||
{
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -484,9 +480,7 @@ void tags_header_deregister_vertex_and_index_buffers(
|
||||
void structure_bsp_header_register_vertex_buffers(
|
||||
struct cache_file_structure_bsp_header *header)
|
||||
{
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < header->vertex_buffer_count; index++)
|
||||
{
|
||||
@@ -510,9 +504,7 @@ void structure_bsp_header_register_vertex_buffers(
|
||||
void structure_bsp_header_deregister_vertex_buffers(
|
||||
struct cache_file_structure_bsp_header *header)
|
||||
{
|
||||
/* port: a long, as the buffers' counts are (a short wrapped on a count
|
||||
past 0x7FFF, and the loop never ended) */
|
||||
long index;
|
||||
short index;
|
||||
|
||||
rasterizer_globals.current_lock_operation = _rasterizer_lock_bsp_switch;
|
||||
|
||||
@@ -598,28 +590,11 @@ boolean cache_files_precache_map_begin(
|
||||
if (cache_file_read_header_from_dvd(cache_map_name, &header))
|
||||
{
|
||||
long buffer_size = cache_copy_buffer_size(copy_map);
|
||||
void *buffer = texture_cache_steal_memory(buffer_size);
|
||||
short map_file_index = cached_map_files_find_free_map(
|
||||
header.file_length,
|
||||
header.scenario_type);
|
||||
void *buffer;
|
||||
struct cached_map_file *map_file;
|
||||
|
||||
/* port: a map no cache file holds (of no type the cache files are
|
||||
for, or too big for its type's) is not precached; the texture
|
||||
cache's memory is taken only once one does */
|
||||
if (map_file_index == NONE)
|
||||
{
|
||||
error(_error_silent, "no cache file can hold map '%s' (%08x bytes, type %d)",
|
||||
cache_map_name, header.file_length, header.scenario_type);
|
||||
if (copy_map)
|
||||
{
|
||||
display_error_damaged_media();
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
buffer = texture_cache_steal_memory(buffer_size);
|
||||
map_file = cached_map_file_get(map_file_index);
|
||||
struct cached_map_file *map_file = cached_map_file_get(map_file_index);
|
||||
|
||||
memset(
|
||||
&map_file->header,
|
||||
@@ -782,14 +757,6 @@ boolean cache_file_open(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
224,
|
||||
map_file_index!=NONE);
|
||||
/* port: a map in no cache file is not opened (its index would be
|
||||
NONE's, before the cache files) */
|
||||
if (map_file_index == NONE)
|
||||
{
|
||||
error(_error_silent, "the map '%s' is in no cache file", scenario_name);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
memset(
|
||||
cache_file_globals.requests,
|
||||
0,
|
||||
@@ -830,16 +797,6 @@ short cache_file_read(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
276,
|
||||
offset>=0);
|
||||
/* port: the offset and size are the map's (a texture's or sound's,
|
||||
which nothing checks): a negative one is no read, failed at once (a
|
||||
negative size read the file to its end over what follows the buffer) */
|
||||
if (offset < 0 || size < 0)
|
||||
{
|
||||
error(_error_silent, "cache file read of %08x bytes at %08x refused", size, offset);
|
||||
*completion_flag_reference = _cache_file_read_failed;
|
||||
|
||||
return NONE;
|
||||
}
|
||||
if (size & (CACHE_FILE_SECTOR_SIZE - 1))
|
||||
{
|
||||
size = (size | (CACHE_FILE_SECTOR_SIZE - 1)) + 1;
|
||||
@@ -1099,13 +1056,7 @@ static void CALLBACK cache_file_read_io_completion_routine(
|
||||
"c:\\halo\\SOURCE\\cache\\cache_files_windows.c",
|
||||
1389,
|
||||
finished_request->overlapped.hEvent);
|
||||
/* port: a read that failed or came up short completes as failed: what
|
||||
waits on it stops waiting, and a caller that checks (the map's tags and
|
||||
bsps) sees it, instead of taking what is in its buffer for the map */
|
||||
*(volatile boolean *)finished_request->overlapped.hEvent =
|
||||
error_code == ERROR_SUCCESS && bytes_transferred == (unsigned long)finished_request->size ?
|
||||
TRUE :
|
||||
_cache_file_read_failed;
|
||||
*(volatile boolean *)finished_request->overlapped.hEvent = TRUE;
|
||||
finished_request->pending = FALSE;
|
||||
finished_request->running = FALSE;
|
||||
|
||||
@@ -1271,11 +1222,9 @@ static short cached_map_files_find_free_map(
|
||||
* arm's assertion failure calls system_exit, which does not return in January
|
||||
* (0x47c960 jumps to halt_and_catch_fire 0x4f21c0, which loops or calls exit).
|
||||
* Source-policy approval pending (2026-09-27 audit). */
|
||||
/* port: the type is the map's header's, and a release build's assertion
|
||||
goes on: a type no cache file is for finds none */
|
||||
default:
|
||||
match_vassert("c:\\halo\\SOURCE\\cache\\cache_files_windows.c", 1172, FALSE, NULL);
|
||||
return NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
for (map_file_index = first_map_file_index;
|
||||
|
||||
@@ -46,6 +46,7 @@ symbols in this file:
|
||||
#define GAME_STATE_SIZE HALO_PORT_GAME_STATE_SIZE
|
||||
#define GAME_STATE_VERIFY_SIZE HALO_PORT_GAME_STATE_CPU_SIZE
|
||||
#define TAG_CACHE_BASE_ADDRESS 0x803A6000
|
||||
#define TAG_CACHE_SIZE 0x1600000
|
||||
#define TEXTURE_CACHE_SIZE 0x1600000
|
||||
#define SOUND_CACHE_SIZE 0x400000
|
||||
|
||||
|
||||
@@ -10,10 +10,6 @@ header included in hcex build.
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* port: the tag cache's size, which the loader checks a map's tag data and
|
||||
structure bsps against (cache_files.c) */
|
||||
#define TAG_CACHE_SIZE 0x1600000
|
||||
|
||||
/* ---------- macros */
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||