Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a950de4b2b | ||
|
|
1984996d04 | ||
|
|
93b96cf5b8 | ||
|
|
f68db3b03e | ||
|
|
bd08baacac | ||
|
|
f39126e019 | ||
|
|
f649bd1a3e | ||
|
|
719563a48c | ||
|
|
32cc1cb206 | ||
|
|
d4c1fbe9a7 | ||
|
|
adf220d344 | ||
|
|
32ad9ed6b0 | ||
|
|
cf19bcfdfa | ||
|
|
3b5796f1a0 | ||
|
|
68135a712d | ||
|
|
1f238ff045 | ||
|
|
88354e5c6c | ||
|
|
8cdce233e4 | ||
|
|
81137e3af3 | ||
|
|
fb670e79b9 | ||
|
|
8022a0b4a1 | ||
|
|
696383f191 | ||
|
|
9a33ecd2d3 | ||
|
|
9447bc2ae4 | ||
|
|
f04c8c4045 | ||
|
|
b026590824 | ||
|
|
2a2b2cdbf3 | ||
|
|
5861e57d99 | ||
|
|
50d685e465 | ||
|
|
0e6b006878 | ||
|
|
ca73ba7909 | ||
|
|
c254d6df05 | ||
|
|
9910c4a9dc | ||
|
|
b1d59e654d | ||
|
|
1f3d1be207 | ||
|
|
d97de6d499 | ||
|
|
fbe48a869f | ||
|
|
206eb0b52b | ||
|
|
1c66715a19 | ||
|
|
87f336df78 | ||
|
|
0a86e406c8 | ||
|
|
c09b40a2ef | ||
|
|
1e2006d06c | ||
|
|
e69e087602 | ||
|
|
87a38e2895 | ||
|
|
788ab74a6b | ||
|
|
6d2dcfaaec | ||
|
|
98e848de39 | ||
|
|
7754532683 | ||
|
|
19b0bcb42e | ||
|
|
f30dd50317 | ||
|
|
07a15190b4 | ||
|
|
f254be4b45 | ||
|
|
a6350d207a | ||
|
|
1929702f5b | ||
|
|
9d6b980ba6 | ||
|
|
9e4f8a2dcb | ||
|
|
53c268b8a6 | ||
|
|
592be6270a | ||
|
|
8817c37470 | ||
|
|
d635d83725 | ||
|
|
2a6dc2b738 | ||
|
|
6112dcfc5a | ||
|
|
62fa7e13ef | ||
|
|
9a12828762 | ||
|
|
7e82e7128e | ||
|
|
94882796b7 | ||
|
|
3dba558e4f | ||
|
|
1dc533fe18 | ||
|
|
8a8e70599f | ||
|
|
b115a41263 | ||
|
|
320464258a | ||
|
|
f5bb75c27c | ||
|
|
5cd1daff77 | ||
|
|
6329be8aa1 | ||
|
|
7a2f4caa50 | ||
|
|
938c37a669 | ||
|
|
fae3fbad69 | ||
|
|
5a35ea2b20 | ||
|
|
2e179ea90f | ||
|
|
9b517e554c | ||
|
|
595bbc5611 | ||
|
|
761210d405 | ||
|
|
7ba780ef59 | ||
|
|
d9c43e3478 | ||
|
|
c04ce7fd00 | ||
|
|
3f06b1b9d8 | ||
|
|
1b2d058a04 | ||
|
|
b5fa53ed95 | ||
|
|
1f2c10a3ca | ||
|
|
12f1155752 | ||
|
|
2b0327bc80 | ||
|
|
58aa352705 | ||
|
|
59c4b2160e | ||
|
|
1cf336f0ff | ||
|
|
d10969dc83 | ||
|
|
fe1d51dd58 | ||
|
|
24e4123882 | ||
|
|
197c199473 | ||
|
|
0acc3b1381 | ||
|
|
1e74e9b03e | ||
|
|
5d108579ed | ||
|
|
7a1ffca28b | ||
|
|
b843156fe8 | ||
|
|
99a31ca82d | ||
|
|
9666575b39 | ||
|
|
8466846d45 | ||
|
|
bc580cf1cf | ||
|
|
8e6b9a211e | ||
|
|
d0016bca89 | ||
|
|
3d575f32ed | ||
|
|
727b9ca80d | ||
|
|
99d7c924db | ||
|
|
57d96e4681 | ||
|
|
177ad4d40d | ||
|
|
bfb668d498 | ||
|
|
fc2b6551ca | ||
|
|
5316a6b145 | ||
|
|
59b1fb422b | ||
|
|
dc383d10da | ||
|
|
a143d9490d | ||
|
|
e55c29a97e | ||
|
|
16361bdef1 | ||
|
|
5ecd3075a0 | ||
|
|
13c14df95c | ||
|
|
b525bf8514 | ||
|
|
087ca8a875 | ||
|
|
24957ff6a0 | ||
|
|
402d4b8351 | ||
|
|
d1825ab08d | ||
|
|
268ec84b74 | ||
|
|
8dc796028e | ||
|
|
8aa381ed14 | ||
|
|
c3f82fa9b2 | ||
|
|
5ff4b6ee8d | ||
|
|
91e838e2f6 | ||
|
|
520b8401db | ||
|
|
2ed4a7b49d | ||
|
|
503992d544 | ||
|
|
ccc7df6611 | ||
|
|
ddcbe79283 | ||
|
|
862d88737e | ||
|
|
8610b28376 | ||
|
|
9e6247e0cb | ||
|
|
dea5fe3b1e | ||
|
|
f0804aa587 | ||
|
|
16ad4cc900 | ||
|
|
a38ede078a | ||
|
|
fa85aa53f5 | ||
|
|
8be9a038a1 | ||
|
|
783b591a6a | ||
|
|
e82d880060 | ||
|
|
7d0e587c51 | ||
|
|
01586cfebf | ||
|
|
de52060644 | ||
|
|
ab7c507a29 | ||
|
|
ba6fe0a826 | ||
|
|
3f6c0c37a4 | ||
|
|
606109551d | ||
|
|
8194c5a00b | ||
|
|
737551fd51 | ||
|
|
7a56c624f5 | ||
|
|
3ae09c3d2c | ||
|
|
3a22816c2f | ||
|
|
2853a42253 | ||
|
|
39ece781ee | ||
|
|
cf17201ec8 | ||
|
|
45de17116b | ||
|
|
7d56f34b83 | ||
|
|
3c7ba95ff5 | ||
|
|
092505224a | ||
|
|
079883d640 | ||
|
|
54faa1dab6 | ||
|
|
273152cc25 | ||
|
|
9bdd7f30c0 | ||
|
|
685bf260a0 | ||
|
|
933ed2b7b8 | ||
|
|
c115f5292d | ||
|
|
0f22c1d592 | ||
|
|
fb1abedd8b | ||
|
|
cd345aac7c | ||
|
|
7e00135d31 | ||
|
|
95416b2cfc | ||
|
|
ffc1512dcb | ||
|
|
88a7c07ec7 | ||
|
|
8e8713681d | ||
|
|
d7099f3e40 | ||
|
|
4ba36d7305 | ||
|
|
e3389991ac | ||
|
|
855f083117 | ||
|
|
6f3786de91 | ||
|
|
493713b0a6 | ||
|
|
bf356fc58a | ||
|
|
6a7b919522 | ||
|
|
20861c06d2 | ||
|
|
fc7271fd52 | ||
|
|
c1305fa94f | ||
|
|
3d2c04d675 | ||
|
|
0fc85d6c67 | ||
|
|
c0a08f395e | ||
|
|
06cb70fb58 | ||
|
|
1709698053 | ||
|
|
cb72dca40f | ||
|
|
bbea2a2bff | ||
|
|
57cdbd8f80 | ||
|
|
c5c5a8eaeb | ||
|
|
a3ee36b5ef | ||
|
|
30a1e0f40e | ||
|
|
327cf96d6f | ||
|
|
867855d388 | ||
|
|
9e7fd75f57 | ||
|
|
6754403d68 | ||
|
|
aedf5ff9bc | ||
|
|
193cbf59c7 | ||
|
|
fe160e9dbe | ||
|
|
d578f88bd0 | ||
|
|
d3fc997fdc | ||
|
|
a227872ccb | ||
|
|
812ffeea33 | ||
|
|
c05864b3fc | ||
|
|
ce77db8404 | ||
|
|
39350fb81f | ||
|
|
1a15a171ac | ||
|
|
2e3841ff7d | ||
|
|
46edf53657 | ||
|
|
cd747c23c5 | ||
|
|
f148dbd6d9 | ||
|
|
c3adcfe5bf | ||
|
|
3304965653 | ||
|
|
c04765d7f4 | ||
|
|
933aac6175 | ||
|
|
809408c663 | ||
|
|
601a4c1f50 | ||
|
|
133d6a5dc1 | ||
|
|
12c97ffa85 | ||
|
|
61623e68d5 | ||
|
|
23b542601f | ||
|
|
62b630a261 | ||
|
|
0e6327cadf | ||
|
|
5f82df71f0 | ||
|
|
e8e0c2215a | ||
|
|
383355381c | ||
|
|
2b0103a0ef | ||
|
|
e44619818b | ||
|
|
b449c43e22 | ||
|
|
991ab41016 | ||
|
|
5f9748d736 | ||
|
|
56b2a0e8e3 | ||
|
|
5f388f7818 | ||
|
|
557c0b486c |
No files matched your search
@@ -0,0 +1,38 @@
|
||||
name: Problem report
|
||||
description: Something does not work as it should
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
|
||||
- type: dropdown
|
||||
id: headset
|
||||
attributes:
|
||||
label: Headset
|
||||
options:
|
||||
- Meta Quest 3
|
||||
- Meta Quest 3S
|
||||
- Steam Frame
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: build
|
||||
attributes:
|
||||
label: Build number
|
||||
description: The release you installed, e.g. "build 180".
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What you did, what you expected and what happened instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log
|
||||
description: |
|
||||
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: How to install
|
||||
url: https://github.com/Cesarus85/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
|
||||
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
|
||||
@@ -67,7 +67,9 @@ jobs:
|
||||
run: python tools/ci_build.py linux release
|
||||
|
||||
- name: Test
|
||||
run: python -m pytest -q tools/test_linux_port.py
|
||||
run: |
|
||||
python -m pytest -q tools/test_linux_port.py
|
||||
python tools/test_light_storage.py
|
||||
|
||||
- name: Compile cache
|
||||
run: ccache --show-stats
|
||||
@@ -200,6 +202,9 @@ 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
|
||||
|
||||
@@ -232,6 +237,13 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
port/android
|
||||
port/third_party/extract-xiso
|
||||
tools
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: halo-*
|
||||
@@ -240,13 +252,18 @@ jobs:
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir release
|
||||
for build in artifacts/*/; do
|
||||
name="$(basename "$build")"
|
||||
# stored, not deflated: the updater in builds up to 57 waits for
|
||||
# an end of stream the game's zlib 1.1 does not always report,
|
||||
# and fails on a deflated entry that unpacked correctly
|
||||
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||||
# (Ringfall: Mjolnir VR publishes its Android builds only, under
|
||||
# its own name: Updater.java looks for them; the desktop ports are
|
||||
# built to keep them building)
|
||||
for config in debug release; do
|
||||
(cd "artifacts/halo-android-$config" &&
|
||||
zip -qr "$GITHUB_WORKSPACE/release/ringfall-mjolnir-vr-android-$config.zip" .)
|
||||
done
|
||||
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
|
||||
# bundle with the release APK, and the desktop file that fetches it
|
||||
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
|
||||
# the APK itself, for sideloading on the Quest (SideQuest, adb)
|
||||
cp artifacts/halo-android-release/app-release.apk release/RingfallMjolnirVR.apk
|
||||
ls -l release
|
||||
|
||||
- name: Publish
|
||||
@@ -258,10 +275,18 @@ jobs:
|
||||
run: |
|
||||
tag="build-$GITHUB_RUN_NUMBER"
|
||||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
|
||||
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
|
||||
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
|
||||
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
|
||||
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
||||
{
|
||||
printf '%s\n\n' "$subject"
|
||||
printf '**Ringfall: Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
|
||||
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
|
||||
printf '| Meta Quest 3 / 3S | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
|
||||
printf '| Steam Frame | `Install-Ringfall-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
|
||||
printf 'The other files are for the built-in updater and the Steam Frame installer.\n\n'
|
||||
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
|
||||
} > notes.md
|
||||
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
|
||||
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Keep the last five
|
||||
@@ -316,7 +341,7 @@ jobs:
|
||||
title="Build failed"; color=15548997
|
||||
fi
|
||||
latest="$SERVER/$REPOSITORY/releases/latest"
|
||||
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
|
||||
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||||
else
|
||||
|
||||
@@ -30,3 +30,8 @@ original/
|
||||
|
||||
# CI build outputs (tools/ci_build.py)
|
||||
dist/
|
||||
|
||||
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
|
||||
port/android/signing.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
@@ -1,140 +1,181 @@
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
<p align="center">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
|
||||
</p>
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
# Ringfall: Mjolnir VR
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
|
||||
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
|
||||
multiplayer in full 6DoF virtual reality, with your hands in the game.
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
Ringfall: Mjolnir VR runs on your headset by itself, without a PC. It
|
||||
brings no game files: it uses a disc image of **your own, purchased copy**
|
||||
of the game.
|
||||
|
||||
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
|
||||
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
> **Unofficial fan project.** Ringfall: Mjolnir VR is a free,
|
||||
> non-commercial fan project. It is not affiliated with, endorsed,
|
||||
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
|
||||
> Bungie. See [Legal](#legal).
|
||||
|
||||
## Download
|
||||
## Contents
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
- [What you get](#what-you-get)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
|
||||
- [Install on Steam Frame](#install-on-steam-frame)
|
||||
- [Playing](#playing)
|
||||
- [Problems and questions](#problems-and-questions)
|
||||
- [Credits](#credits)
|
||||
- [Legal](#legal)
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/cybersecurity/halo-ce-universal/releases/latest/download/halo-android-debug.zip) |
|
||||
## What you get
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
problems.
|
||||
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
|
||||
the HUD, menus, scope and crosshair float as layers of their own.
|
||||
- **Your hands in the game**: the weapon sits in your hand and fires
|
||||
where you point it; arms and fingers follow your controllers;
|
||||
two-handed weapons, melee by swinging, grenades thrown with your hand,
|
||||
crouching and leaning with your body.
|
||||
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
|
||||
turrets with handles.
|
||||
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
|
||||
left-handed mode, buttons remapped freely, graphics presets, refresh
|
||||
rate and resolution, all in the in-game VR options (**Y**).
|
||||
- **Multiplayer**: the PC version's menus and server browser, join codes,
|
||||
password lobbies, co-op campaign and bots, up to 128 players;
|
||||
cross-play with the PC and phone builds of
|
||||
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
|
||||
|
||||
The game updates itself. At start-up it looks for a newer release, and asks
|
||||
if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](port/linux/README.md#updates).
|
||||
## What you need
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/cybersecurity/halo-ce-universal/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
|
||||
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
|
||||
original Xbox**, made from **a disc you own**. Any region and version
|
||||
works.
|
||||
|
||||
## Game data
|
||||
This project does not include, link to or help find game files. Please
|
||||
do not ask for or share disc images in issues or discussions; such posts
|
||||
are deleted.
|
||||
|
||||
The port does not include the game data. Download an Xbox disc image
|
||||
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
## Install on Meta Quest 3 / 3S
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
You install the game once with a computer (Windows, macOS or Linux); after
|
||||
that it updates itself.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](port/android/README.md).
|
||||
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
|
||||
app on your phone, open your headset's settings, then *Developer
|
||||
Mode* (Meta asks you to create a free developer account first).
|
||||
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
|
||||
"Advanced Installer") on your computer and connect the Quest with a
|
||||
USB cable. In the headset, allow USB debugging for this computer.
|
||||
3. **Download `RingfallMjolnirVR.apk`** from the
|
||||
[latest release](../../releases/latest) and drag it onto the SideQuest
|
||||
window. It installs the game.
|
||||
4. **Copy your disc image** to the Quest's `Download` folder: in
|
||||
SideQuest, open the file manager (the folder icon), go to `Download`
|
||||
and drag the `.iso` file in.
|
||||
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
|
||||
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
|
||||
your file in `Download`. The game copies what it needs (about 1.8 GB,
|
||||
a minute or two) and starts.
|
||||
|
||||
## Platforms
|
||||
You can delete the disc image from `Download` afterwards. When a new
|
||||
version comes out, the game asks you at start whether to update.
|
||||
|
||||
Each platform has its own instructions:
|
||||
## Install on Steam Frame
|
||||
|
||||
| Platform | Instructions |
|
||||
| --- | --- |
|
||||
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
|
||||
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
|
||||
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
|
||||
No computer and no keyboard needed.
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
|
||||
*Switch to Desktop*.
|
||||
2. **Put your disc image** in the `Downloads` folder (for example from a
|
||||
USB stick or memory card with the file manager, Dolphin).
|
||||
3. **Download `Install-Ringfall-Mjolnir-VR.desktop`** from the
|
||||
[latest release](../../releases/latest) with the web browser.
|
||||
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
|
||||
If you are asked whether to run it, choose to continue.
|
||||
5. **Answer the dialogs.** The installer finds your disc image, copies
|
||||
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
|
||||
library and offers to switch back to Gaming Mode.
|
||||
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
|
||||
|
||||
## Multiplayer
|
||||
To update, open the same file in Desktop Mode again; your settings,
|
||||
saves and game data stay. More about the Steam Frame:
|
||||
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
## Playing
|
||||
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](port/linux/NETCODE.md).
|
||||
- **Y** opens the VR options in the main and pause menus: turning,
|
||||
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
|
||||
*Quit the game* is on its last page.
|
||||
- In the menus, point with a controller and pull the trigger to click.
|
||||
- The controller layout is on the VR options' *Controller buttons* page.
|
||||
|
||||
## Build the game
|
||||
## Problems and questions
|
||||
|
||||
You do not need the Xbox SDK. The port supplies the SDK declarations that
|
||||
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
|
||||
Please open an [issue](../../issues) and include:
|
||||
|
||||
To build the game:
|
||||
- your headset and the build number of the release you installed;
|
||||
- what you did and what happened;
|
||||
- the game's log, `debug.txt`:
|
||||
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
|
||||
(SideQuest's file manager);
|
||||
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
|
||||
`logcat.txt` and `install.log` beside it).
|
||||
|
||||
1. Install Python and [ninja](https://ninja-build.org/).
|
||||
2. Install the tools for your platform. Refer to the README for the
|
||||
platform.
|
||||
3. In the root folder of the repository, enter `python configure.py`.
|
||||
4. Enter `ninja` with the target for the platform:
|
||||
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
|
||||
|
||||
| Target | Result |
|
||||
| --- | --- |
|
||||
| `ninja linux` | `build/linux/halo` |
|
||||
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
**Network use.** The game connects to the internet only for what you
|
||||
use: the update check (GitHub), online games (to other players
|
||||
directly), and join codes and lobby lists through public MQTT brokers
|
||||
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
|
||||
telemetry.
|
||||
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
## Credits
|
||||
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
Ringfall: Mjolnir VR stands on the work of others:
|
||||
|
||||
### Build options
|
||||
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
|
||||
Community Edition): the port of the decompiled game to Linux, Windows
|
||||
and Android, with its renderer, netcode, PC menus and multiplayer, that
|
||||
this project builds on and follows.
|
||||
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
|
||||
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
|
||||
by moistman42069 (CC0): the arm IK, the forearm's twist and the
|
||||
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
|
||||
left-handed mode and the graphics presets follow its ideas.
|
||||
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
|
||||
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
|
||||
trace markers) are used here.
|
||||
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
|
||||
image readers follow its format handling.
|
||||
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
Give these options to `configure.py`:
|
||||
Thank you all.
|
||||
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
## Building it
|
||||
|
||||
Without `--portable`, the Linux and Windows builds use all the instructions
|
||||
of the processor that builds them (`-march=native`). Such a build does not
|
||||
always start on a different computer.
|
||||
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
|
||||
and NDK needed). See [port/android/README.md](port/android/README.md) for
|
||||
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
|
||||
port it is built on.
|
||||
|
||||
### Optimization profiles
|
||||
## Legal
|
||||
|
||||
The builds use profiles of the game to optimize the code:
|
||||
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
|
||||
It is not affiliated with, endorsed, sponsored or approved by Microsoft
|
||||
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
|
||||
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
|
||||
here only to say which game this mod works with. Meta Quest is a trademark
|
||||
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
|
||||
Corporation.
|
||||
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
No game data, art, music or other content of the game is included or
|
||||
distributed. You need your own, legally purchased copy of the game. If
|
||||
you enjoy this mod, please support the creators of the original by buying
|
||||
their games.
|
||||
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
This project's own code is released under [CC0 1.0](LICENSE.md), as the
|
||||
decompilation it builds on. Third-party code and fonts keep their own
|
||||
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
@@ -0,0 +1,46 @@
|
||||
# Third-party notices
|
||||
|
||||
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
|
||||
It includes or builds on the following, under their own licenses; each
|
||||
license's full text is in the file named.
|
||||
|
||||
## Code
|
||||
|
||||
| Component | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | CC0 1.0 | [LICENSE.md](LICENSE.md) |
|
||||
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
|
||||
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
|
||||
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
|
||||
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
|
||||
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
|
||||
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
|
||||
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
|
||||
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
|
||||
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
|
||||
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
|
||||
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
|
||||
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
|
||||
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
|
||||
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
|
||||
|
||||
The disc image readers include this acknowledgement, as extract-xiso's
|
||||
license asks: *This product includes software developed by in
|
||||
<in@fishtank.com>.*
|
||||
|
||||
## Fonts
|
||||
|
||||
| Font | Used for | License | Text |
|
||||
| --- | --- | --- | --- |
|
||||
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
|
||||
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
|
||||
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
|
||||
|
||||
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
|
||||
each font is used.
|
||||
|
||||
## Not included
|
||||
|
||||
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
|
||||
executables) is part of this repository or its releases. The game reads
|
||||
them from the player's own disc image.
|
||||
@@ -0,0 +1,145 @@
|
||||
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
|
||||
> the port Ringfall: Mjolnir VR is built on, kept for its build and
|
||||
> development notes. Its download links are OpenCE's. For the VR mod, see
|
||||
> the [main README](../README.md).
|
||||
|
||||
# Halo: Combat Evolved for Linux, Windows and Android
|
||||
|
||||
[](https://discord.gg/9gqcHyr5km)
|
||||
|
||||
This project is a port of the Halo: Combat Evolved decompilation to Linux,
|
||||
Windows and Android. The decompilation is of the Xbox build 2342
|
||||
(`cachebeta.exe`, SHA-256
|
||||
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
|
||||
|
||||
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
|
||||
|
||||
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
|
||||
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
|
||||
|
||||
## Download
|
||||
|
||||
GitHub Actions builds the game for each commit. These links download the
|
||||
builds of the latest release:
|
||||
|
||||
| Platform | Release | Debug |
|
||||
| --- | --- | --- |
|
||||
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
|
||||
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
|
||||
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
|
||||
|
||||
Use the release build to play. The debug build stops at the first failed
|
||||
assertion and writes it to the log. Use the debug build to find and report
|
||||
problems.
|
||||
|
||||
The game updates itself. At start-up it looks for a newer release, and asks
|
||||
if you want to install it. Refer to "Updates" in
|
||||
[port/linux/README.md](../port/linux/README.md#updates).
|
||||
|
||||
Each build of the `main` branch that passes on all three platforms is a new
|
||||
release. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
|
||||
page keeps the last five releases. If the latest build has a problem, get
|
||||
an older build from that page.
|
||||
|
||||
## Game data
|
||||
|
||||
The port does not include the game data. It needs an Xbox disc image
|
||||
(`.xiso` or `.iso`) made from your own copy of Halo: Combat Evolved. All versions of the game
|
||||
operate. The maps of the European (PAL) version were made for a slower
|
||||
console. The port changes them to play as the North American (NTSC) maps do,
|
||||
so players of the two versions can play together.
|
||||
|
||||
1. Start the game.
|
||||
2. At the first start, the game asks for the disc image. Select it.
|
||||
3. The game extracts the `maps/` folder. Then the game starts.
|
||||
|
||||
On Linux and Windows, the game puts `maps/` next to the executable. On
|
||||
Android, copy the disc image to the phone first. The app puts `maps/` in its
|
||||
data folder. Refer to [port/android/README.md](../port/android/README.md).
|
||||
|
||||
## Platforms
|
||||
|
||||
Each platform has its own instructions:
|
||||
|
||||
| Platform | Instructions |
|
||||
| --- | --- |
|
||||
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](../port/linux/README.md) |
|
||||
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](../port/windows/README.md) |
|
||||
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](../port/android/README.md) |
|
||||
|
||||
The Linux README also gives the controls, the settings and the multiplayer
|
||||
functions. These are almost the same on all platforms.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
The game can play system link games on a local network and on the internet:
|
||||
|
||||
- A system link game can have up to 128 players on up to 128 machines.
|
||||
- Linux, Windows and Android machines can play in the same game.
|
||||
- An invite link lets a machine join a game on the internet. No server of
|
||||
this project is necessary.
|
||||
- The netcode is new. Each machine moves its own player at once,
|
||||
and the host makes the decisions for the game. Refer to
|
||||
[port/linux/NETCODE.md](../port/linux/NETCODE.md).
|
||||
|
||||
## Build the game
|
||||
|
||||
You do not need the Xbox SDK. The port supplies the SDK declarations that
|
||||
the game uses. Refer to [port/include/xdk](../port/include/xdk/README.md).
|
||||
|
||||
To build the game:
|
||||
|
||||
1. Install Python and [ninja](https://ninja-build.org/).
|
||||
2. Install the tools for your platform. Refer to the README for the
|
||||
platform.
|
||||
3. In the root folder of the repository, enter `python configure.py`.
|
||||
4. Enter `ninja` with the target for the platform:
|
||||
|
||||
| Target | Result |
|
||||
| --- | --- |
|
||||
| `ninja linux` | `build/linux/halo` |
|
||||
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
|
||||
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
|
||||
|
||||
If you enter `ninja` without a target, ninja builds the game for the
|
||||
computer that you use.
|
||||
|
||||
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
|
||||
enter `python tools/ci_build.py linux release`.
|
||||
|
||||
### Build options
|
||||
|
||||
Give these options to `configure.py`:
|
||||
|
||||
| Option | Result |
|
||||
| --- | --- |
|
||||
| (none) | A debug build. A failed assertion stops the game. |
|
||||
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
|
||||
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
|
||||
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
|
||||
| `--pgo=off` | No profile-guided optimization. |
|
||||
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
|
||||
|
||||
Without `--portable`, the Linux and Windows builds use all the instructions
|
||||
of the processor that builds them (`-march=native`). Such a build does not
|
||||
always start on a different computer.
|
||||
|
||||
### Optimization profiles
|
||||
|
||||
The builds use profiles of the game to optimize the code:
|
||||
|
||||
- `pgo/halo_linux.profdata` for Linux and Android.
|
||||
- `pgo/halo_windows.profdata` for Windows.
|
||||
|
||||
The profiles need clang 22 or later. With an older clang, the builds do not
|
||||
use the profiles.
|
||||
|
||||
To record a new profile:
|
||||
|
||||
1. Delete the profile.
|
||||
2. Enter `python configure.py --pgo=train`.
|
||||
3. Enter `ninja linux` or `ninja windows`.
|
||||
|
||||
The build then plays the main menu and the first minute of each campaign
|
||||
level. This procedure continues for approximately 15 minutes. The game
|
||||
data must be in `assets/`.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Android
|
||||
|
||||
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
|
||||
its Java classes keep the decompilation's package, `com.halo.decomp`).
|
||||
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
|
||||
`ninja android_apk` makes an app from it:
|
||||
`port/android/app/build/outputs/apk/debug/app-debug.apk`.
|
||||
@@ -41,8 +43,8 @@ ninja) and these items:
|
||||
## Game data
|
||||
|
||||
The game needs the `maps/` folder from an Xbox disc image (`.xiso` or
|
||||
`.iso`) of any version of the game. The app extracts `maps/` from the disc
|
||||
image. The app keeps the data in `/sdcard/Android/data/com.halo.decomp/files`.
|
||||
`.iso`) made from your own copy of the game, any version. The app extracts `maps/` from the disc
|
||||
image. The app keeps the data in `/sdcard/Android/data/io.github.cesarus85.ringfall/files`.
|
||||
|
||||
To install the data with the app:
|
||||
|
||||
@@ -57,16 +59,16 @@ To install the data with the app:
|
||||
To install the data from a computer:
|
||||
|
||||
1. Start the app one time. The app makes its folders.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
2. Enter `adb push <folder>/. /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
|
||||
| Item | Location in `/sdcard/Android/data/com.halo.decomp/files` |
|
||||
| Item | Location in `/sdcard/Android/data/io.github.cesarus85.ringfall/files` |
|
||||
| --- | --- |
|
||||
| Saved games (`z:\` and `u:\`) | `save` |
|
||||
| Log | `debug.txt` |
|
||||
| Settings | `config.toml` |
|
||||
|
||||
To make a copy of the saved games, enter
|
||||
`adb pull /sdcard/Android/data/com.halo.decomp/files/save`.
|
||||
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
|
||||
|
||||
## Controls
|
||||
|
||||
@@ -100,9 +102,9 @@ not accept touch input.
|
||||
The settings are in `config.toml` in the data folder of the app. To change
|
||||
them:
|
||||
|
||||
1. Enter `adb pull /sdcard/Android/data/com.halo.decomp/files/config.toml`.
|
||||
1. Enter `adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/config.toml`.
|
||||
2. Change the file.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/com.halo.decomp/files/`.
|
||||
3. Enter `adb push config.toml /sdcard/Android/data/io.github.cesarus85.ringfall/files/`.
|
||||
|
||||
At the first start, the game writes the file with the default values. To
|
||||
get the default values again, delete the file.
|
||||
@@ -148,14 +150,22 @@ select "Yes":
|
||||
|
||||
1. The app downloads the new version.
|
||||
2. The package installer of Android opens. At the first update, Android asks
|
||||
you to let Halo install apps. Allow it.
|
||||
you to let the game install apps. Allow it.
|
||||
3. Select "Update". Android replaces the app.
|
||||
4. Select "Open" to start the new version.
|
||||
|
||||
To install over the previous version, each build must have the same
|
||||
signature. GitHub Actions signs each build with the key in the
|
||||
`ANDROID_KEYSTORE_BASE64` and `ANDROID_KEYSTORE_PASSWORD` secrets of the
|
||||
repository. If you installed a build that has a different signature, remove
|
||||
repository. A computer that builds for its own devices signs with the same
|
||||
key when `port/android/signing.properties` (not in the repository) names it:
|
||||
|
||||
storeFile=/path/to/ringfall-release.jks
|
||||
password=<the keystore's password>
|
||||
keyAlias=ringfall
|
||||
|
||||
Keep the keystore and its password safe: a lost key means every player must
|
||||
remove the app before the next version. If you installed a build that has a different signature, remove
|
||||
that build before you install a new build. Removing the app deletes its data
|
||||
folder: first make a copy of `maps/` and `save/`.
|
||||
|
||||
@@ -182,7 +192,7 @@ instead.
|
||||
|
||||
Controls in play (in the menus the buttons are the Xbox's by position):
|
||||
|
||||
| Controller | Halo |
|
||||
| Controller | Game |
|
||||
| --- | --- |
|
||||
| right trigger / left trigger | fire / throw grenade |
|
||||
| right grip / left grip | melee / switch weapon |
|
||||
@@ -230,7 +240,7 @@ through a shader that decodes to linear, and the stored bytes are the
|
||||
game's. Camera shake is off in VR.
|
||||
|
||||
A file named `xr_test` in the data folder
|
||||
(`adb shell touch /sdcard/Android/data/com.halo.decomp/files/xr_test`)
|
||||
(`adb shell touch /sdcard/Android/data/io.github.cesarus85.ringfall/files/xr_test`)
|
||||
starts a self-test instead of the game, which needs no game data: each eye
|
||||
in its own colour (red while the right trigger is held), a checkerboard
|
||||
screen in the room (its top-left square red), and the controllers' buttons
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Steam Frame
|
||||
|
||||
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
What differs is found at run time, not by a build of its own.
|
||||
|
||||
## Installing it (players)
|
||||
|
||||
On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
|
||||
1. Put your own disc image of Halo: Combat Evolved for the original Xbox (`.iso` or
|
||||
`.xiso`, any version) in Downloads (or Documents, the Desktop, a memory
|
||||
card).
|
||||
2. Download `Install-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
|
||||
Development" from the library. While the headset sleeps Lepton stops, and
|
||||
adb with it.
|
||||
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
|
||||
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
|
||||
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
|
||||
connected as well.
|
||||
- Install with `adb install -r --no-streaming`. It prints "Lepton post
|
||||
installation failed due to APP_PATH being unset", but installs. Start the
|
||||
game with `am start -n io.github.cesarus85.ringfall/com.halo.decomp.LauncherActivity`.
|
||||
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
|
||||
data must live elsewhere (stage 2). `/sdcard/Documents` and
|
||||
`/sdcard/Download` are the SteamOS user's folders and are kept.
|
||||
|
||||
## What the port does for it
|
||||
|
||||
- SDL3's clipboard handler takes a clipboard service for granted, which
|
||||
Lepton has not: `tools/android_build.py` patches it to do without.
|
||||
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
|
||||
app cannot load: the APK carries a pass-through layer under its name
|
||||
(`port/android/xr_layer/fdm_injection_stub.c`).
|
||||
|
||||
## Stages
|
||||
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | done |
|
||||
| 2 | The game's data where Lepton keeps it | done: `/home/steamos/Documents/RingfallMjolnirVR` |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | done: their alpha from their colour |
|
||||
| 4 | The freeze at the cryo bay's calibration | not seen again (2026-10-04) |
|
||||
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
|
||||
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
|
||||
| 7 | Multiplayer, voice, cross-play with the Quest | |
|
||||
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
|
||||
|
||||
## Starting it on the Frame
|
||||
|
||||
A button in the Steam library ("Ringfall: Mjolnir VR", a non-Steam game)
|
||||
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
|
||||
which starts the game as Steam starts an Android title of its own:
|
||||
`lepton waitforexitandrun -- ~/Documents/RingfallMjolnirVR/RingfallMjolnirVR.apk`, in a container
|
||||
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
|
||||
anew whenever the file is newer. Updating the game is replacing that APK.
|
||||
|
||||
What the script does that Steam would for an Android title:
|
||||
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
|
||||
install Lepton links the app's data to it, a dangling link without it;
|
||||
and the shader path, which Lepton reads Steam's shader dump path against;
|
||||
- a container id of its own: with the shortcut's id, SteamVR's client in the
|
||||
container stalled in xrCreateInstance;
|
||||
- a few starts: a new APK's first start only installs it, and a start that
|
||||
ends within 30 s is tried again (up to three times), unless the game was
|
||||
quit (it writes `quit_requested.txt`).
|
||||
|
||||
Steam's "Exit game" does not reach the game (its process is the container's,
|
||||
not Steam's): quit it with the VR options' "Quit the game" (Y in the main or
|
||||
pause menu, twice). A container ended within 30 s of its start makes
|
||||
Lepton drop the app's install, so the next start installs it again.
|
||||
|
||||
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
|
||||
it starts; appid 3151454348 for this name and path), its pictures in
|
||||
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
|
||||
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
|
||||
beside Lepton's `<frame-ip>:5555`.
|
||||
|
||||
## The game's data
|
||||
|
||||
Lepton uninstalls the app and empties its folders whenever it starts again.
|
||||
The game's data lives in the SteamOS user's Documents instead: when
|
||||
`/home/steamos/Documents` can be written, the launcher makes
|
||||
`/home/steamos/Documents/RingfallMjolnirVR` and the launcher and the game take it as
|
||||
their data folder (config.toml, saves and debug.txt go there too), so a
|
||||
disc image imported in the app stays as well. The installer above fills it;
|
||||
by hand, once:
|
||||
|
||||
adb -s <frame-ip>:5555 shell mkdir -p /home/steamos/Documents/RingfallMjolnirVR/maps
|
||||
adb -s <frame-ip>:5555 push assets/maps/. /home/steamos/Documents/RingfallMjolnirVR/maps/
|
||||
adb -s <frame-ip>:5555 shell chmod -R 777 /home/steamos/Documents/RingfallMjolnirVR
|
||||
|
||||
After a Lepton restart only the APK is installed again. (Use the path under
|
||||
/home, not /sdcard/Documents: Android 11's scoped storage stands between the
|
||||
app and the latter.)
|
||||
|
||||
## The controllers
|
||||
|
||||
SteamVR offers the Frame's controllers as themselves
|
||||
(`XR_VALVE_frame_controller_interaction`,
|
||||
`/interaction_profiles/valve/frame_controller_valve`; Valve's "Steam Frame
|
||||
Input" page). Without bindings for it, SteamVR maps the Quest's onto them,
|
||||
and the left Touch controller's X and Y land on the D-pad. With them the
|
||||
game gets the Xbox's layout, as the right controller has it: A jump, B
|
||||
flashlight (held: switch grenades), X action and reload, Y switch weapon,
|
||||
menu pause; the left one's View is Back (scores), its D-pad the D-pad, the
|
||||
bumpers switch grenades (left) and the flashlight (right). The VR options'
|
||||
page of buttons shows this layout on the Frame.
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
- 2026-10-04: SteamVR draws a quad's texels by their alpha alone; the
|
||||
game's menus and HUD have none in their additive parts, so nothing showed.
|
||||
On SteamVR (by the runtime's name) the overlays' alpha now comes from their
|
||||
colour (host_xr.c; `HALO_XR_QUAD_ALPHA=colour|keep` overrides). With it,
|
||||
menus, HUD and the campaign ran well (the user: "lief 1a"); no freeze.
|
||||
- 2026-10-04: no online games found: the public MQTT brokers had banned the
|
||||
home connection for a while (too many connections from tests that day);
|
||||
broker.emqx.io came back within minutes. Lepton's network is the Frame's
|
||||
own (same IP), DNS works.
|
||||
- 2026-10-04: the refresh rate is SteamOS's setting; the runtime offers
|
||||
only that one (the game asks for the highest at or below
|
||||
`vr.refresh_rate`, and logs the rates offered). At 72 Hz the game holds
|
||||
72 fps (about 11.6 ms a frame with multiview, eyes 7.6 ms; without it
|
||||
eyes about 9 ms). At 90 Hz it cannot (11.1 ms a frame), and SteamVR's
|
||||
making up of the missed frames showed as a doubled picture when turning
|
||||
the head. Keep the Frame at 72 Hz, or lower `vr.resolution_scale` (about
|
||||
0.85) for 90.
|
||||
- 2026-10-05: doubled pictures when turning at 72 Hz too, the game at the
|
||||
edge of its frame (12.5 of 13.9 ms; SteamVR reprojecting 2-8% of the
|
||||
frames). The time was not the drawing's but waits: the layer copies read
|
||||
the GL state back (glGet*) and released each image at once, and with
|
||||
Mesa's GL thread (zink) every read waits for the thread to run all that
|
||||
is queued, every release for the GPU (the left eye's copy took 4.5-7 ms
|
||||
of the frame, the right one's, just after it, 0.7). On SteamVR the copies
|
||||
are now deferred (host_xr.c): no state read back (the game sets its own
|
||||
again), the images released together as the frame ends, and the eyes'
|
||||
work flushed to the GPU as soon as they are copied.
|
||||
`debug.xr_deferred_copies` = 0 brings back the old copies for comparing,
|
||||
1 defers them on any runtime; `vr.log_performance` logs the frame's parts
|
||||
and, with the compositor's counters, each copy's time.
|
||||
@@ -0,0 +1,307 @@
|
||||
# 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,16 +13,30 @@ def buildNumberText = System.getenv("HALO_BUILD_NUMBER") ?: "0"
|
||||
def buildNumber = buildNumberText.isInteger() ? buildNumberText.toInteger() : 0
|
||||
|
||||
// the workflow's signing key (.github/workflows/build.yml): every build of
|
||||
// main signed with it can install over the one before; without it, the
|
||||
// main signed with it can install over the one before; on a computer that
|
||||
// builds for its own devices, the same key from port/android/signing.properties
|
||||
// (not in the repository: storeFile, password, keyAlias), so that its
|
||||
// builds and the releases install over each other; without either, the
|
||||
// debug key of the computer that builds
|
||||
def signingKey = System.getenv("HALO_ANDROID_KEYSTORE")
|
||||
def signingPassword = System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
def signingAlias = System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "ringfall"
|
||||
def signingProperties = new File(rootDir, "signing.properties")
|
||||
if (!signingKey && signingProperties.isFile()) {
|
||||
def properties = new Properties()
|
||||
signingProperties.withInputStream { properties.load(it) }
|
||||
signingKey = properties.getProperty("storeFile")
|
||||
signingPassword = properties.getProperty("password")
|
||||
signingAlias = properties.getProperty("keyAlias", "ringfall")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.halo.decomp"
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.halo.decomp"
|
||||
// (the app's own id; the Java classes keep the decompilation's package)
|
||||
applicationId "io.github.cesarus85.ringfall"
|
||||
minSdk 28
|
||||
targetSdk 35
|
||||
versionCode Math.max(1, buildNumber)
|
||||
@@ -60,9 +74,9 @@ android {
|
||||
if (signingKey) {
|
||||
halo {
|
||||
storeFile file(signingKey)
|
||||
storePassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
keyAlias System.getenv("HALO_ANDROID_KEY_ALIAS") ?: "halo"
|
||||
keyPassword System.getenv("HALO_ANDROID_KEYSTORE_PASSWORD")
|
||||
storePassword signingPassword
|
||||
keyAlias signingAlias
|
||||
keyPassword signingPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- the downloaded new version, for the package installer (Updater) -->
|
||||
<provider
|
||||
android:name=".UpdateProvider"
|
||||
android:authorities="com.halo.decomp.update"
|
||||
android:authorities="${applicationId}.update"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.nio.channels.FileChannel;
|
||||
* Starts the game once its data is in place.
|
||||
*
|
||||
* The game reads the Xbox game data (the folder holding maps/) from the
|
||||
* app's external files directory, /sdcard/Android/data/com.halo.decomp/files.
|
||||
* app's external files directory, /sdcard/Android/data/<application id>/files.
|
||||
* If it is missing, this screen lets the player pick an Xbox disc image of
|
||||
* the game (.xiso or .iso, any version) with the system file picker, and
|
||||
* copies its maps folder there (XisoExtractor), as the desktop games do; or
|
||||
@@ -35,6 +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 File dataRoot;
|
||||
private TextView status;
|
||||
@@ -46,6 +47,17 @@ public class LauncherActivity extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dataRoot = getExternalFilesDir(null);
|
||||
// the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
// starts again; the game's data lives in the SteamOS user's
|
||||
// Documents instead, whenever they can be written, so that a disc
|
||||
// image imported here stays too (port/android/STEAM_FRAME.md; the
|
||||
// same choice as host_main.c's)
|
||||
File shared = new File(SHARED_DATA_ROOT);
|
||||
if (shared.getParentFile().canWrite() || shared.canWrite()) {
|
||||
shared.mkdirs();
|
||||
if (shared.canWrite())
|
||||
dataRoot = shared;
|
||||
}
|
||||
// created by the app, so that files pushed into it with adb stay
|
||||
// readable (a directory adb creates there belongs to the shell user)
|
||||
if (dataRoot != null)
|
||||
@@ -135,15 +147,15 @@ public class LauncherActivity extends Activity {
|
||||
layout.setBackgroundColor(Color.rgb(12, 16, 20));
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText("Halo needs its game data");
|
||||
title.setText("Ringfall: Mjolnir VR needs game data");
|
||||
title.setTextColor(Color.WHITE);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
layout.addView(title);
|
||||
|
||||
TextView message = new TextView(this);
|
||||
message.setText("Choose an Xbox disc image of Halo: Combat Evolved (an .iso or .xiso file, any "
|
||||
+ "version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
|
||||
message.setText("Choose your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or "
|
||||
+ ".xiso file, any version) on this device. Its maps folder is copied into the app's storage (about 1.8 GB), "
|
||||
+ "and you can delete the image afterwards.\n\n"
|
||||
+ "You can also copy a maps folder from a computer:\n"
|
||||
+ "adb push <folder with maps>/. " + (dataRoot != null ? dataRoot.getAbsolutePath() : "") + "/");
|
||||
|
||||
@@ -13,13 +13,13 @@ import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
* Hands Android's package installer the downloaded new version of the app
|
||||
* (Updater), read-only, the one file content://com.halo.decomp.update/halo.apk.
|
||||
* (Updater), read-only, the one file content://<application id>.update/update.apk.
|
||||
* (The app has no AndroidX, whose FileProvider does the same.)
|
||||
*/
|
||||
public class UpdateProvider extends ContentProvider {
|
||||
static final String AUTHORITY = "com.halo.decomp.update";
|
||||
static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".update";
|
||||
static final String DIRECTORY = "update";
|
||||
static final String APK = "halo.apk";
|
||||
static final String APK = "update.apk";
|
||||
|
||||
private File apk() {
|
||||
return new File(new File(getContext().getCacheDir(), DIRECTORY), APK);
|
||||
|
||||
@@ -49,7 +49,8 @@ import java.util.zip.ZipInputStream;
|
||||
final class Updater {
|
||||
/* the VR port's own releases: the original's, made without VR, must
|
||||
never replace it (they share the package name) */
|
||||
private static final String REPOSITORY = "Cesarus85/halo-ce-vr";
|
||||
private static final String APP_NAME = "Ringfall: Mjolnir VR";
|
||||
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
|
||||
private static final String USER_AGENT = "halo-ce-universal-updater";
|
||||
private static final int TIMEOUT_MILLISECONDS = 20000;
|
||||
|
||||
@@ -202,8 +203,8 @@ final class Updater {
|
||||
if (activity.isFinishing())
|
||||
return;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("Halo: new version")
|
||||
.setMessage("A new version of Halo was detected (build " + latest + "; this is build "
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setMessage("A new version of " + APP_NAME + " was detected (build " + latest + "; this is build "
|
||||
+ BuildConfig.HALO_BUILD_NUMBER + ").\n\nDo you want to update? The game will close and start "
|
||||
+ "the new version.")
|
||||
.setCancelable(false)
|
||||
@@ -215,7 +216,7 @@ final class Updater {
|
||||
|
||||
private static void confirmNever(Activity activity) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("Halo: new version")
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setMessage("Stop asking about new versions?\n\nTo ask again, set auto = true in the [update] section "
|
||||
+ "of config.toml.")
|
||||
.setCancelable(false)
|
||||
@@ -232,7 +233,7 @@ final class Updater {
|
||||
/* ---------- updating */
|
||||
|
||||
private static void update(Activity activity, int latest) {
|
||||
String asset = "halo-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
String asset = "ringfall-mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
File directory = new File(activity.getCacheDir(), UpdateProvider.DIRECTORY);
|
||||
LinearLayout layout = new LinearLayout(activity);
|
||||
TextView status = new TextView(activity);
|
||||
@@ -247,7 +248,7 @@ final class Updater {
|
||||
layout.addView(status);
|
||||
layout.addView(bar);
|
||||
AlertDialog progress = new AlertDialog.Builder(activity)
|
||||
.setTitle("Halo: new version")
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setView(layout)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
@@ -275,7 +276,7 @@ final class Updater {
|
||||
activity.runOnUiThread(() -> {
|
||||
progress.dismiss();
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("Halo: new version")
|
||||
.setTitle(APP_NAME + ": new version")
|
||||
.setMessage("The update failed:\n\n" + e.getMessage())
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Halo</string>
|
||||
<string name="app_name">Ringfall: Mjolnir VR</string>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 21 KiB |
@@ -119,7 +119,9 @@ headset before the game started, and set HALO_VR); 0 on failure */
|
||||
int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate);
|
||||
/* waits for the next frame and reports the predicted poses */
|
||||
int host_xr_begin_frame(struct halo_xr_frame *frame);
|
||||
/* copies a render target texture (0: clears) into a layer's image */
|
||||
/* copies a render target texture (0: clears) into a layer's image: 0 not
|
||||
copied, 1 copied with the GL state put back, 2 copied with the copy's GL
|
||||
state left (the deferred copies, host_xr.c), which the game sets again */
|
||||
int host_xr_copy_layer(int layer, unsigned int texture, int clear_alpha);
|
||||
/* the same for part of the texture: left, top, right, bottom, 0..1 from its
|
||||
top left */
|
||||
@@ -133,6 +135,11 @@ into a layer's image, opaque */
|
||||
int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
|
||||
/* darkens the eye images from their edges, strength 0..1 (0: off) */
|
||||
void host_xr_set_vignette(float strength);
|
||||
/* the refresh rates the headset offers (Hz, at most maximum of them; 0 when
|
||||
it lets none be chosen), and the one it runs at (0: unknown); and asking
|
||||
for the highest offered at or below wanted (0: the highest) */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current);
|
||||
void host_xr_set_refresh_rate(float wanted);
|
||||
|
||||
/* ---------- Android */
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ 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,8 +11,52 @@ 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)
|
||||
{
|
||||
@@ -88,14 +132,13 @@ 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]);
|
||||
@@ -108,19 +151,17 @@ 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, (GLuint)read_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
}
|
||||
|
||||
/* copies a snapshot out; its frame must be finished (host_gl_wait_frame) */
|
||||
int host_gl_counters_read(uint32_t slot, void *out, uint32_t size)
|
||||
{
|
||||
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)
|
||||
@@ -128,7 +169,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, (GLuint)write_binding);
|
||||
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||
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, "Halo", message, NULL);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Ringfall: Mjolnir VR", message, NULL);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ int host_errno(void)
|
||||
|
||||
static char data_root[512];
|
||||
static char save_root[512];
|
||||
/* where a device that empties the app's folders keeps the game's data */
|
||||
#define SHARED_DATA_ROOT "/home/steamos/Documents/RingfallMjolnirVR"
|
||||
|
||||
void host_android_path(int which, char *buffer, uint32_t size)
|
||||
{
|
||||
@@ -269,6 +271,12 @@ static void *game_main(void *unused)
|
||||
external = SDL_GetAndroidExternalStoragePath();
|
||||
if (!external)
|
||||
host_fatal("Android storage is unavailable: %s", SDL_GetError());
|
||||
/* the Steam Frame: Lepton empties the app's own folders whenever it
|
||||
starts again; the game's data lives in the SteamOS user's Documents
|
||||
instead, whenever they can be written (port/android/STEAM_FRAME.md; the
|
||||
same choice as LauncherActivity's, which makes the folder) */
|
||||
if (access(SHARED_DATA_ROOT, W_OK) == 0)
|
||||
external = SHARED_DATA_ROOT;
|
||||
snprintf(data_root, sizeof(data_root), "%s", external);
|
||||
snprintf(save_root, sizeof(save_root), "%s/save", external);
|
||||
/* readable by adb (the shell user), for managing saves */
|
||||
@@ -284,7 +292,7 @@ static void *game_main(void *unused)
|
||||
}
|
||||
if (!directory_has_maps(data_root))
|
||||
{
|
||||
host_fatal("The Halo game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
|
||||
host_fatal("The game data was not found.\n\nCopy the PAL game data (build 01.01.14.2342), "
|
||||
"the folder that contains maps, into\n%s\nor import it from the launcher screen.", data_root);
|
||||
}
|
||||
|
||||
@@ -293,6 +301,19 @@ static void *game_main(void *unused)
|
||||
environment_set(&environment, "HALO_SAVE_ROOT", save_root);
|
||||
time_zone(zone, sizeof(zone));
|
||||
environment_set(&environment, "TZ", zone);
|
||||
/* internet play's MQTT brokers (network.brokers_file): the APK's list,
|
||||
written beside config.toml at each start, as a desktop update replaces
|
||||
the file beside its game */
|
||||
{
|
||||
size_t brokers_size = 0;
|
||||
void *brokers = SDL_LoadFile("brokers.txt", &brokers_size);
|
||||
|
||||
snprintf(path, sizeof(path), "%s/brokers.txt", data_root);
|
||||
if (!brokers || !SDL_SaveFile(path, brokers, brokers_size))
|
||||
host_logf(HOST_LOG_ERROR, "cannot write %s: %s", path, SDL_GetError());
|
||||
SDL_free(brokers);
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s/config.toml", data_root);
|
||||
|
||||
image = SDL_LoadFile("halo_guest.elf", &image_size);
|
||||
if (!image)
|
||||
@@ -305,6 +326,26 @@ static void *game_main(void *unused)
|
||||
ranges: the headset's runtime, loaded by the probe, maps its libraries
|
||||
wherever there is room, the Xbox window at 0x80000000 included */
|
||||
snprintf(path, sizeof(path), "%s/config.toml", data_root);
|
||||
{
|
||||
/* (debug.gl_thread: Mesa's GL thread on or off, as the driver's
|
||||
mesa_glthread, before the GL context is made; -1 leaves it be) */
|
||||
double gl_thread = config_number(path, "debug.gl_thread", -1.0);
|
||||
|
||||
if (gl_thread >= 0.0)
|
||||
{
|
||||
setenv("mesa_glthread", gl_thread != 0.0 ? "true" : "false", 1);
|
||||
host_logf(HOST_LOG_INFO, "Mesa's GL thread %s (debug.gl_thread)", gl_thread != 0.0 ? "on" : "off");
|
||||
}
|
||||
}
|
||||
{
|
||||
/* (debug.xr_deferred_copies: the headset's layer copies deferred,
|
||||
host_xr.c, or one by one; -1 as the runtime has it, deferred on
|
||||
SteamVR) */
|
||||
double deferred = config_number(path, "debug.xr_deferred_copies", -1.0);
|
||||
|
||||
if (deferred >= 0.0)
|
||||
setenv("HALO_XR_DEFERRED", deferred != 0.0 ? "1" : "0", 1);
|
||||
}
|
||||
{
|
||||
int eye_width, eye_height;
|
||||
|
||||
@@ -359,7 +400,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
host_logf(HOST_LOG_INFO, "Halo for Android starting");
|
||||
host_logf(HOST_LOG_INFO, "Ringfall: Mjolnir VR starting");
|
||||
host_install_signal_handlers();
|
||||
if (host_native_thread_create(game_main, NULL, MAIN_STACK_SIZE) != 0)
|
||||
host_fatal("cannot start the game thread");
|
||||
|
||||
@@ -151,7 +151,11 @@ uint32_t host_sdl_gl_create_context(uint32_t window)
|
||||
{
|
||||
SDL_Window *object = handle_get(window, _handle_window);
|
||||
|
||||
return object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
uint32_t context = object ? handle_new(_handle_context, SDL_GL_CreateContext(object)) : 0;
|
||||
|
||||
/* (the driver opens its trace markers with the context) */
|
||||
host_gl_quiet_trace_markers();
|
||||
return context;
|
||||
}
|
||||
|
||||
int host_sdl_gl_make_current(uint32_t window, uint32_t context)
|
||||
|
||||
@@ -38,6 +38,8 @@ picture; the copy flips them to GL's bottom-up images.
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID
|
||||
#define XR_USE_GRAPHICS_API_OPENGL_ES
|
||||
@@ -56,6 +58,7 @@ struct xr_swapchain
|
||||
GLuint framebuffers[MAXIMUM_SWAPCHAIN_IMAGES];
|
||||
uint32_t acquired_index;
|
||||
int filled; /* this frame */
|
||||
int held; /* acquired, released only as the frame ends (xr.deferred) */
|
||||
};
|
||||
|
||||
struct xr_hand_actions
|
||||
@@ -102,15 +105,54 @@ 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;
|
||||
XrAction button_x;
|
||||
XrAction button_y;
|
||||
XrAction view;
|
||||
XrAction bumper;
|
||||
XrAction dpad[4];
|
||||
XrAction haptic;
|
||||
struct xr_hand_actions hands[HALO_XR_HAND_COUNT];
|
||||
|
||||
/* where the head and the left hand are this frame (the wrist layer) */
|
||||
/* where the head and the hands are this frame (the wrist layer) */
|
||||
XrPosef head;
|
||||
XrPosef left_grip;
|
||||
int left_grip_valid;
|
||||
XrPosef grips[HALO_XR_HAND_COUNT];
|
||||
int grips_valid[HALO_XR_HAND_COUNT];
|
||||
|
||||
int has_performance_settings;
|
||||
/* (XR_META_performance_metrics: the compositor's counts, in the log,
|
||||
for telling frames shown late from the game's own pace) */
|
||||
int has_metrics;
|
||||
PFN_xrQueryPerformanceMetricsCounterMETA query_metric;
|
||||
XrPath metric_paths[16];
|
||||
uint32_t metric_count;
|
||||
struct timespec metrics_logged;
|
||||
/* (SteamVR: the frames' thread kept on the fast cores, xr_fast_cores) */
|
||||
int steamvr;
|
||||
int cores_pinned;
|
||||
/* (with the metrics: the time the copies into the layers' images and
|
||||
xrEndFrame took, since the last line, and the frames) */
|
||||
double copy_seconds[HALO_XR_LAYER_COUNT];
|
||||
double end_seconds;
|
||||
double acquire_seconds;
|
||||
double release_seconds;
|
||||
int timed_frames;
|
||||
/* The copies without waits in the frame (SteamVR: HALO_XR_DEFERRED,
|
||||
debug.xr_deferred_copies). Each copy otherwise reads the GL state back
|
||||
to restore it (glGet*), and releases its image at once: with Mesa's GL
|
||||
thread (the Steam Frame's zink) each read waits for the thread to run
|
||||
everything queued, and each release for the GPU to finish it, so the
|
||||
game's one thread stood still for the eyes' whole drawing in the middle
|
||||
of every frame. Deferred, a copy leaves its GL state for the game to set
|
||||
again (host_xr_copy_layer's 2) and keeps its image until the frame ends,
|
||||
where all are released at once. */
|
||||
int deferred;
|
||||
int has_refresh_rate;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate;
|
||||
PFN_xrGetDisplayRefreshRateFB get_refresh_rate;
|
||||
@@ -122,6 +164,11 @@ static struct
|
||||
GLint rect_uniform;
|
||||
GLint cut_disc_uniform;
|
||||
GLint vignette_uniform;
|
||||
GLint alpha_from_colour_uniform;
|
||||
/* the runtime draws a quad's texels by their alpha alone, ignoring the
|
||||
colour of those with none (SteamVR on the Steam Frame): the game's
|
||||
overlays, whose additive parts have none, get alpha from their colour */
|
||||
int quad_alpha_from_colour;
|
||||
GLint tangents_uniform;
|
||||
float vignette;
|
||||
/* the same copy from a layer of a texture array (the eyes drawn at
|
||||
@@ -223,6 +270,7 @@ static const char copy_fragment_shader[] =
|
||||
"precision highp float;\n"
|
||||
"uniform sampler2D source;\n"
|
||||
"uniform float cut_disc;\n"
|
||||
"uniform float alpha_from_colour; /* 1: as opaque as it is bright (a quad's, SteamVR) */\n"
|
||||
"uniform float vignette; /* 0..1: the eye images darken from their edges */\n"
|
||||
"uniform vec4 tangents; /* the eye's edges: left, right, down, up */\n"
|
||||
"in vec2 coordinate;\n"
|
||||
@@ -236,6 +284,8 @@ static const char copy_fragment_shader[] =
|
||||
"{\n"
|
||||
" vec4 texel = texture(source, coordinate);\n"
|
||||
" color = vec4(to_linear(texel.rgb), texel.a);\n"
|
||||
" if (alpha_from_colour > 0.5)\n"
|
||||
" color.a = max(color.a, max(color.r, max(color.g, color.b)));\n"
|
||||
" if (cut_disc > 0.5)\n"
|
||||
" {\n"
|
||||
" float inside = 1.0 - smoothstep(0.95, 1.0, length(disc));\n"
|
||||
@@ -326,6 +376,7 @@ static int copy_initialize(void)
|
||||
xr.rect_uniform = glGetUniformLocation(xr.program, "rect");
|
||||
xr.cut_disc_uniform = glGetUniformLocation(xr.program, "cut_disc");
|
||||
xr.vignette_uniform = glGetUniformLocation(xr.program, "vignette");
|
||||
xr.alpha_from_colour_uniform = glGetUniformLocation(xr.program, "alpha_from_colour");
|
||||
xr.tangents_uniform = glGetUniformLocation(xr.program, "tangents");
|
||||
xr.copy_array_layer = -1;
|
||||
{
|
||||
@@ -538,22 +589,51 @@ struct binding
|
||||
const char *path;
|
||||
};
|
||||
|
||||
static void suggest(const char *profile, const struct binding *bindings, int count)
|
||||
{
|
||||
XrActionSuggestedBinding suggested[32];
|
||||
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||||
int index;
|
||||
/* 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
|
||||
|
||||
for (index = 0; index < count && index < 32; index++)
|
||||
static int suggest_bindings(const char *profile, const struct binding *bindings, int count,
|
||||
const struct binding *optional, int optional_count)
|
||||
{
|
||||
XrActionSuggestedBinding suggested[MAXIMUM_SUGGESTED_BINDINGS];
|
||||
XrInteractionProfileSuggestedBinding information = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
|
||||
XrResult result;
|
||||
int index, total = 0;
|
||||
|
||||
if (count + optional_count > MAXIMUM_SUGGESTED_BINDINGS)
|
||||
{
|
||||
suggested[index].action = *bindings[index].action;
|
||||
suggested[index].binding = path_from_string(bindings[index].path);
|
||||
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++;
|
||||
}
|
||||
information.interactionProfile = path_from_string(profile);
|
||||
information.suggestedBindings = suggested;
|
||||
information.countSuggestedBindings = (uint32_t)index;
|
||||
if (!XR_SUCCEEDED(xrSuggestInteractionProfileBindings(xr.instance, &information)))
|
||||
host_logf(HOST_LOG_WARN, "xr: no bindings for %s", profile);
|
||||
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);
|
||||
}
|
||||
|
||||
static int actions_initialize(void)
|
||||
@@ -580,6 +660,16 @@ 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);
|
||||
xr.bumper = action_create("bumper", "Bumper", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[0] = action_create("dpad_up", "D-pad up", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[1] = action_create("dpad_down", "D-pad down", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[2] = action_create("dpad_left", "D-pad left", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.dpad[3] = action_create("dpad_right", "D-pad right", XR_ACTION_TYPE_BOOLEAN_INPUT);
|
||||
xr.haptic = action_create("haptic", "Rumble", XR_ACTION_TYPE_VIBRATION_OUTPUT);
|
||||
|
||||
{
|
||||
@@ -644,8 +734,95 @@ static int actions_initialize(void)
|
||||
{ &xr.haptic, "/user/hand/right/output/haptic" },
|
||||
};
|
||||
|
||||
suggest("/interaction_profiles/oculus/touch_controller", touch, sizeof(touch) / sizeof(touch[0]));
|
||||
suggest("/interaction_profiles/valve/index_controller", index, sizeof(index) / sizeof(index[0]));
|
||||
/* the Steam Frame's controllers, as themselves: the right one has
|
||||
A, B, X, Y and a menu button, the left one a D-pad and a View button
|
||||
(port/android/STEAM_FRAME.md) */
|
||||
static const struct binding frame_controller[] =
|
||||
{
|
||||
{ &xr.grip_pose, "/user/hand/left/input/grip/pose" },
|
||||
{ &xr.grip_pose, "/user/hand/right/input/grip/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/left/input/aim/pose" },
|
||||
{ &xr.aim_pose, "/user/hand/right/input/aim/pose" },
|
||||
{ &xr.trigger, "/user/hand/left/input/trigger/value" },
|
||||
{ &xr.trigger, "/user/hand/right/input/trigger/value" },
|
||||
{ &xr.squeeze, "/user/hand/left/input/squeeze/value" },
|
||||
{ &xr.squeeze, "/user/hand/right/input/squeeze/value" },
|
||||
{ &xr.thumbstick, "/user/hand/left/input/thumbstick" },
|
||||
{ &xr.thumbstick, "/user/hand/right/input/thumbstick" },
|
||||
{ &xr.thumbstick_click, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &xr.thumbstick_click, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &xr.button_a, "/user/hand/right/input/a/click" },
|
||||
{ &xr.button_b, "/user/hand/right/input/b/click" },
|
||||
{ &xr.button_x, "/user/hand/right/input/x/click" },
|
||||
{ &xr.button_y, "/user/hand/right/input/y/click" },
|
||||
{ &xr.menu, "/user/hand/right/input/menu/click" },
|
||||
{ &xr.view, "/user/hand/left/input/view/click" },
|
||||
{ &xr.bumper, "/user/hand/left/input/bumper/click" },
|
||||
{ &xr.bumper, "/user/hand/right/input/bumper/click" },
|
||||
{ &xr.dpad[0], "/user/hand/left/input/dpad_up/click" },
|
||||
{ &xr.dpad[1], "/user/hand/left/input/dpad_down/click" },
|
||||
{ &xr.dpad[2], "/user/hand/left/input/dpad_left/click" },
|
||||
{ &xr.dpad[3], "/user/hand/left/input/dpad_right/click" },
|
||||
{ &xr.haptic, "/user/hand/left/output/haptic" },
|
||||
{ &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_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/khr/simple_controller", simple, sizeof(simple) / sizeof(simple[0]));
|
||||
}
|
||||
|
||||
@@ -688,6 +865,21 @@ 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 };
|
||||
@@ -713,6 +905,30 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
sync.activeActionSets = &active;
|
||||
if (!XR_SUCCEEDED(xrSyncActions(xr.session, &sync)))
|
||||
return;
|
||||
{
|
||||
/* (the controllers' profile the runtime chose, in the log as it
|
||||
changes) */
|
||||
static XrPath logged[HALO_XR_HAND_COUNT];
|
||||
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
XrInteractionProfileState state = { XR_TYPE_INTERACTION_PROFILE_STATE };
|
||||
|
||||
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(xr.session, xr.hands[hand].path, &state)) &&
|
||||
state.interactionProfile != logged[hand])
|
||||
{
|
||||
char name[XR_MAX_PATH_LENGTH] = "none";
|
||||
uint32_t length = 0;
|
||||
|
||||
logged[hand] = state.interactionProfile;
|
||||
if (state.interactionProfile != XR_NULL_PATH)
|
||||
xrPathToString(xr.instance, state.interactionProfile, sizeof(name), &length, name);
|
||||
if (hand == HALO_XR_HAND_RIGHT)
|
||||
xr.frame_layout = strstr(name, "frame_controller") != NULL;
|
||||
host_logf(HOST_LOG_INFO, "xr: the %s hand's controller: %s", hand ? "right" : "left", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (hand = 0; hand < HALO_XR_HAND_COUNT; hand++)
|
||||
{
|
||||
struct halo_xr_hand *out = &frame->hands[hand];
|
||||
@@ -730,11 +946,8 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
{
|
||||
out->flags = flags_from_location(location.locationFlags);
|
||||
pose_from_xr(&out->grip, &location.pose);
|
||||
if (hand == HALO_XR_HAND_LEFT)
|
||||
{
|
||||
xr.left_grip = location.pose;
|
||||
xr.left_grip_valid = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
xr.grips[hand] = location.pose;
|
||||
xr.grips_valid[hand] = (out->flags & HALO_XR_POSE_VALID) != 0;
|
||||
}
|
||||
location.locationFlags = 0;
|
||||
if (XR_SUCCEEDED(xrLocateSpace(xr.hands[hand].aim_space, xr.local_space,
|
||||
@@ -757,7 +970,42 @@ static void hands_read(struct halo_xr_frame *frame)
|
||||
(action_boolean(xr.menu, hand) ? HALO_XR_BUTTON_MENU : 0) |
|
||||
(action_boolean(xr.thumbrest, hand) ? HALO_XR_BUTTON_THUMBREST : 0) |
|
||||
(out->trigger > 0.5f ? HALO_XR_BUTTON_TRIGGER : 0) |
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0);
|
||||
(out->squeeze > 0.5f ? HALO_XR_BUTTON_SQUEEZE : 0) |
|
||||
(action_boolean(xr.view, hand) ? HALO_XR_BUTTON_VIEW : 0) |
|
||||
(action_boolean(xr.bumper, hand) ? HALO_XR_BUTTON_BUMPER : 0) |
|
||||
(action_boolean(xr.dpad[0], hand) ? HALO_XR_BUTTON_DPAD_UP : 0) |
|
||||
(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)
|
||||
{
|
||||
/* (the Frame's right X, Y and menu button: the game's X, Y and Start,
|
||||
which the Quest has on the left) */
|
||||
struct halo_xr_hand *left = &frame->hands[HALO_XR_HAND_LEFT];
|
||||
struct halo_xr_hand *right = &frame->hands[HALO_XR_HAND_RIGHT];
|
||||
|
||||
if (action_boolean(xr.button_x, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_A;
|
||||
if (action_boolean(xr.button_y, HALO_XR_HAND_RIGHT))
|
||||
left->buttons |= HALO_XR_BUTTON_B;
|
||||
if (right->buttons & HALO_XR_BUTTON_MENU)
|
||||
{
|
||||
right->buttons &= ~HALO_XR_BUTTON_MENU;
|
||||
left->buttons |= HALO_XR_BUTTON_MENU;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,6 +1022,13 @@ static void refresh_rate_choose(float wanted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
char line[128] = "";
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
snprintf(line + strlen(line), sizeof(line) - strlen(line), " %.0f", rates[index]);
|
||||
host_logf(HOST_LOG_INFO, "xr: refresh rates offered:%s Hz", line);
|
||||
}
|
||||
/* the highest rate not above the one wanted (0: the highest) */
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
@@ -790,7 +1045,9 @@ On the back of the left wrist, like a watch: behind the palm, where the
|
||||
grip pose is, and out of the back of the hand. In the left grip pose -Z
|
||||
runs to the fingers, +Y up the handle and -X out of the back of the hand;
|
||||
the layer faces -X with its top towards the elbow, as the wrist turned to
|
||||
the eyes reads it. Shown only while it is. */
|
||||
the eyes reads it. Shown only while it is. Left-handed (wrist_hand), on the
|
||||
right wrist, as the mirror image: there +X runs out of the back of the
|
||||
hand. */
|
||||
|
||||
#define WRIST_IMAGE_SIZE 512
|
||||
#define CROSSHAIR_IMAGE_SIZE 256
|
||||
@@ -830,26 +1087,32 @@ static void quaternion_rotate_vector(const XrQuaternionf *q, const XrVector3f *v
|
||||
|
||||
static int wrist_pose(const struct halo_xr_submit *submit, XrPosef *pose)
|
||||
{
|
||||
/* the layer's axes in the grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z */
|
||||
static const XrQuaternionf layer_in_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
const XrVector3f offset = { -WRIST_ABOVE_HAND, -WRIST_DOWN_HANDLE - submit->wrist_right,
|
||||
WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
/* the layer's axes in the left grip's: x = -Y, y = +Z, z = -X, so the
|
||||
layer's right and down, as it is read, are the grip's -Y and -Z; in the
|
||||
right grip's x = +Y, y = +Z, z = +X (the mirror image, read the right
|
||||
way round: its x turned back) */
|
||||
static const XrQuaternionf layer_in_left_grip = { 0.5f, -0.5f, -0.5f, 0.5f };
|
||||
static const XrQuaternionf layer_in_right_grip = { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||
int right = submit->wrist_hand == HALO_XR_HAND_RIGHT;
|
||||
const XrPosef *grip = &xr.grips[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT];
|
||||
const XrVector3f offset = { right ? WRIST_ABOVE_HAND : -WRIST_ABOVE_HAND,
|
||||
-WRIST_DOWN_HANDLE - submit->wrist_right, WRIST_BEHIND_PALM - submit->wrist_down };
|
||||
const XrVector3f layer_normal = { 0.0f, 0.0f, 1.0f };
|
||||
/* clockwise as read: about the layer's normal, negatively */
|
||||
float half_turn = -submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
/* clockwise as read: about the layer's normal, negatively (the mirror
|
||||
image's the other way) */
|
||||
float half_turn = (right ? 1.0f : -1.0f) * submit->wrist_turn * WRIST_PI / 360.0f;
|
||||
XrQuaternionf turn = { 0.0f, 0.0f, sinf(half_turn), cosf(half_turn) };
|
||||
XrQuaternionf unturned;
|
||||
XrVector3f moved, normal, to_eyes;
|
||||
float distance;
|
||||
|
||||
if (!xr.left_grip_valid)
|
||||
if (!xr.grips_valid[right ? HALO_XR_HAND_RIGHT : HALO_XR_HAND_LEFT])
|
||||
return 0;
|
||||
quaternion_rotate_vector(&xr.left_grip.orientation, &offset, &moved);
|
||||
pose->position.x = xr.left_grip.position.x + moved.x;
|
||||
pose->position.y = xr.left_grip.position.y + moved.y;
|
||||
pose->position.z = xr.left_grip.position.z + moved.z;
|
||||
quaternion_multiply(&xr.left_grip.orientation, &layer_in_grip, &unturned);
|
||||
quaternion_rotate_vector(&grip->orientation, &offset, &moved);
|
||||
pose->position.x = grip->position.x + moved.x;
|
||||
pose->position.y = grip->position.y + moved.y;
|
||||
pose->position.z = grip->position.z + moved.z;
|
||||
quaternion_multiply(&grip->orientation, right ? &layer_in_right_grip : &layer_in_left_grip, &unturned);
|
||||
quaternion_multiply(&unturned, &turn, &pose->orientation);
|
||||
|
||||
quaternion_rotate_vector(&pose->orientation, &layer_normal, &normal);
|
||||
@@ -895,6 +1158,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
/* the loader and the runtime keep the activity */
|
||||
activity = (*environment)->NewGlobalRef(environment, activity);
|
||||
|
||||
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
|
||||
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
|
||||
from its namespace, failing xrCreateInstance; the Android loader reads
|
||||
no variable to turn it off, so the app carries a pass-through layer
|
||||
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
|
||||
|
||||
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
|
||||
{
|
||||
@@ -922,6 +1191,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
extensions[index].next = NULL;
|
||||
}
|
||||
xrEnumerateInstanceExtensionProperties(NULL, extension_count, &extension_count, extensions);
|
||||
{
|
||||
/* (the runtime's extensions, in the log: what a headset offers) */
|
||||
char line[512] = "";
|
||||
|
||||
for (index = 0; index < extension_count; index++)
|
||||
{
|
||||
if (strlen(line) + strlen(extensions[index].extensionName) + 2 >= sizeof(line))
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
line[0] = 0;
|
||||
}
|
||||
strcat(line, " ");
|
||||
strcat(line, extensions[index].extensionName);
|
||||
}
|
||||
if (line[0])
|
||||
host_logf(HOST_LOG_INFO, "xr: extensions %s", line);
|
||||
}
|
||||
if (!has_extension(extensions, extension_count, XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME))
|
||||
{
|
||||
host_logf(HOST_LOG_ERROR, "xr: the runtime has no OpenGL ES support");
|
||||
@@ -933,14 +1219,20 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
xr.has_performance_settings = has_extension(extensions, extension_count, XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME);
|
||||
if (xr.has_performance_settings)
|
||||
enabled[enabled_count++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME;
|
||||
xr.has_frame_controller = has_extension(extensions, extension_count, "XR_VALVE_frame_controller_interaction");
|
||||
if (xr.has_frame_controller)
|
||||
enabled[enabled_count++] = "XR_VALVE_frame_controller_interaction";
|
||||
xr.has_refresh_rate = has_extension(extensions, extension_count, XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME);
|
||||
if (xr.has_refresh_rate)
|
||||
enabled[enabled_count++] = XR_FB_DISPLAY_REFRESH_RATE_EXTENSION_NAME;
|
||||
xr.has_metrics = has_extension(extensions, extension_count, XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
|
||||
if (xr.has_metrics)
|
||||
enabled[enabled_count++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
|
||||
|
||||
android.applicationVM = machine;
|
||||
android.applicationActivity = activity;
|
||||
instance.next = &android;
|
||||
strcpy(instance.applicationInfo.applicationName, "Halo VR");
|
||||
strcpy(instance.applicationInfo.applicationName, "Ringfall Mjolnir VR");
|
||||
instance.applicationInfo.applicationVersion = 1;
|
||||
strcpy(instance.applicationInfo.engineName, "halo-ce-universal");
|
||||
instance.applicationInfo.engineVersion = 1;
|
||||
@@ -960,6 +1252,23 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
}
|
||||
xrGetSystemProperties(xr.instance, xr.system, &properties);
|
||||
strncpy(xr.system_name, properties.systemName, sizeof(xr.system_name) - 1);
|
||||
{
|
||||
/* (SteamVR: the quads' alpha from their colour; HALO_XR_QUAD_ALPHA
|
||||
"colour" or "keep" says otherwise, for trying) */
|
||||
XrInstanceProperties instance = { XR_TYPE_INSTANCE_PROPERTIES };
|
||||
const char *choice = getenv("HALO_XR_QUAD_ALPHA");
|
||||
|
||||
if (XR_SUCCEEDED(xrGetInstanceProperties(xr.instance, &instance)))
|
||||
{
|
||||
xr.quad_alpha_from_colour = strstr(instance.runtimeName, "SteamVR") != NULL;
|
||||
xr.steamvr = xr.quad_alpha_from_colour;
|
||||
host_logf(HOST_LOG_INFO, "xr: runtime %s", instance.runtimeName);
|
||||
}
|
||||
if (choice)
|
||||
xr.quad_alpha_from_colour = !strcmp(choice, "colour");
|
||||
if (xr.quad_alpha_from_colour)
|
||||
host_logf(HOST_LOG_INFO, "xr: the overlays' alpha from their colour");
|
||||
}
|
||||
|
||||
views[0].type = views[1].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;
|
||||
views[0].next = views[1].next = NULL;
|
||||
@@ -1066,6 +1375,29 @@ 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;
|
||||
PFN_xrSetPerformanceMetricsStateMETA set_state = NULL;
|
||||
XrPerformanceMetricsStateMETA state = { XR_TYPE_PERFORMANCE_METRICS_STATE_META };
|
||||
|
||||
xrGetInstanceProcAddr(xr.instance, "xrEnumeratePerformanceMetricsCounterPathsMETA",
|
||||
(PFN_xrVoidFunction *)&enumerate);
|
||||
xrGetInstanceProcAddr(xr.instance, "xrSetPerformanceMetricsStateMETA", (PFN_xrVoidFunction *)&set_state);
|
||||
xrGetInstanceProcAddr(xr.instance, "xrQueryPerformanceMetricsCounterMETA",
|
||||
(PFN_xrVoidFunction *)&xr.query_metric);
|
||||
state.enabled = XR_TRUE;
|
||||
xr.has_metrics = enumerate && set_state && xr.query_metric &&
|
||||
XR_SUCCEEDED(enumerate(xr.instance, 16, &xr.metric_count, xr.metric_paths)) &&
|
||||
XR_SUCCEEDED(set_state(xr.session, &state));
|
||||
host_logf(HOST_LOG_INFO, "xr: performance metrics %s (%u counters)", xr.has_metrics ? "on" : "off",
|
||||
xr.has_metrics ? xr.metric_count : 0);
|
||||
}
|
||||
|
||||
if (xr.has_performance_settings)
|
||||
{
|
||||
/* the game's one busy thread and its GPU work both need the clocks
|
||||
@@ -1082,6 +1414,16 @@ int host_xr_initialize(struct halo_xr_info *info, float wanted_refresh_rate)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/* (deferred on SteamVR; HALO_XR_DEFERRED "1" or "0" says otherwise:
|
||||
debug.xr_deferred_copies, host_main.c) */
|
||||
const char *choice = getenv("HALO_XR_DEFERRED");
|
||||
|
||||
xr.deferred = choice && *choice ? atoi(choice) != 0 : xr.steamvr;
|
||||
host_logf(HOST_LOG_INFO, "xr: layer copies %s", xr.deferred ?
|
||||
"deferred (no waits for the GL thread or the GPU in the frame)" : "restored and released one by one");
|
||||
}
|
||||
|
||||
info->eye_width = xr.eye_width;
|
||||
info->eye_height = xr.eye_height;
|
||||
info->overlay_width = xr.eye_width;
|
||||
@@ -1218,7 +1560,7 @@ int host_xr_begin_frame(struct halo_xr_frame *frame)
|
||||
pose_from_xr(&frame->head, &head.pose);
|
||||
xr.head = head.pose;
|
||||
}
|
||||
xr.left_grip_valid = 0;
|
||||
xr.grips_valid[HALO_XR_HAND_LEFT] = xr.grips_valid[HALO_XR_HAND_RIGHT] = 0;
|
||||
if (xr.focused)
|
||||
hands_read(frame);
|
||||
return 1;
|
||||
@@ -1245,8 +1587,30 @@ int host_xr_copy_layer_array(int layer, uint32_t texture, int array_layer)
|
||||
|
||||
/* copies part of a render target (left, top, right, bottom, 0..1 from its
|
||||
top left) into a layer's image */
|
||||
static double seconds_now(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom);
|
||||
|
||||
int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom)
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int copied = copy_layer_region(layer, texture, clear_alpha, left, top, right, bottom);
|
||||
|
||||
if (xr.has_metrics && layer >= 0 && layer < HALO_XR_LAYER_COUNT)
|
||||
xr.copy_seconds[layer] += seconds_now() - start;
|
||||
return copied;
|
||||
}
|
||||
|
||||
static int copy_layer_region(int layer, uint32_t texture, int clear_alpha, float left, float top, float right,
|
||||
float bottom)
|
||||
{
|
||||
struct xr_swapchain *swapchain;
|
||||
XrSwapchainImageAcquireInfo acquire = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
|
||||
@@ -1258,17 +1622,27 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
return 0;
|
||||
swapchain = &xr.swapchains[layer];
|
||||
if (swapchain->filled)
|
||||
return 1;
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return xr.deferred ? 2 : 1;
|
||||
wait.timeout = XR_INFINITE_DURATION;
|
||||
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
|
||||
return 0;
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
|
||||
gl_save(&saved);
|
||||
if (!xr_ok(xrAcquireSwapchainImage(swapchain->handle, &acquire, &swapchain->acquired_index),
|
||||
"xrAcquireSwapchainImage"))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (!xr_ok(xrWaitSwapchainImage(swapchain->handle, &wait), "xrWaitSwapchainImage"))
|
||||
return 0;
|
||||
if (xr.has_metrics)
|
||||
xr.acquire_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
/* (deferred: nothing read back; the game sets its state again) */
|
||||
if (xr.deferred)
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
else
|
||||
gl_save(&saved);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, swapchain->framebuffers[swapchain->acquired_index]);
|
||||
glViewport(0, 0, swapchain->width, swapchain->height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
@@ -1316,6 +1690,8 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glUniform1i(xr.source_uniform, 0);
|
||||
glUniform4f(xr.rect_uniform, left, top, right, bottom);
|
||||
glUniform1f(xr.cut_disc_uniform, clear_alpha == HALO_XR_COPY_DISC ? 1.0f : 0.0f);
|
||||
glUniform1f(xr.alpha_from_colour_uniform, xr.quad_alpha_from_colour && clear_alpha == HALO_XR_COPY_ALPHA &&
|
||||
layer != HALO_XR_LAYER_EYE_LEFT && layer != HALO_XR_LAYER_EYE_RIGHT ? 1.0f : 0.0f);
|
||||
if ((layer == HALO_XR_LAYER_EYE_LEFT || layer == HALO_XR_LAYER_EYE_RIGHT) && xr.vignette > 0.001f)
|
||||
{
|
||||
const XrFovf *fov = &xr.views[layer - HALO_XR_LAYER_EYE_LEFT].fov;
|
||||
@@ -1331,13 +1707,22 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glBindVertexArray(xr.vertex_array);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glBindSampler(0, xr.sampler);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
if (xr.deferred)
|
||||
{
|
||||
/* (the game's render targets, all a copy reads, have no
|
||||
swizzle: only its uploaded textures get one, xbox_textures.c) */
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, &swizzle_r);
|
||||
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, &swizzle_b);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_RED);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, swizzle_r);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, swizzle_b);
|
||||
}
|
||||
if (clear_alpha == HALO_XR_COPY_OPAQUE)
|
||||
{
|
||||
/* an opaque layer: the game's alpha channel holds other things */
|
||||
@@ -1351,13 +1736,95 @@ int host_xr_copy_layer_region(int layer, uint32_t texture, int clear_alpha, floa
|
||||
glClearColor(0.0f, 0.0f, 0.0f, clear_alpha ? 1.0f : 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
swapchain->filled = 1;
|
||||
if (xr.deferred)
|
||||
{
|
||||
swapchain->held = 1;
|
||||
/* (the eyes, most of the frame's work, on to the GPU while the
|
||||
game draws the rest) */
|
||||
if (layer == HALO_XR_LAYER_EYE_RIGHT)
|
||||
glFlush();
|
||||
return 2;
|
||||
}
|
||||
gl_restore(&saved);
|
||||
|
||||
xrReleaseSwapchainImage(swapchain->handle, &release);
|
||||
swapchain->filled = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* deferred: the images the frame's copies kept, released before it ends */
|
||||
static void release_held(void)
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int layer;
|
||||
|
||||
for (layer = 0; layer < HALO_XR_LAYER_COUNT; layer++)
|
||||
{
|
||||
XrSwapchainImageReleaseInfo release = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
|
||||
struct xr_swapchain *swapchain = &xr.swapchains[layer];
|
||||
|
||||
if (!swapchain->held)
|
||||
continue;
|
||||
swapchain->held = 0;
|
||||
xr_ok(xrReleaseSwapchainImage(swapchain->handle, &release), "xrReleaseSwapchainImage");
|
||||
}
|
||||
if (xr.has_metrics)
|
||||
xr.release_seconds += seconds_now() - start;
|
||||
}
|
||||
|
||||
/* The Steam Frame: Android in a container, without XR_EXT_performance_settings
|
||||
to ask for the clocks, and the scheduler free to put the game's one busy
|
||||
thread, which simulates and draws each frame, on a slow core (its two
|
||||
2.3 GHz ones) now and then: frames late to the compositor, shown twice,
|
||||
the picture doubled as the head turns. The thread that ends the frames is
|
||||
kept on the cores no slower than nine tenths of the fastest. */
|
||||
static void xr_fast_cores(void)
|
||||
{
|
||||
cpu_set_t set;
|
||||
long fastest = 0, speeds[CPU_SETSIZE];
|
||||
int cpu, count = 0;
|
||||
char line[128] = "";
|
||||
|
||||
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
|
||||
{
|
||||
char path[96];
|
||||
FILE *file;
|
||||
|
||||
speeds[cpu] = 0;
|
||||
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu);
|
||||
file = fopen(path, "r");
|
||||
if (!file)
|
||||
continue;
|
||||
if (fscanf(file, "%ld", &speeds[cpu]) != 1)
|
||||
speeds[cpu] = 0;
|
||||
fclose(file);
|
||||
if (speeds[cpu] > fastest)
|
||||
fastest = speeds[cpu];
|
||||
}
|
||||
if (!fastest)
|
||||
{
|
||||
host_logf(HOST_LOG_INFO, "xr: the cores' speeds unknown: the frames' thread left where it runs");
|
||||
return;
|
||||
}
|
||||
CPU_ZERO(&set);
|
||||
for (cpu = 0; cpu < CPU_SETSIZE && cpu < 64; cpu++)
|
||||
{
|
||||
if (speeds[cpu] && speeds[cpu] * 10 >= fastest * 9)
|
||||
{
|
||||
char item[8];
|
||||
|
||||
CPU_SET(cpu, &set);
|
||||
count++;
|
||||
snprintf(item, sizeof(item), " %d", cpu);
|
||||
strcat(line, item);
|
||||
}
|
||||
}
|
||||
if (count && sched_setaffinity(0, sizeof(set), &set) == 0)
|
||||
host_logf(HOST_LOG_INFO, "xr: the frames' thread on the fast cores:%s (fastest %ld MHz)", line, fastest / 1000);
|
||||
else
|
||||
host_logf(HOST_LOG_INFO, "xr: the frames' thread could not be kept on the fast cores");
|
||||
}
|
||||
|
||||
int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
{
|
||||
XrFrameEndInfo end = { XR_TYPE_FRAME_END_INFO };
|
||||
@@ -1375,6 +1842,7 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
if (!xr.frame_begun)
|
||||
return 0;
|
||||
xr.frame_begun = 0;
|
||||
release_held();
|
||||
|
||||
if (xr.frame_state.shouldRender && submit->eyes && xr.views_valid &&
|
||||
xr.swapchains[HALO_XR_LAYER_EYE_LEFT].filled && xr.swapchains[HALO_XR_LAYER_EYE_RIGHT].filled)
|
||||
@@ -1500,11 +1968,83 @@ int host_xr_end_frame(const struct halo_xr_submit *submit)
|
||||
}
|
||||
}
|
||||
|
||||
if (xr.steamvr && !xr.cores_pinned)
|
||||
{
|
||||
xr.cores_pinned = 1;
|
||||
if (!getenv("HALO_XR_ANY_CORE"))
|
||||
xr_fast_cores();
|
||||
}
|
||||
/* (every five seconds, the compositor's counters, and the core the
|
||||
frames' thread is on) */
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
if (now.tv_sec - xr.metrics_logged.tv_sec >= 5)
|
||||
{
|
||||
char line[768] = "";
|
||||
uint32_t counter;
|
||||
|
||||
xr.metrics_logged = now;
|
||||
for (counter = 0; counter < xr.metric_count && counter < 16; counter++)
|
||||
{
|
||||
XrPerformanceMetricsCounterMETA value = { XR_TYPE_PERFORMANCE_METRICS_COUNTER_META };
|
||||
char name[XR_MAX_PATH_LENGTH];
|
||||
uint32_t length = 0;
|
||||
char item[96];
|
||||
|
||||
if (!XR_SUCCEEDED(xr.query_metric(xr.session, xr.metric_paths[counter], &value)) ||
|
||||
!XR_SUCCEEDED(xrPathToString(xr.instance, xr.metric_paths[counter], sizeof(name), &length, name)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
snprintf(item, sizeof(item), " %s=%g", strrchr(name, '/') ? strrchr(name, '/') + 1 : name,
|
||||
(value.counterFlags & XR_PERFORMANCE_METRICS_COUNTER_FLOAT_VALUE_VALID_BIT_META) ?
|
||||
(double)value.floatValue : (double)value.uintValue);
|
||||
if (strlen(line) + strlen(item) < sizeof(line))
|
||||
strcat(line, item);
|
||||
}
|
||||
host_logf(HOST_LOG_INFO, "xr: metrics%s, on core %d", line, sched_getcpu());
|
||||
if (xr.timed_frames)
|
||||
{
|
||||
double frames = (double)xr.timed_frames;
|
||||
char copies[256] = "";
|
||||
int timed;
|
||||
|
||||
for (timed = 0; timed < HALO_XR_LAYER_COUNT; timed++)
|
||||
{
|
||||
char item[24];
|
||||
|
||||
snprintf(item, sizeof(item), " %d:%.2f", timed, xr.copy_seconds[timed] * 1e3 / frames);
|
||||
strcat(copies, item);
|
||||
xr.copy_seconds[timed] = 0.0;
|
||||
}
|
||||
host_logf(HOST_LOG_INFO, "xr: per frame ms: copies (layer:ms)%s; of them acquiring images %.2f; "
|
||||
"releasing them at the end %.2f; xrEndFrame %.2f (copies %s)", copies,
|
||||
xr.acquire_seconds * 1e3 / frames, xr.release_seconds * 1e3 / frames, xr.end_seconds * 1e3 / frames,
|
||||
xr.deferred ? "deferred" : "one by one");
|
||||
xr.acquire_seconds = xr.release_seconds = xr.end_seconds = 0.0;
|
||||
xr.timed_frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end.displayTime = xr.frame_state.predictedDisplayTime;
|
||||
end.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
end.layerCount = layer_count;
|
||||
end.layers = layers;
|
||||
return xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
|
||||
{
|
||||
double start = xr.has_metrics ? seconds_now() : 0.0;
|
||||
int ended = xr_ok(xrEndFrame(xr.session, &end), "xrEndFrame");
|
||||
|
||||
if (xr.has_metrics)
|
||||
{
|
||||
xr.end_seconds += seconds_now() - start;
|
||||
xr.timed_frames++;
|
||||
}
|
||||
return ended;
|
||||
}
|
||||
}
|
||||
|
||||
void host_xr_set_vignette(float strength)
|
||||
@@ -1512,6 +2052,34 @@ void host_xr_set_vignette(float strength)
|
||||
xr.vignette = strength < 0.0f ? 0.0f : strength > 1.0f ? 1.0f : strength;
|
||||
}
|
||||
|
||||
/* the VR options' refresh rates: those the runtime offers (none where it
|
||||
lets none be chosen, as SteamVR, whose rate is SteamOS's), and the one it
|
||||
runs at, by XR_FB_display_refresh_rate or else by its frames' period */
|
||||
int host_xr_refresh_rates(float *rates, int maximum, float *current)
|
||||
{
|
||||
float offered[16];
|
||||
uint32_t count = 0, index;
|
||||
|
||||
*current = 0.0f;
|
||||
if (!xr.initialized)
|
||||
return 0;
|
||||
if (xr.has_refresh_rate)
|
||||
xr.get_refresh_rate(xr.session, current);
|
||||
if (*current <= 0.0f && xr.frame_state.predictedDisplayPeriod > 0)
|
||||
*current = 1e9f / (float)xr.frame_state.predictedDisplayPeriod;
|
||||
if (!xr.has_refresh_rate || !XR_SUCCEEDED(xr.enumerate_refresh_rates(xr.session, 16, &count, offered)))
|
||||
return 0;
|
||||
for (index = 0; index < count && (int)index < maximum; index++)
|
||||
rates[index] = offered[index];
|
||||
return (int)index;
|
||||
}
|
||||
|
||||
void host_xr_set_refresh_rate(float wanted)
|
||||
{
|
||||
if (xr.initialized)
|
||||
refresh_rate_choose(wanted);
|
||||
}
|
||||
|
||||
void host_xr_haptic(int hand, float amplitude, float seconds)
|
||||
{
|
||||
XrHapticActionInfo information = { XR_TYPE_HAPTIC_ACTION_INFO };
|
||||
@@ -1599,7 +2167,7 @@ void host_xr_test(void)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||
window = SDL_CreateWindow("Halo", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
window = SDL_CreateWindow("Ringfall: Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
context = window ? SDL_GL_CreateContext(window) : NULL;
|
||||
if (!context || !SDL_GL_MakeCurrent(window, context))
|
||||
host_fatal("VR test: no OpenGL ES context: %s", SDL_GetError());
|
||||
|
||||
@@ -61,6 +61,8 @@ host_xr_end_frame
|
||||
host_xr_haptic
|
||||
host_xr_set_vignette
|
||||
host_xr_copy_layer_array
|
||||
host_xr_refresh_rates
|
||||
host_xr_set_refresh_rate
|
||||
host_android_path
|
||||
|
||||
host_errno
|
||||
@@ -24,7 +24,7 @@ enum
|
||||
HALO_XR_LAYER_EYE_LEFT = 0,
|
||||
HALO_XR_LAYER_EYE_RIGHT = 1,
|
||||
HALO_XR_LAYER_OVERLAY = 2, /* HUD, menus, cinematics: a flat quad */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the left wrist */
|
||||
HALO_XR_LAYER_WRIST = 3, /* the motion tracker, on the free hand's wrist */
|
||||
HALO_XR_LAYER_SCOPE = 4, /* the zoomed view, a disc over the weapon */
|
||||
HALO_XR_LAYER_CROSSHAIR = 5, /* the weapon's crosshair, where it aims */
|
||||
HALO_XR_LAYER_NAMES = 6, /* players' names, each a quad over a head */
|
||||
@@ -53,6 +53,29 @@ enum
|
||||
HALO_XR_BUTTON_TRIGGER = 1 << 4, /* trigger past its click point */
|
||||
HALO_XR_BUTTON_SQUEEZE = 1 << 5, /* grip past its click point */
|
||||
HALO_XR_BUTTON_THUMBREST = 1 << 6, /* thumb resting (touch) */
|
||||
/* the Steam Frame's controllers' (HALO_XR_LAYOUT_FRAME): the left one's
|
||||
View button and D-pad, either one's bumper (their right one's X, Y and
|
||||
menu button come as the left hand's A, B and MENU: the game's X, Y and
|
||||
Start, as on the Quest) */
|
||||
HALO_XR_BUTTON_VIEW = 1 << 7,
|
||||
HALO_XR_BUTTON_BUMPER = 1 << 8,
|
||||
HALO_XR_BUTTON_DPAD_UP = 1 << 9,
|
||||
HALO_XR_BUTTON_DPAD_DOWN = 1 << 10,
|
||||
HALO_XR_BUTTON_DPAD_LEFT = 1 << 11,
|
||||
HALO_XR_BUTTON_DPAD_RIGHT = 1 << 12,
|
||||
/* the fingers' touch (the hands drawn: vr.c): the thumb on the stick, a
|
||||
button or the rest; the index finger on the trigger; and whether the
|
||||
controller tells the thumb's at all */
|
||||
HALO_XR_BUTTON_THUMB_TOUCH = 1 << 13,
|
||||
HALO_XR_BUTTON_INDEX_TOUCH = 1 << 14,
|
||||
HALO_XR_BUTTON_TOUCH_KNOWN = 1 << 15,
|
||||
};
|
||||
|
||||
/* the controllers' layout, for what the game tells the player */
|
||||
enum
|
||||
{
|
||||
HALO_XR_LAYOUT_TOUCH = 0, /* the Quest's: X, Y and menu on the left */
|
||||
HALO_XR_LAYOUT_FRAME = 1, /* the Steam Frame's: A, B, X, Y and menu on the right, a D-pad on the left */
|
||||
};
|
||||
|
||||
/* halo_xr_hand.flags, halo_xr_view.flags, halo_xr_frame.head_flags */
|
||||
@@ -112,6 +135,7 @@ struct halo_xr_frame
|
||||
struct halo_xr_pose head;
|
||||
struct halo_xr_view views[HALO_XR_EYE_COUNT];
|
||||
struct halo_xr_hand hands[HALO_XR_HAND_COUNT];
|
||||
int controller_layout; /* HALO_XR_LAYOUT_* */
|
||||
};
|
||||
|
||||
/* halo_xr_submit.overlay_mode */
|
||||
@@ -131,15 +155,18 @@ struct halo_xr_submit
|
||||
float overlay_width; /* metres; the height follows from 4:3 */
|
||||
/* the screen's placement in LOCAL space (HALO_XR_OVERLAY_SCREEN) */
|
||||
struct halo_xr_pose screen;
|
||||
/* the wrist layer was filled: shown on the back of the left wrist,
|
||||
this wide (metres), while the wrist is turned to the eyes */
|
||||
/* the wrist layer was filled: shown on the back of the left wrist (of
|
||||
the right one, left-handed: wrist_hand, a HALO_XR_HAND_*), this wide
|
||||
(metres), while the wrist is turned to the eyes */
|
||||
int wrist;
|
||||
float wrist_size;
|
||||
/* its placement as the wrist turned to the eyes reads it: turned this
|
||||
many degrees clockwise, and moved this far right and down (metres) */
|
||||
/* its placement as the left wrist turned to the eyes reads it: turned
|
||||
this many degrees clockwise, and moved this far right and down (metres;
|
||||
on the right wrist, the mirror image) */
|
||||
float wrist_turn;
|
||||
float wrist_right;
|
||||
float wrist_down;
|
||||
int wrist_hand;
|
||||
/* the scope layer was filled: a disc this wide (metres), here in LOCAL
|
||||
space, facing back along the weapon */
|
||||
int scope;
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
# Ringfall: Mjolnir VR's installer for the Steam Frame, run in its Desktop
|
||||
# Mode from the bundle the release's "Install Ringfall: Mjolnir VR" desktop
|
||||
# file downloads (steam_frame_installer.desktop; the bundle: this script,
|
||||
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
|
||||
# steam_frame_xiso.py and the library pictures in grid/). Everything is
|
||||
# answered with dialogs, no keyboard:
|
||||
# - the game in ~/Documents/RingfallMjolnirVR (port/android/STEAM_FRAME.md),
|
||||
# its APK replaced (Lepton installs a newer one at the next start), so
|
||||
# running the installer again updates the game; the folder of the builds
|
||||
# before the name, ~/Documents/HaloCE, becomes it;
|
||||
# - the game data: the maps folder copied out of a disc image of the
|
||||
# original Xbox game (.iso or .xiso) found in Downloads, Documents, the
|
||||
# Desktop or on a memory card, or chosen, unless it is there already;
|
||||
# - the game in the Steam library, with its pictures, unless it is there
|
||||
# already: added by Steam itself (steam://addnonsteamgame, as SteamOS's
|
||||
# "Add to Steam" does), from a desktop file beside the game. Steam is not
|
||||
# closed: it keeps its shortcuts in memory and writes them back, and on the
|
||||
# Steam Frame Desktop Mode runs inside Gaming Mode's session, which ends
|
||||
# with Steam. The shortcut of the builds before the name ("Halo VR") is
|
||||
# left for the player to remove (Steam would write it back).
|
||||
# RINGFALL_UNATTENDED=1 answers every question with its default and prints
|
||||
# instead of showing dialogs (RINGFALL_IMAGE: the disc image to use), for
|
||||
# testing over adb; it never closes Steam.
|
||||
|
||||
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
|
||||
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
|
||||
STEAM_DIR="$HOME/.local/share/Steam"
|
||||
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
|
||||
TITLE="Ringfall: Mjolnir VR"
|
||||
NAME="Ringfall: Mjolnir VR"
|
||||
APK_NAME="RingfallMjolnirVR.apk"
|
||||
LAUNCHER="ringfall-mjolnir-vr.sh"
|
||||
EXE="\"$GAME_DIR/$LAUNCHER\""
|
||||
# (the desktop file Steam adds the game from)
|
||||
NAME_FILE="Ringfall Mjolnir VR"
|
||||
# the builds before the name: their folder and Steam shortcut
|
||||
OLD_DIR="$HOME/Documents/HaloCE"
|
||||
OLD_NAME="Halo VR"
|
||||
|
||||
# what the installer did, for looking into problems: written to /tmp while
|
||||
# it runs, then kept beside the game as install.log
|
||||
LOG="/tmp/ringfall-mjolnir-vr-install.log"
|
||||
: > "$LOG"
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
|
||||
trap keep_log EXIT
|
||||
log "installer from $BUNDLE"
|
||||
|
||||
# whether Steam runs: its pid file (~/.steam/steam.pid), or its client's
|
||||
# process (ubuntu12_32/steam: the process is not always named "steam")
|
||||
steam_running()
|
||||
{
|
||||
local pid
|
||||
pid="$(cat "$HOME/.steam/steam.pid" 2>/dev/null)"
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
|
||||
}
|
||||
|
||||
unattended() { [ "${RINGFALL_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
|
||||
|
||||
info()
|
||||
{
|
||||
if unattended; then printf "info: %b\n" "$1"; else zenity --info --title="$TITLE" --width=460 --text="$1"; fi
|
||||
}
|
||||
|
||||
fail()
|
||||
{
|
||||
log "error: $1"
|
||||
if unattended; then printf "error: %b\n" "$1" >&2; else zenity --error --title="$TITLE" --width=460 --text="$1"; fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# yes/no; unattended: the default given ($2, "yes" or "no")
|
||||
ask()
|
||||
{
|
||||
if unattended; then [ "$2" = yes ]; return; fi
|
||||
zenity --question --title="$TITLE" --width=460 --text="$1"
|
||||
}
|
||||
|
||||
# the disc image the player chooses
|
||||
choose_image()
|
||||
{
|
||||
if unattended; then return 1; fi
|
||||
zenity --file-selection --title="Choose a disc image of Halo: Combat Evolved for the Xbox (.iso or .xiso)" \
|
||||
--filename="$HOME/Downloads/" --file-filter="Disc images | *.iso *.ISO *.xiso *.XISO" \
|
||||
--file-filter="All files | *"
|
||||
}
|
||||
|
||||
# the disc images near at hand, largest first (the game's image is a few GB;
|
||||
# anything under 500 MB is not one)
|
||||
find_images()
|
||||
{
|
||||
find "$HOME/Downloads" "$HOME/Documents" "$HOME/Desktop" /run/media -maxdepth 3 -type f \
|
||||
\( -iname '*.iso' -o -iname '*.xiso' \) -size +500M -printf '%s\t%p\n' 2>/dev/null |
|
||||
sort -rn | cut -f2-
|
||||
}
|
||||
|
||||
extract_maps()
|
||||
{
|
||||
local image="$1" log
|
||||
log="$(mktemp)"
|
||||
if unattended; then
|
||||
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" | grep '^#' | tail -n 1
|
||||
status=${PIPESTATUS[0]}
|
||||
else
|
||||
python3 "$BUNDLE/steam_frame_xiso.py" "$image" "$GAME_DIR" 2>"$log" |
|
||||
zenity --progress --title="$TITLE" --width=460 --auto-close --no-cancel \
|
||||
--text="Copying the game data out of the disc image..."
|
||||
status=${PIPESTATUS[0]}
|
||||
fi
|
||||
if [ "$status" != 0 ]; then
|
||||
reason="$(sed -n 's/^error: //p' "$log" | head -n 1)"
|
||||
rm -f "$log"
|
||||
fail "The game data could not be copied out of\n$image\n\n${reason:-Unknown error.}"
|
||||
fi
|
||||
rm -f "$log"
|
||||
}
|
||||
|
||||
# the Steam users' shortcut files (userdata/0 is no user)
|
||||
shortcut_files()
|
||||
{
|
||||
local config
|
||||
for config in "$STEAM_DIR"/userdata/*/config; do
|
||||
case "$config" in */userdata/0/*) continue ;; esac
|
||||
[ -f "$config/shortcuts.vdf" ] && echo "$config/shortcuts.vdf"
|
||||
done
|
||||
}
|
||||
|
||||
# whether a Steam user's library holds the launcher given
|
||||
have_shortcut_to()
|
||||
{
|
||||
local file
|
||||
while read -r file; do
|
||||
grep -qaF "$1" "$file" && return 0
|
||||
done < <(shortcut_files)
|
||||
return 1
|
||||
}
|
||||
|
||||
have_shortcut() { have_shortcut_to "$GAME_DIR/$LAUNCHER"; }
|
||||
|
||||
# the id Steam gave the shortcut whose Exe is $2, in shortcuts.vdf $1 (Steam
|
||||
# picks it; its library pictures in config/grid are named after it)
|
||||
shortcut_id()
|
||||
{
|
||||
python3 - "$1" "$2" <<'PYTHON'
|
||||
import struct, sys
|
||||
|
||||
path, exe = sys.argv[1], sys.argv[2].encode()
|
||||
|
||||
# Steam's binary VDF: 0x00 a map, 0x01 a string, 0x02 an int32, 0x08 a map's end
|
||||
def read_map(data, at):
|
||||
items = []
|
||||
while True:
|
||||
kind = data[at]; at += 1
|
||||
if kind == 0x08:
|
||||
return items, at
|
||||
end = data.index(b"\0", at); key = data[at:end].lower(); at = end + 1
|
||||
if kind == 0x00:
|
||||
value, at = read_map(data, at)
|
||||
elif kind == 0x01:
|
||||
end = data.index(b"\0", at); value = data[at:end]; at = end + 1
|
||||
elif kind == 0x02:
|
||||
value = struct.unpack_from("<I", data, at)[0]; at += 4
|
||||
else:
|
||||
raise ValueError("unknown VDF type %d" % kind)
|
||||
items.append((key, value))
|
||||
|
||||
with open(path, "rb") as file:
|
||||
top, _ = read_map(file.read(), 0)
|
||||
for key, shortcuts in top:
|
||||
if key == b"shortcuts":
|
||||
for _, entry in shortcuts:
|
||||
fields = dict(entry)
|
||||
if fields.get(b"exe") == exe and b"appid" in fields:
|
||||
print(fields[b"appid"])
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
PYTHON
|
||||
}
|
||||
|
||||
# the game added to the library of the Steam user logged in, by Steam (it
|
||||
# must run), from a desktop file beside the game; then its pictures
|
||||
add_shortcut()
|
||||
{
|
||||
local desktop="$GAME_DIR/$NAME_FILE.desktop" url file id
|
||||
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
|
||||
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
|
||||
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
|
||||
log "asking Steam: $url"
|
||||
# (Steam's own "Add to Steam" touches this first: steamos-add-to-steam)
|
||||
touch /tmp/addnonsteamgamefile
|
||||
steam "$url" >/dev/null 2>&1 &
|
||||
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
|
||||
have_shortcut || return 1
|
||||
while read -r file; do
|
||||
id="$(shortcut_id "$file" "$EXE")" || continue
|
||||
log "shortcut $id in $file"
|
||||
mkdir -p "$(dirname "$file")/grid"
|
||||
cp "$BUNDLE/grid/library.png" "$(dirname "$file")/grid/${id}.png"
|
||||
cp "$BUNDLE/grid/libraryp.png" "$(dirname "$file")/grid/${id}p.png"
|
||||
cp "$BUNDLE/grid/library_hero.png" "$(dirname "$file")/grid/${id}_hero.png"
|
||||
cp "$BUNDLE/grid/library_logo.png" "$(dirname "$file")/grid/${id}_logo.png"
|
||||
done < <(shortcut_files)
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- the game
|
||||
|
||||
[ -x "$LEPTON" ] || fail "Lepton, which runs Android games on the Steam Frame, was not found.\n\nStart any Android game from the Steam store once, then run this installer again."
|
||||
for file in "$APK_NAME" "$LAUNCHER" steam_frame_xiso.py grid/library.png; do
|
||||
[ -f "$BUNDLE/$file" ] || fail "The installer is incomplete ($file is missing). Download it again."
|
||||
done
|
||||
|
||||
# the builds before the name: their folder becomes the game's (settings,
|
||||
# saves and game data kept), without their APK and launcher
|
||||
if [ -d "$OLD_DIR" ] && [ ! -e "$GAME_DIR" ]; then
|
||||
log "renaming $OLD_DIR to $GAME_DIR"
|
||||
mv "$OLD_DIR" "$GAME_DIR" || fail "Could not rename $OLD_DIR to $GAME_DIR."
|
||||
rm -f "$GAME_DIR/HaloVR.apk" "$GAME_DIR/halo-vr.sh"
|
||||
fi
|
||||
|
||||
if [ -f "$GAME_DIR/$APK_NAME" ] || [ -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
ask "$NAME is installed already. Update it to this version?\n\n(Your settings, saves and game data stay.)" yes || exit 0
|
||||
else
|
||||
ask "This installs $NAME on your Steam Frame.\n\nYou need your own disc image of Halo: Combat Evolved for the original Xbox (an .iso or .xiso file, any version), for example in Downloads.\n\nContinue?" yes || exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$GAME_DIR/maps" || fail "Could not create $GAME_DIR."
|
||||
# (the game runs in Lepton's container as a user of its own)
|
||||
chmod 777 "$GAME_DIR" "$GAME_DIR/maps"
|
||||
cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCHER.tmp" &&
|
||||
mv -f "$GAME_DIR/$LAUNCHER.tmp" "$GAME_DIR/$LAUNCHER" || fail "Could not write $GAME_DIR/$LAUNCHER."
|
||||
# (newer than the installed one: Lepton installs it at the next start)
|
||||
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
|
||||
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
|
||||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
|
||||
|
||||
# --- the game data
|
||||
|
||||
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
image="${RINGFALL_IMAGE:-}"
|
||||
if [ -z "$image" ]; then
|
||||
mapfile -t images < <(find_images)
|
||||
for candidate in "${images[@]}"; do
|
||||
if ask "Use this disc image?\n\n$candidate" yes; then
|
||||
image="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -z "$image" ]; then
|
||||
if [ ${#images[@]} = 0 ]; then
|
||||
info "No disc image was found in Downloads, Documents, on the Desktop or a memory card.\n\nChoose it in the next window."
|
||||
fi
|
||||
image="$(choose_image)" || fail "No disc image was chosen. Run the installer again once it is on the Frame (for example in Downloads)."
|
||||
fi
|
||||
log "extracting $image"
|
||||
extract_maps "$image"
|
||||
chmod -R a+rwX "$GAME_DIR/maps"
|
||||
image_note="\n\nThe disc image itself is no longer needed:\n$image"
|
||||
fi
|
||||
|
||||
# --- Steam
|
||||
|
||||
if have_shortcut; then
|
||||
log "the Steam shortcut is there"
|
||||
elif unattended; then
|
||||
echo "info: the Steam shortcut is missing (not added unattended)"
|
||||
elif ! steam_running; then
|
||||
info "Steam is not running, so $NAME could not be added to your library.\n\nStart Steam and run the installer again."
|
||||
else
|
||||
add_shortcut || fail "Steam did not add $NAME to your library. Run the installer again; if it happens again, add\n$GAME_DIR/$NAME_FILE.desktop\nwith Steam's \"Add a Non-Steam Game\"."
|
||||
fi
|
||||
|
||||
if unattended; then
|
||||
echo "info: installed in $GAME_DIR"
|
||||
exit 0
|
||||
fi
|
||||
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
|
||||
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
|
||||
fi
|
||||
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
|
||||
steamos-session-select gamescope >/dev/null 2>&1 ||
|
||||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
# Ringfall: Mjolnir VR's installer for the Steam Frame
|
||||
# (port/android/STEAM_FRAME.md): a release asset,
|
||||
# "Install-Ringfall-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
|
||||
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
|
||||
# the repository) and runs its installer, steam_frame_install.sh.
|
||||
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
|
||||
# escaped)
|
||||
Type=Application
|
||||
Name=Install Ringfall: Mjolnir VR
|
||||
Comment=Installs Ringfall: Mjolnir VR for the Steam Frame, or updates it
|
||||
Icon=steam
|
||||
Terminal=false
|
||||
Exec=bash -c "if curl -fL --retry 3 -o /tmp/ringfall-mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/ringfall-mjolnir-vr-steam-frame.zip && rm -rf /tmp/ringfall-mjolnir-vr-install && python3 -m zipfile -e /tmp/ringfall-mjolnir-vr-steam-frame.zip /tmp/ringfall-mjolnir-vr-install; then exec bash /tmp/ringfall-mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Ringfall: Mjolnir VR' --text='Ringfall: Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Ringfall: Mjolnir VR on the Steam Frame: Lepton runs the APK in a container of its own,
|
||||
# and installs it anew whenever the APK is newer than the installed one
|
||||
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/RingfallMjolnirVR.
|
||||
# (a container of its own, not the shortcut's id: with that, SteamVR's
|
||||
# client in the container waited on xrCreateInstance)
|
||||
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
|
||||
# (what Steam gives an Android title of its own, and a non-Steam game not:
|
||||
# the app's data in the container links to the compat data path's
|
||||
# internal/, a dangling link without it; and the shader paths: Lepton reads
|
||||
# the dump path against the other)
|
||||
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
|
||||
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
|
||||
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
|
||||
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
|
||||
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
|
||||
DATA="$HOME/Documents/RingfallMjolnirVR"
|
||||
APK="$DATA/RingfallMjolnirVR.apk"
|
||||
LOG="$DATA/launcher.log"
|
||||
# (written by the game when the player quits it: no start again)
|
||||
QUIT="$DATA/quit_requested.txt"
|
||||
rm -f "$QUIT"
|
||||
CONTEXT="steamlaunch-$SteamAppId"
|
||||
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
log "start (pid $$, parent $PPID)"
|
||||
|
||||
# Steam's "Exit game" ends the game's processes as Steam knows them (this
|
||||
# script, under Steam's reaper), not the container, which podman runs: a
|
||||
# signal here is passed on to Lepton, and a watcher of its own takes the
|
||||
# container down once this script is gone (killed)
|
||||
stopping=0
|
||||
child=0
|
||||
stop()
|
||||
{
|
||||
log "signal $1"
|
||||
stopping=1
|
||||
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
|
||||
}
|
||||
trap 'stop TERM' TERM
|
||||
trap 'stop INT' INT
|
||||
trap 'stop HUP' HUP
|
||||
|
||||
script=$$
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
|
||||
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
|
||||
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
# the game's log while it runs (Lepton's logcat of its container), for
|
||||
# looking into problems: logcat.txt, anew each start
|
||||
LOGCAT="$DATA/logcat.txt"
|
||||
: > "$LOGCAT"
|
||||
setsid bash -c '
|
||||
while kill -0 '"$script"' 2>/dev/null; do
|
||||
"'"$LEPTON"'" logcat "'"$CONTEXT"'" 2>/dev/null | grep -v "^ERROR: \|^steamlaunch-\|^$" >> "'"$LOGCAT"'"
|
||||
sleep 3
|
||||
done
|
||||
' </dev/null >/dev/null 2>&1 &
|
||||
|
||||
run()
|
||||
{
|
||||
"$LEPTON" waitforexitandrun -- "$APK" &
|
||||
child=$!
|
||||
# (wait returns early when a signal comes: until Lepton is gone)
|
||||
while kill -0 "$child" 2>/dev/null; do
|
||||
wait "$child"
|
||||
done
|
||||
child=0
|
||||
}
|
||||
|
||||
# (a new APK is only installed on its first start, not started; and a start
|
||||
# right after a game that was ended hard can fail in SteamVR, which then
|
||||
# needs a moment: again, up to three times, while it ends within 30 s)
|
||||
for attempt in 1 2 3; do
|
||||
started=$SECONDS
|
||||
run
|
||||
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
|
||||
break
|
||||
fi
|
||||
log "ended within $((SECONDS - started)) s: again"
|
||||
sleep 5
|
||||
done
|
||||
rm -f "$QUIT"
|
||||
log "end"
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
|
||||
disc's ".iso") on the Steam Frame's SteamOS side, for its installer
|
||||
(steam_frame_install.sh). It reads the image as the app's own import does
|
||||
(app/src/main/java/com/halo/decomp/XisoExtractor.java, which this follows;
|
||||
the desktop games' is port/linux/src/xiso.c).
|
||||
|
||||
steam_frame_xiso.py <image> <destination>
|
||||
|
||||
writes <destination>/maps (through maps.partial, so an interrupted copy
|
||||
never leaves maps/ui.map behind) and prints its progress for zenity
|
||||
--progress: a percentage on a line of its own, and "# <text>" lines. On
|
||||
failure it prints "error: <reason the player can act on>" to stderr and
|
||||
exits 1.
|
||||
|
||||
The image's file system is XDVDFS, read as extract-xiso does
|
||||
(https://github.com/XboxDev/extract-xiso, extract-xiso.c, whose format
|
||||
handling this follows; its license is below and in
|
||||
port/third_party/extract-xiso/LICENSE.TXT): 2048-byte sectors; a volume
|
||||
descriptor at 0x10000 that starts and ends with "MICROSOFT*XBOX*MEDIA" and
|
||||
gives the root directory's sector and size; and directories whose entries
|
||||
form a binary tree (each entry: left and right subtree offsets in 4-byte
|
||||
units, the start sector, the size, attributes, the name's length and the
|
||||
name). Images made from a whole disc put the game partition further in, at
|
||||
one of the offsets below.
|
||||
|
||||
Some parts of this code are copyright in@fishtank.com. This product
|
||||
includes software developed by in <in@fishtank.com>.
|
||||
|
||||
Copyright (c) 2003 in <in@fishtank.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
|
||||
This product includes software developed by in <in@fishtank.com>.
|
||||
|
||||
4. Neither the name of "in" nor the email address "in@fishtank.com"
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
AUTHOR OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
SECTOR_SIZE = 2048
|
||||
VOLUME_DESCRIPTOR_OFFSET = 0x10000
|
||||
ENTRY_HEADER_SIZE = 14
|
||||
ATTRIBUTE_DIRECTORY = 0x10
|
||||
# directory tables are a few sectors; anything much larger is not one
|
||||
MAXIMUM_DIRECTORY_SIZE = 4 << 20
|
||||
MAXIMUM_FILES = 256
|
||||
COPY_BUFFER_SIZE = 1 << 20
|
||||
VOLUME_MAGIC = b"MICROSOFT*XBOX*MEDIA"
|
||||
# where the game partition starts: a plain image, and whole-disc images
|
||||
# (extract-xiso's GLOBAL, XGD3 and XGD1 offsets)
|
||||
PARTITION_OFFSETS = (0, 0x0FD90000, 0x02080000, 0x18300000)
|
||||
|
||||
|
||||
class ExtractError(Exception):
|
||||
"""a reason the player can act on"""
|
||||
|
||||
|
||||
def read_at(image, offset, size):
|
||||
image.seek(offset)
|
||||
data = image.read(size)
|
||||
if len(data) != size:
|
||||
raise ExtractError("Could not read the disc image (is it complete?).")
|
||||
return data
|
||||
|
||||
|
||||
def find_volume(image):
|
||||
"""the game partition's offset, and the root directory's sector and size"""
|
||||
for offset in PARTITION_OFFSETS:
|
||||
try:
|
||||
descriptor = read_at(image, offset + VOLUME_DESCRIPTOR_OFFSET, SECTOR_SIZE)
|
||||
except ExtractError:
|
||||
continue
|
||||
if descriptor[:20] != VOLUME_MAGIC or descriptor[0x7EC:0x7EC + 20] != VOLUME_MAGIC:
|
||||
continue
|
||||
sector, size = struct.unpack_from("<II", descriptor, 20)
|
||||
return offset, sector, size
|
||||
raise ExtractError("This is not an Xbox disc image.")
|
||||
|
||||
|
||||
def read_directory(image, partition, sector, size, what):
|
||||
if size <= 0 or size > MAXIMUM_DIRECTORY_SIZE:
|
||||
raise ExtractError(what)
|
||||
return read_at(image, partition + sector * SECTOR_SIZE, size)
|
||||
|
||||
|
||||
def walk(table, offset, depth, directories, out, visited):
|
||||
"""collects the entries (name, sector, size) of the subtree at offset
|
||||
(4-byte units), files or directories as asked"""
|
||||
at = offset * 4
|
||||
# a malformed table must not loop or run off its end
|
||||
visited[0] += 1
|
||||
if depth > 64 or visited[0] > 4096 or at + ENTRY_HEADER_SIZE > len(table):
|
||||
return
|
||||
left, right, sector, size, attributes, name_length = struct.unpack_from("<HHIIBB", table, at)
|
||||
# 0xFFFF: padding, an empty directory
|
||||
if left == 0xFFFF:
|
||||
return
|
||||
if left:
|
||||
walk(table, left, depth + 1, directories, out, visited)
|
||||
is_directory = bool(attributes & ATTRIBUTE_DIRECTORY)
|
||||
if (at + ENTRY_HEADER_SIZE + name_length <= len(table) and name_length > 0
|
||||
and is_directory == directories and len(out) < MAXIMUM_FILES):
|
||||
name = table[at + ENTRY_HEADER_SIZE:at + ENTRY_HEADER_SIZE + name_length].decode("latin-1")
|
||||
# (as extract-xiso refuses them: no name may leave the folder)
|
||||
if name not in (".", "..") and "/" not in name and "\\" not in name:
|
||||
out.append((name, sector, size))
|
||||
if right:
|
||||
walk(table, right, depth + 1, directories, out, visited)
|
||||
|
||||
|
||||
def extract(image_path, destination):
|
||||
with open(image_path, "rb") as image:
|
||||
partition, root_sector, root_size = find_volume(image)
|
||||
|
||||
# the root's maps folder
|
||||
table = read_directory(image, partition, root_sector, root_size,
|
||||
"The disc image's file system is damaged.")
|
||||
directories = []
|
||||
walk(table, 0, 0, True, directories, [0])
|
||||
maps = [entry for entry in directories if entry[0].lower() == "maps"]
|
||||
if not maps:
|
||||
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
|
||||
|
||||
# its files
|
||||
table = read_directory(image, partition, maps[-1][1], maps[-1][2],
|
||||
"The disc image's maps folder is damaged.")
|
||||
files = []
|
||||
walk(table, 0, 0, False, files, [0])
|
||||
if not any(name.lower() == "ui.map" for name, _, _ in files):
|
||||
raise ExtractError("The disc image's maps folder has no ui.map: it is not a Halo disc.")
|
||||
total = sum(size for _, _, size in files) or 1
|
||||
|
||||
partial = os.path.join(destination, "maps.partial")
|
||||
finished = os.path.join(destination, "maps")
|
||||
try:
|
||||
os.makedirs(partial, exist_ok=True)
|
||||
except OSError:
|
||||
raise ExtractError("Could not create %s." % partial)
|
||||
done = 0
|
||||
shown = -1
|
||||
for name, sector, size in files:
|
||||
path = os.path.join(partial, name)
|
||||
offset = partition + sector * SECTOR_SIZE
|
||||
remaining = size
|
||||
print("# Copying maps/%s" % name, flush=True)
|
||||
try:
|
||||
with open(path, "wb") as out:
|
||||
while remaining > 0:
|
||||
count = min(remaining, COPY_BUFFER_SIZE)
|
||||
out.write(read_at(image, offset, count))
|
||||
offset += count
|
||||
remaining -= count
|
||||
done += count
|
||||
# (zenity closes at 100: that is left to the caller)
|
||||
percent = min(99, done * 100 // total)
|
||||
if percent != shown:
|
||||
print(percent, flush=True)
|
||||
shown = percent
|
||||
except ExtractError:
|
||||
raise ExtractError("Could not read %s from the disc image (is it complete?)." % name)
|
||||
except OSError:
|
||||
raise ExtractError("Could not write %s (is the storage full?)." % path)
|
||||
|
||||
# (the maps folder may be there, empty)
|
||||
try:
|
||||
os.makedirs(finished, exist_ok=True)
|
||||
for name, _, _ in files:
|
||||
os.replace(os.path.join(partial, name), os.path.join(finished, name))
|
||||
os.rmdir(partial)
|
||||
except OSError as error:
|
||||
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: steam_frame_xiso.py <image> <destination>", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
extract(sys.argv[1], sys.argv[2])
|
||||
except ExtractError as error:
|
||||
print("error: %s" % error, file=sys.stderr)
|
||||
return 1
|
||||
except OSError as error:
|
||||
print("error: Could not open the disc image (%s)." % error.strerror, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
FDM_INJECTION_STUB.C
|
||||
|
||||
An OpenXR API layer that does nothing, named as the Steam Frame's
|
||||
XR_APILAYER_VALVE_fdm_injection names its library
|
||||
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
|
||||
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
|
||||
Android app (in Lepton) cannot load the system's library from its own
|
||||
namespace, and the Khronos loader then fails xrCreateInstance altogether;
|
||||
nor does the Android loader read the variable that would turn the layer
|
||||
off. With this one in the app's libraries the loader finds a layer of that
|
||||
name, which hands every call on to the next. (The real layer adds fragment
|
||||
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
|
||||
manifest names it and it is never loaded.
|
||||
*/
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function)
|
||||
{
|
||||
if (!next_get_instance_proc_addr)
|
||||
return XR_ERROR_HANDLE_INVALID;
|
||||
return next_get_instance_proc_addr(instance, name, function);
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
|
||||
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
|
||||
{
|
||||
XrApiLayerCreateInfo next_layer_info;
|
||||
|
||||
if (!layer_info || !layer_info->nextInfo)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
|
||||
next_layer_info = *layer_info;
|
||||
next_layer_info.nextInfo = layer_info->nextInfo->next;
|
||||
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default")))
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
|
||||
const char *layer_name, XrNegotiateApiLayerRequest *request)
|
||||
{
|
||||
(void)layer_name;
|
||||
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
|
||||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
|
||||
{
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
|
||||
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
|
||||
XR_CURRENT_API_VERSION;
|
||||
request->getInstanceProcAddr = layer_get_instance_proc_addr;
|
||||
request->createApiLayerInstance = layer_create_api_layer_instance;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
@@ -314,7 +314,7 @@
|
||||
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
|
||||
"source_scale": 4,
|
||||
"source": [
|
||||
-34,
|
||||
-2,
|
||||
108
|
||||
],
|
||||
"clip": [
|
||||
@@ -380,7 +380,7 @@
|
||||
"svg": "hud_xbox_order__bitmaps__combined__hud_ammo_meters.svg",
|
||||
"source_scale": 4,
|
||||
"source": [
|
||||
-25,
|
||||
-5,
|
||||
312
|
||||
],
|
||||
"clip": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<defs>
|
||||
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="167.994" cy="346.437" r="165.922"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
|
||||
<g id="art">
|
||||
<path d="M167.994,346.437 L69.695,202.598 L266.292,202.598 Z" fill="url(#cone)"/>
|
||||
<path d="M167.994,346.437 L54.604,180.515 L281.384,180.515 Z" fill="url(#cone)"/>
|
||||
<circle cx="167.994" cy="345.987" r="53.201" fill="#878787"/>
|
||||
<path d="M167.994,346.437 L137.767,302.207 A53.201,53.201 0 0 1 198.220,302.207 Z" fill="url(#coned)"/>
|
||||
<!-- "15m" pixel label -->
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -2,7 +2,7 @@
|
||||
<defs>
|
||||
<radialGradient id="cone" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#000"/></radialGradient><radialGradient id="coned" gradientUnits="userSpaceOnUse" cx="126.000" cy="130.342" r="128.840"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#878787"/></radialGradient>
|
||||
<g id="art">
|
||||
<path d="M126.000,130.342 L49.632,18.677 L202.368,18.677 Z" fill="url(#cone)"/>
|
||||
<path d="M126.000,130.342 L37.886,1.502 L214.114,1.502 Z" fill="url(#cone)"/>
|
||||
<circle cx="126.000" cy="129.985" r="41.776" fill="#878787"/>
|
||||
<path d="M126.000,130.342 L102.251,95.616 A41.776,41.776 0 0 1 149.749,95.616 Z" fill="url(#coned)"/>
|
||||
<!-- "15m" pixel label -->
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -51,7 +51,7 @@ until it is redrawn.
|
||||
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` |
|
||||
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` |
|
||||
|
||||
13 of 147 frames are placeholders.
|
||||
13 of 156 frames are placeholders.
|
||||
|
||||
The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which
|
||||
the Xbox's map has.
|
||||
@@ -10,11 +10,10 @@ do (save a setting, join a game) does not happen; the screens open and close as
|
||||
|
||||
| Function | Uses | |
|
||||
| --- | --- | --- |
|
||||
| `common button bar update` | 24 | PC function |
|
||||
| `common button bar update` | 25 | PC function |
|
||||
| `direct ip connect init` | 1 | PC function |
|
||||
| `direct ip connect update` | 1 | PC function |
|
||||
| `direct ip edit field` | 4 | PC function |
|
||||
| `gamespy screen dispose` | 1 | PC function |
|
||||
| `gamespy screen update` | 1 | PC function |
|
||||
| `gamespy select button` | 8 | PC function |
|
||||
| `gamespy select header` | 22 | PC function |
|
||||
@@ -22,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
|
||||
| `gamespy update filter settings` | 2 | PC function |
|
||||
| `gt edit list update` | 1 | PC function |
|
||||
| `gt select list update` | 1 | PC function |
|
||||
| `mouse spinner 1wide click` | 64 | PC function |
|
||||
| `mouse spinner 1wide click` | 66 | PC function |
|
||||
| `mp map list update` | 1 | PC function |
|
||||
| `mp prof init teamplay options` | 1 | PC function |
|
||||
| `mp prof init vehicle options` | 1 | PC function |
|
||||
@@ -34,4 +33,4 @@ do (save a setting, join a game) does not happen; the screens open and close as
|
||||
| `ss edit server name` | 2 | PC function |
|
||||
| `ss start game` | 2 | PC function |
|
||||
|
||||
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
|
||||
Wired: `campaign menu continue`, `campaign menu init`, `controls back handler`, `controls begin binding`, `controls screen change set`, `controls screen defaults`, `controls screen init`, `controls update menu`, `difficulty item select`, `direct ip connect go`, `emit custom activation event`, `gamespy back handler`, `gamespy dismiss error`, `gamespy dismiss filters`, `gamespy screen dispose`, `gamespy screen init`, `load game list update`, `load game menu activated`, `load game menu delete finish`, `load game menu delete request`, `load game menu dispose`, `load game menu init`, `main menu quit game`, `mouse emit accept event`, `mouse emit back event`, `mouse emit x event`, `mp type set mode`, `profile manager select`, `profile set edit begin`, `single prev cl item activated`, `solo map list update`.
|
||||
@@ -309,13 +309,37 @@
|
||||
<bitmap name="main_menu/solo_level_select/header_load_level">
|
||||
<frame png="ce/shell/main_menu/solo_level_select/header_load_level__0.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="main_menu/multiplayer_type_select/coop/header_player_2">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/coop/header_player_2.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<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>
|
||||
<bitmap name="main_menu/multiplayer_type_select/lobby/header_add_player">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="main_menu/multiplayer_type_select/lobby/header_lobby">
|
||||
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png" width="512" height="64"/>
|
||||
</bitmap>
|
||||
<bitmap name="pause/pausebox_left">
|
||||
<frame png="ce/port/pause/pausebox_left__0.png" width="16" height="256"/>
|
||||
<frame png="ce/port/pause/pausebox_left__1.png" width="16" height="256"/>
|
||||
</bitmap>
|
||||
<bitmap name="pause/pausebox_center">
|
||||
<frame png="ce/port/pause/pausebox_center__0.png" width="4" height="256"/>
|
||||
<frame png="ce/port/pause/pausebox_center__1.png" width="4" height="256"/>
|
||||
</bitmap>
|
||||
<bitmap name="pause/pausebox_right">
|
||||
<frame png="ce/port/pause/pausebox_right__0.png" width="16" height="256"/>
|
||||
<frame png="ce/port/pause/pausebox_right__1.png" width="16" height="256"/>
|
||||
</bitmap>
|
||||
<bitmap name="blank">
|
||||
<frame png="ce/port/blank__0.png" width="4" height="4"/>
|
||||
</bitmap>
|
||||
</menus>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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/multiplayer_type_coop_item" type="text" left="51" width="232" height="32" bitmap="bitmaps/list_item_bkd" string_list="main_menu/multiplayer_type_select/multiplayer_options" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
|
||||
<on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
|
||||
<on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/player_2_profile_list"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/header_player_2"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/player_2_help" x="20" y="416"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/header_player_2" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/coop/header_player_2">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/player_2_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
|
||||
<data input="3wide player profile list update"/>
|
||||
<on event="created" run="port coop player 2 list initialize"/>
|
||||
<on event="deleted" run="player profile list dispose"/>
|
||||
<on event="custom_activation" run="port coop player 2" open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="a" run="single prev cl item activated"/>
|
||||
<on event="start" run="single prev cl item activated"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/player_2_help" type="text" width="350" height="24" text="Player 2: choose with your controller." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/coop/player_2_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
|
||||
<data input="common button bar update"/>
|
||||
<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>
|
||||
<child widget="common_button_back" x="510" y="1"/>
|
||||
</widget>
|
||||
</menus>
|
||||
@@ -1,6 +1,57 @@
|
||||
<?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">
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/ticker_rules_info" x="80"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/join_game/join_game_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/join_game_button_update" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_refresh" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/button_clipboard" x="128" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_filters" x="256" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/join_game/join_game_button_join" x="384" y="1"/>
|
||||
|
||||
@@ -1,34 +1,93 @@
|
||||
<?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/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_screen" width="640" height="480" flags="pass_unhandled_to_focused_child main_menu_if_no_history" bitmap="bitmaps/gradient">
|
||||
<on event="created" run="port lobby open"/>
|
||||
<on event="created" run="net server accept conx"/>
|
||||
<on event="created" run="net server allow start"/>
|
||||
<on event="b" run="net game unjoin player" back="true"/>
|
||||
<on event="back" run="net game unjoin player" back="true"/>
|
||||
<on event="b" run="port lobby leave" back="true"/>
|
||||
<on event="back" run="port lobby leave" back="true"/>
|
||||
<on event="start" run="port lobby join" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
|
||||
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_list"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/header_lobby" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_lobby">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_join_help" type="text" controller="1" left="355" top="446" width="275" height="24" font="ui\small_ui" color="#FF2896FF" align="right" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
|
||||
<data input="net splitscreen prejoin players"/>
|
||||
<data input="port lobby update"/>
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
|
||||
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
|
||||
<child widget="main_menu/new_select/list_item_4" x="20" y="193"/>
|
||||
<child widget="main_menu/new_select/list_item_5" x="20" y="223"/>
|
||||
<child widget="main_menu/new_select/list_item_6" x="20" y="253"/>
|
||||
<child widget="main_menu/new_select/list_item_7" x="20" y="283"/>
|
||||
<child widget="main_menu/new_select/list_item_8" x="20" y="313"/>
|
||||
<child widget="main_menu/new_select/list_item_9" x="20" y="343"/>
|
||||
<child widget="main_menu/new_select/list_item_10" x="20" y="373"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_0" x="20" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_3" x="20" y="163"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_4" x="20" y="193"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_5" x="20" y="223"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_6" x="20" y="253"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_7" x="20" y="283"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_8" x="20" y="313"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_9" x="20" y="343"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/list_item_10" x="20" y="373"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_1" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_2" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_3" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_4" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_5" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_6" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_7" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_8" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_9" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/list_item_10" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<on event="left right" run="swap player team"/>
|
||||
<child widget="main_menu/new_select/list_item_text" x="25"/>
|
||||
<child widget="main_menu/new_select/list_item_arrows"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_desc" width="640" height="480">
|
||||
<child widget="main_menu/current_profile_name"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_right_item" x="22" y="2"/>
|
||||
@@ -36,6 +95,8 @@
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_right_item" controller="1" left="406" top="75" width="162" height="326" bitmap="bitmaps/spinner_list_3_wide_item_background">
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_pic"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_map_name"/>
|
||||
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
|
||||
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_game_data"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_map_pic" controller="1" left="419" top="87" width="140" height="114" bitmap="ui\shell\bitmaps\mp_map_grafix">
|
||||
@@ -45,10 +106,16 @@
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_game_data" type="text" controller="1" left="417" top="250" width="146" height="144" font="ui\small_ui" color="#FF2896FF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_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/lobby/lobby_button_team" x="250" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="380" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_team" x="120" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_start" x="250" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_add" x="380" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_button_leave" x="510" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="port lobby add player"/>
|
||||
<on event="start" run="port lobby add player"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/lobby_button_team" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="SWITCH TEAM" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="swap player team"/>
|
||||
<on event="start" run="swap player team"/>
|
||||
@@ -64,14 +131,46 @@
|
||||
<on event="start" run="mouse emit back event"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
<on event="created" run="net server defer start"/>
|
||||
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_list"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/header_add_player"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/player_profile_help" x="20" y="416"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/header_add_player" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/lobby/header_add_player">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
|
||||
<data input="3wide player profile list update"/>
|
||||
<on event="created" run="port lobby player list initialize"/>
|
||||
<on event="deleted" run="player profile list dispose"/>
|
||||
<on event="custom_activation" run="port lobby player choose" back="true" branch="true"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
|
||||
<child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/player_profile_help" type="text" width="350" height="24" text="The new player's profile." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
<on event="b" back="true"/>
|
||||
<on event="back" back="true"/>
|
||||
<on event="created" run="port lobby open"/>
|
||||
<on event="b" run="port lobby preview leave" back="true"/>
|
||||
<on event="back" run="port lobby preview leave" back="true"/>
|
||||
<on event="start" run="port lobby preview add" open="main_menu/multiplayer_type_select/lobby/player_profile_screen" branch="true"/>
|
||||
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_list"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/header_lobby"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/lobby_join_help"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child left_right_tabs_items" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/lobby/lobby_desc">
|
||||
<data input="port lobby preview update"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_status" x="30" y="75"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_bar" y="414"/>
|
||||
@@ -79,7 +178,8 @@
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_status" type="text" controller="1" width="360" height="300" font="ui\large_ui" color="#FF2896FF">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_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/lobby/preview_button_join" x="380" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_join" x="250" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_add" x="380" y="1"/>
|
||||
<child widget="main_menu/multiplayer_type_select/lobby/preview_button_back" x="510" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_join" 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">
|
||||
@@ -87,6 +187,11 @@
|
||||
<on event="start" run="port lobby preview join"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_add" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="ADD PLAYER" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="port lobby add player"/>
|
||||
<on event="start" run="port lobby add player"/>
|
||||
<on event="left_mouse" run="mouse emit accept event"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/lobby/preview_button_back" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" text="BACK" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
|
||||
<on event="a" run="mouse emit back event"/>
|
||||
<on event="start" run="mouse emit back event"/>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
|
||||
<child widget="main_menu/solo_level_select/replay_level_right_name"/>
|
||||
<child widget="main_menu/solo_level_select/replay_level_right_pic"/>
|
||||
<child widget="main_menu/solo_level_select/replay_level_right_data"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/mp_map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
|
||||
</widget>
|
||||
@@ -33,7 +36,9 @@
|
||||
<on event="created" run="mp level list initialize"/>
|
||||
<on event="deleted" run="mp level list dispose"/>
|
||||
<on event="custom_activation" run="mp level select" open="main_menu/multiplayer_type_select/connected/gametype_select_screen_wrapper"/>
|
||||
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
|
||||
<on event="b" run="port map list back"/>
|
||||
<on event="back" run="port map list back"/>
|
||||
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
|
||||
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
|
||||
|
||||
@@ -9,20 +9,27 @@
|
||||
</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="350" 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="366" 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="103"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="133"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="163"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="193"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="223"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_vehicle_options" x="54" y="253"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_indicator_options" x="54" y="283"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/op_team_options" x="54" y="313"/>
|
||||
<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/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">
|
||||
@@ -64,6 +71,83 @@
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/invite_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_listing" 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/listing_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/listing_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/listing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<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"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY" setting="network.coop_friendly_fire" values="off|on|shields_only|explosives_only" 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_extra_enemies" 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/extra_enemies_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NONE|PER PLAYER|STATIC MULTIPLIER" setting="network.coop_enemies_mode" values="none|per_player|multiplier" 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_enemies_per_player" 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/enemies_per_player_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="25%|50%|100%|150%|200%" setting="network.coop_enemies" values="25|50|100|150|200" 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_enemies_multiplier" 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/enemies_multiplier_label"/>
|
||||
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="2X|4X|8X|16X|32X" setting="network.coop_enemies_multiplier" values="2|4|8|16|32" 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_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"/>
|
||||
@@ -148,6 +232,13 @@
|
||||
<string text="VEHICLE OPTIONS:"/>
|
||||
<string text="INDICATOR OPTIONS:"/>
|
||||
<string text="TEAMPLAY OPTIONS:"/>
|
||||
<string text="LISTING:"/>
|
||||
<string text="FRIENDLY FIRE:"/>
|
||||
<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=""/>
|
||||
@@ -160,5 +251,19 @@
|
||||
<string text="Each team's vehicles and their respawn time, for\nthis game."/>
|
||||
<string text="The motion tracker and nav points, for this game."/>
|
||||
<string text="Friendly fire and team balance, for this game."/>
|
||||
<string text="Anyone can see and join your game: it is listed\nin everyone's Server Browser."/>
|
||||
<string text="Only players with your invite link can join."/>
|
||||
<string text="Players can not be hurt by weapons and explosives\nfired by the other players."/>
|
||||
<string text="Players can be hurt by weapons or explosives\nfired by the other players."/>
|
||||
<string text="Damage from the other players will only reduce\nshields. Health will be unaffected."/>
|
||||
<string text="Players can be hurt by damage from explosives\nfired by the other players."/>
|
||||
<string text="Enemy squads are as the campaign has them."/>
|
||||
<string text="Enemy squads grow with the players: by the amount\nbelow for each player past the first."/>
|
||||
<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>
|
||||
@@ -59,13 +59,14 @@
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_item" x="-32" y="210"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_internet_item" y="243"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_create_lan_item" y="276"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="342"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_coop_item" y="309"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_gametypes_item" y="375"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/multiplayer_type_select/multiplayer_type_select_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
<on event="created" run="mp type menu initialize"/>
|
||||
<child widget="main_menu/multiplayer_type_select/header_multiplayer"/>
|
||||
<child widget="main_menu/multiplayer_type_select/multiplayer_type_select_list"/>
|
||||
<child widget="main_menu/blueline" x="64" y="325"/>
|
||||
<child widget="main_menu/blueline" x="64" y="358"/>
|
||||
</widget>
|
||||
</menus>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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/new_select/list_item_0_map_kind" width="256" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds_sm" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
|
||||
<child widget="main_menu/new_select/list_item_0_map_kind_spinner" x="15" y="2"/>
|
||||
</widget>
|
||||
<widget name="main_menu/new_select/list_item_0_map_kind_spinner" type="spinner" width="226" height="22" flags="left_right_tabs_items" string_list="main_menu/new_select/var_map_kinds" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 226 19 232">
|
||||
<on event="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<strings name="main_menu/new_select/var_map_kinds">
|
||||
<string text="SINGLEPLAYER"/>
|
||||
<string text="MULTIPLAYER"/>
|
||||
</strings>
|
||||
</menus>
|
||||
@@ -1,6 +1,15 @@
|
||||
<?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/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" x="286" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/map_weapons_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" 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="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_label"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/loadout_spinner" x="286" y="1"/>
|
||||
@@ -28,6 +37,10 @@
|
||||
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/secondary_weapon_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout_weapon" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" 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="left_mouse" run="mouse spinner 1wide click"/>
|
||||
</widget>
|
||||
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_map_weapons">
|
||||
<string text="YES"/>
|
||||
<string text="NO"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_loadout">
|
||||
<string text="CATEGORY"/>
|
||||
<string text="CUSTOM"/>
|
||||
@@ -43,7 +56,5 @@
|
||||
<string text="PLASMA PISTOL"/>
|
||||
<string text="PLASMA RIFLE"/>
|
||||
<string text="NEEDLER"/>
|
||||
<string text="FUEL ROD"/>
|
||||
<string text="FLAMETHROWER"/>
|
||||
</strings>
|
||||
</menus>
|
||||
@@ -26,9 +26,10 @@
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_infinite_grenades" x="54" y="73"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_weapon_set" x="54" y="103"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_starting_equipment" x="54" y="133"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="223"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_map_weapons" x="54" y="163"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_loadout" x="54" y="193"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_primary_weapon" x="54" y="223"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/op_secondary_weapon" x="54" y="253"/>
|
||||
<child widget="main_menu/settings_select/multiplayer_setup/item_options_edit/item_button_bar" y="414"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
<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_enabled" x="54" y="163"/>
|
||||
<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/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">
|
||||
@@ -51,11 +52,20 @@
|
||||
<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="3" 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="4" 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"/>
|
||||
@@ -74,6 +84,7 @@
|
||||
<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">
|
||||
@@ -81,6 +92,7 @@
|
||||
<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,18 +9,24 @@
|
||||
</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="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 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>
|
||||
<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_window_scale" x="54" y="99" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="125"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="151" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="177"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="203" platform="desktop"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="229"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="255"/>
|
||||
<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/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">
|
||||
@@ -37,20 +43,38 @@
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/mode_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="FULLSCREEN|BORDERLESS|WINDOWED" setting="display.mode" values="fullscreen|borderless|windowed" 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_window_scale" 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/window_scale_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_spinner" x="320" y="1"/>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" 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/resolution_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_scale_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="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/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="1" 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/window_scale_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="640 x 480|1280 x 960|1920 x 1440|2560 x 1920" setting="display.window_scale" values="1|2|3|4" 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">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE" setting="display.resolution" values="native" 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_window_size" 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/window_size_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/window_size_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="2" 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/window_size_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="1280 x 960" setting="display.window_size" values="1280x960" 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_resolution_scaling" 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/resolution_scaling_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/resolution_scaling_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="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/video_settings/resolution_scaling_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NATIVE|ORIGINAL" setting="display.resolution_scaling" values="native|original" 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_vsync" 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/vsync_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_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="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/vsync_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="4" 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/vsync_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.vsync" 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"/>
|
||||
@@ -59,7 +83,7 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_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="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/max_fps_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="5" 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/max_fps_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="AUTO|30|60|120|144|165|240|NONE" setting="display.max_fps" values="0|30|60|120|144|165|240|-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"/>
|
||||
@@ -68,7 +92,7 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_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="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/interpolation_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="6" 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/interpolation_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.interpolation" 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"/>
|
||||
@@ -77,7 +101,7 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_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="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_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="7" 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/direct_camera_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.direct_camera" 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"/>
|
||||
@@ -86,7 +110,7 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_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="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_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="8" 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/high_res_hud_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_hud" 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"/>
|
||||
@@ -95,11 +119,47 @@
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label"/>
|
||||
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" x="320" y="1"/>
|
||||
</widget>
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_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="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
|
||||
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_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="9" 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/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"/>
|
||||
@@ -112,23 +172,35 @@
|
||||
</widget>
|
||||
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels">
|
||||
<string text="DISPLAY MODE:"/>
|
||||
<string text="RESOLUTION:"/>
|
||||
<string text="WINDOW SIZE:"/>
|
||||
<string text="RESOLUTION SCALING:"/>
|
||||
<string text="V-SYNC:"/>
|
||||
<string text="FRAME RATE LIMIT:"/>
|
||||
<string text="SMOOTH MOTION:"/>
|
||||
<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=""/>
|
||||
<string text="Fullscreen and borderless draw at the display's\nresolution; windowed, 640x480 scaled. F11: window."/>
|
||||
<string text="The window's size when windowed (its edges can\nalso be dragged)."/>
|
||||
<string text="Fullscreen takes the display; borderless covers it\nwith a window. F11 switches to the window."/>
|
||||
<string text="What fullscreen and borderless draw at. Fullscreen\nsets the display to it; borderless scales it."/>
|
||||
<string text="The window's size: 4:3, then 16:10, 16:9 and 21:9\n(its edges can also be dragged)."/>
|
||||
<string text="Native draws at the resolution; Original draws\nthe Xbox's 640x480 and scales it up."/>
|
||||
<string text="Wait for the display between frames, so that the\npicture never tears."/>
|
||||
<string text="With V-Sync off, the most frames a second. Auto:\ntwice the display's refresh rate."/>
|
||||
<string text="Draw a frame for every display refresh, blending\nbetween the game's 30 ticks a second."/>
|
||||
<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>
|
||||
@@ -14,6 +14,9 @@
|
||||
<child widget="main_menu/solo_level_select/difficulty_indicator" x="416" y="343"/>
|
||||
<child widget="main_menu/solo_level_select/difficulty_indicator" x="466" y="343"/>
|
||||
<child widget="main_menu/solo_level_select/difficulty_indicator" x="516" y="343"/>
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_name"/>
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_pic"/>
|
||||
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_right_data"/>
|
||||
</widget>
|
||||
<widget name="main_menu/solo_level_select/replay_level_right_name" type="text" controller="1" left="417" top="204" width="146" height="43" string_list="main_menu/map_list" string_index="2" font="ui\large_ui" color="#FF2896FF">
|
||||
</widget>
|
||||
@@ -38,7 +41,7 @@
|
||||
<on event="created" run="initialize sp level list solo"/>
|
||||
<on event="deleted" run="dispose sp level list"/>
|
||||
<on event="custom_activation" run="solo level set map" open="main_menu/difficulty_select/difficulty_select_list_screen"/>
|
||||
<child widget="main_menu/new_select/list_item_0" x="20" y="73"/>
|
||||
<child widget="main_menu/new_select/list_item_0_map_kind" x="82" y="73"/>
|
||||
<child widget="main_menu/new_select/list_item_1" x="20" y="103"/>
|
||||
<child widget="main_menu/new_select/list_item_2" x="20" y="133"/>
|
||||
<child widget="main_menu/new_select/list_item_3" x="20" y="163"/>
|
||||
|
||||
|
After Width: | Height: | Size: 75 B |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -374,6 +374,7 @@
|
||||
<string text="Join a game by its invite link,\nor one a Discord invite\nreached."/>
|
||||
<string text="Host a game on the Internet:\nplayers join by its invite\nlink or Discord."/>
|
||||
<string text="Host a game on your LAN\nonly."/>
|
||||
<string text="Play the campaign with a\nfriend in split screen:\nplayer 2 on a gamepad."/>
|
||||
<string text="Set all the attributes for your \nmultiplayer gametypes and\nkeep them for future use."/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/multiplayer_options">
|
||||
@@ -384,6 +385,7 @@
|
||||
<string text="DIRECT LINK"/>
|
||||
<string text="EDIT GAMETYPES"/>
|
||||
<string text="SERVER BROWSER"/>
|
||||
<string text="CO-OP CAMPAIGN"/>
|
||||
</strings>
|
||||
<strings name="main_menu/multiplayer_type_select/server_settings/cap_server_settings_options">
|
||||
<string text="Enter a name for your server."/>
|
||||
@@ -498,6 +500,8 @@
|
||||
<string text="The map's weapons, without any grenades."/>
|
||||
<string text="You will start the game with the weapon set that the \nmap designers custom tailored for it."/>
|
||||
<string text="Use the generic starting weapons across all maps."/>
|
||||
<string text="The map's weapons appear where its designers\nplaced them."/>
|
||||
<string text="No weapons appear on the map: with a loadout of\nNONE and NONE, melee and grenades only."/>
|
||||
<string text="The weapons follow the weapon set above."/>
|
||||
<string text="Everyone starts with the primary and secondary\nweapons below; the map's weapons are its own."/>
|
||||
<string text="No primary weapon."/>
|
||||
@@ -510,8 +514,6 @@
|
||||
<string text="Everyone spawns with a plasma pistol as their primary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a plasma rifle as their primary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a needler as their primary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a fuel rod as their primary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a flamethrower as their primary\nweapon (where the map has one)."/>
|
||||
<string text="No secondary weapon."/>
|
||||
<string text="A random secondary weapon, of those the map has,\neach time you spawn."/>
|
||||
<string text="Everyone spawns with a assault rifle as their secondary\nweapon (where the map has one)."/>
|
||||
@@ -522,8 +524,6 @@
|
||||
<string text="Everyone spawns with a plasma pistol as their secondary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a plasma rifle as their secondary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a needler as their secondary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a fuel rod as their secondary\nweapon (where the map has one)."/>
|
||||
<string text="Everyone spawns with a flamethrower as their secondary\nweapon (where the map has one)."/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/item_options_labels">
|
||||
<string text="INFINITE GRENADES:"/>
|
||||
@@ -534,6 +534,7 @@
|
||||
<string text="LOADOUT:"/>
|
||||
<string text="PRIMARY WEAPON:"/>
|
||||
<string text="SECONDARY WEAPON:"/>
|
||||
<string text="MAP WEAPONS:"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/multiplayer_setup/item_options_edit/var_infinite_grenades">
|
||||
<string text="YES"/>
|
||||
@@ -963,6 +964,12 @@
|
||||
<string text="At the end of this game there will be fifteen angry\nmen. Angry because the winning player killed them."/>
|
||||
<string text="You'll need to secure twenty-five kills in order to\nwin the game."/>
|
||||
<string text="We hope you've got plenty of time, because you'll\nneed to make a full fifty kills to win this one."/>
|
||||
<string text="Seventy-five kills to win. Settle in for a long\nfight."/>
|
||||
<string text="A hundred kills to win. Made for big games."/>
|
||||
<string text="A hundred and fifty kills to win. Only a crowded\nserver gets there."/>
|
||||
<string text="Two hundred kills to win. Bring friends. Lots of\nthem."/>
|
||||
<string text="Two hundred and fifty kills to win."/>
|
||||
<string text="Five hundred kills to win. You'll be here a while."/>
|
||||
<string text="This is a team game."/>
|
||||
<string text="It's every man for himself."/>
|
||||
<string text="The game will have no time limit. It will only end\nwhen someone wins."/>
|
||||
@@ -994,6 +1001,12 @@
|
||||
<string text="15"/>
|
||||
<string text="25"/>
|
||||
<string text="50"/>
|
||||
<string text="75"/>
|
||||
<string text="100"/>
|
||||
<string text="150"/>
|
||||
<string text="200"/>
|
||||
<string text="250"/>
|
||||
<string text="500"/>
|
||||
</strings>
|
||||
<strings name="main_menu/settings_select/multiplayer_setup/playlist_edit/slayer_edit/var_team_play">
|
||||
<string text="YES"/>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"ce/main_menu.gametype_select.xml",
|
||||
"ce/main_menu.multiplayer_type_select.connected.4way_profile_select.xml",
|
||||
"ce/main_menu.multiplayer_type_select.connected.xml",
|
||||
"ce/main_menu.multiplayer_type_select.coop.xml",
|
||||
"ce/main_menu.multiplayer_type_select.direct_ip.xml",
|
||||
"ce/main_menu.multiplayer_type_select.join_game.filters.xml",
|
||||
"ce/main_menu.multiplayer_type_select.join_game.port.xml",
|
||||
@@ -16,6 +17,7 @@
|
||||
"ce/main_menu.multiplayer_type_select.mp_map_select.xml",
|
||||
"ce/main_menu.multiplayer_type_select.server_settings.xml",
|
||||
"ce/main_menu.multiplayer_type_select.xml",
|
||||
"ce/main_menu.new_select.port.xml",
|
||||
"ce/main_menu.new_select.xml",
|
||||
"ce/main_menu.player_profiles_select.xml",
|
||||
"ce/main_menu.profile_manager.xml",
|
||||
@@ -49,9 +51,19 @@
|
||||
"ce/main_menu.xml",
|
||||
"ce/shell.xml",
|
||||
"ce/strings.xml",
|
||||
"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",
|
||||
"ce/port/pause/pausebox_center__0.png",
|
||||
"ce/port/pause/pausebox_center__1.png",
|
||||
"ce/port/pause/pausebox_left__0.png",
|
||||
"ce/port/pause/pausebox_left__1.png",
|
||||
"ce/port/pause/pausebox_right__0.png",
|
||||
"ce/port/pause/pausebox_right__1.png",
|
||||
"ce/shell/bitmaps/alert_bkd__0.png",
|
||||
"ce/shell/bitmaps/arrow_sm_down__0.png",
|
||||
"ce/shell/bitmaps/arrow_sm_down__1.png",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" viewBox="0 0 4 4"/>
|
||||
|
After Width: | Height: | Size: 81 B |
@@ -0,0 +1,13 @@
|
||||
<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="323.93" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -0,0 +1,13 @@
|
||||
<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="226.48" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
|
||||
<rect x="0" y="2" width="4" height="190" fill="#0a2649" fill-opacity="0.75"/>
|
||||
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
|
||||
<rect x="0" y="192" width="4" height="2" fill="#2896ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="256" viewBox="0 0 4 256">
|
||||
<rect x="0" y="2" width="4" height="225" fill="#0a2649" fill-opacity="0.75"/>
|
||||
<rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
|
||||
<rect x="0" y="227" width="4" height="2" fill="#2896ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
|
||||
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
|
||||
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
|
||||
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
|
||||
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
|
||||
<g transform="matrix(-1 0 0 1 16 0)">
|
||||
<path d="M9.5,1 H17 V193 H9.5 A8.5,8.5 0 0 1 1,184.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
|
||||
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V184.5 A8.5,8.5 0 0 0 9.5,193 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="256" viewBox="0 0 16 256">
|
||||
<g transform="matrix(-1 0 0 1 16 0)">
|
||||
<path d="M9.5,1 H17 V228 H9.5 A8.5,8.5 0 0 1 1,219.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
|
||||
<path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V219.5 A8.5,8.5 0 0 0 9.5,228 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 411 B |
@@ -0,0 +1,10 @@
|
||||
# The public MQTT brokers of internet play: the machines of an invite find
|
||||
# each other through them, and the server browser's listings go through
|
||||
# them. The game uses every one at once (up to 4), so any one working is
|
||||
# enough. One host:port on each line; "#" starts a comment.
|
||||
# (The game reads this file beside config.toml: network.brokers_file. An
|
||||
# update replaces it; keep a list of your own under another name.)
|
||||
opence.milenko.org:1883
|
||||
broker.emqx.io:1883
|
||||
broker.hivemq.com:1883
|
||||
test.mosquitto.org:1883
|
||||
@@ -21,6 +21,69 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
vehicle it drives) from its local input at once. Remote players are
|
||||
driven by the inputs the host relays every tick, the latest one held
|
||||
until a newer arrives.
|
||||
- **Actors driven.** Only the host runs the AI. Each tick it sends its
|
||||
clients the control its actors gave their units (how they move, where
|
||||
they face, aim and look, their trigger and buttons, their animation
|
||||
impulses) with the units' state, those near a client's players every
|
||||
tick and others less often; a client drives each unit with the latest
|
||||
it has, as it drives a remote player's, until it hears nothing of it for
|
||||
two seconds (`port/linux/game/network_actors.c`).
|
||||
- **Co-op.** A network game on a campaign level with no game engine
|
||||
(Create Game's Map screen, its SINGLEPLAYER maps, over LAN and the
|
||||
internet) is co-op. Only the host runs the
|
||||
level's scripts and spawns players. `network_coop.c` sends the clients
|
||||
everything the scripts do that they would otherwise miss:
|
||||
- every tick: the cinematic, camera, screen fade, the HUD settings the
|
||||
scripts control (what is shown, the mission timer), the skip vote, and
|
||||
which teams are allies and friends (the scripts' allegiances, so the
|
||||
marines are the players' allies on every machine);
|
||||
- once each, numbered so nothing is applied twice: script sounds,
|
||||
chapter titles, help and objective text, "Checkpoint" messages, screen
|
||||
shake, nav points, custom animations on units and scenery, and units
|
||||
opening and closing (dropships' doors);
|
||||
- device groups (doors, elevators, switches; a client sets none itself,
|
||||
and its player's use of one is relayed to the host), and each device's
|
||||
position and power as it changes: a client puts its device where the
|
||||
host's is once that stops, or if they drift apart while it moves;
|
||||
- which named objects exist, so scripted creates and deletes match.
|
||||
|
||||
Every machine follows the host's structure BSP: a switch is sent at once
|
||||
and reliably, and each client's input says which BSP it has loaded.
|
||||
Until a client has the host's, the host takes none of its players'
|
||||
movement and none of its loading zones, and after any switch no loading
|
||||
zone switches again until every machine has the new BSP (ten seconds at
|
||||
most). A loading zone into a
|
||||
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.
|
||||
|
||||
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
|
||||
moving forward (the netcode depends on that) and moves the script
|
||||
threads' wake times along with it. The object, device and name syncs
|
||||
bring the clients up to date.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
@@ -93,7 +156,24 @@ is dead; version 8 is the first whose clients play by the host's rules
|
||||
(below), so a build without them joins no host of it; version 9 tells
|
||||
every machine of a player the host dropped for cheating, each client
|
||||
tells the host its Discord user, and a machine's join request carries its
|
||||
hardware id; version 10 sends every player's ping for the scoreboard.
|
||||
hardware id; version 10 sends every player's ping for the scoreboard;
|
||||
version 11 sends with the game's settings its gametype's PC options;
|
||||
version 12 plays the campaign together (co-op, above), drives the host's
|
||||
actors on its clients and sends the flinches and deaths the host picked;
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -115,9 +195,13 @@ 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, 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`
|
||||
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
|
||||
(`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
|
||||
@@ -132,13 +216,16 @@ 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, and one the host bans with the
|
||||
as hex only. A player dropped for cheating (on its stream's word, as
|
||||
above), 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
|
||||
hardware id is in it (a line taken out unbans). Both are as the player's
|
||||
machine tells them: anyone with administrator or root access can change
|
||||
them, and players behind one address share it.
|
||||
them, and players behind one address share it. The console's `kick <player
|
||||
name>` drops a player as `ban` does (every machine told), but adds no line
|
||||
and keeps no address out: the player may join again at once.
|
||||
A speed hack of less than a tenth is let be: the host's bounds on how far
|
||||
and how fast a client's player moves and fires hold it to the host's time
|
||||
anyway.
|
||||
@@ -282,8 +369,10 @@ a pregame keep-alive every five seconds from the host
|
||||
host's latest tick it had heard of when it made the report.
|
||||
- The host deals a report once it has checked it: from that machine's
|
||||
player; damage one of their weapons (a vehicle's a driver's or
|
||||
gunner's; now or in the last ten seconds), their grenades (while they
|
||||
have them, and for a while after) or the vehicle they drove (in the
|
||||
gunner's; now or in the last ten seconds), a grenade the host's own game
|
||||
saw them throw in the last ten seconds that has not gone off (each
|
||||
throw's explosion is taken once, its other hits that tick with it:
|
||||
holding grenades deals nothing) or the vehicle they drove (in the
|
||||
last ten seconds: its collisions) can deal (its projectiles' impacts
|
||||
and detonations, followed through the tags), no harder than it can be
|
||||
(all of it, but an airborne melee blow's half again); of the shape the
|
||||
|
||||
@@ -83,7 +83,8 @@ These files are in the data root:
|
||||
| `init.txt` | Console commands that the game does at start-up. For example, `map_name levels\a10\a10` starts the first campaign level. |
|
||||
|
||||
The settings are in `config.toml` next to the executable. Refer to
|
||||
"Settings".
|
||||
"Settings". Internet play's MQTT brokers are in `brokers.txt` next to it
|
||||
(`network.brokers_file`).
|
||||
|
||||
If the game stops because of a fatal signal, it writes the address and a
|
||||
backtrace to the standard error. To find the function at the address, enter
|
||||
@@ -95,8 +96,12 @@ The keyboard and the mouse are a control scheme of their own for the player
|
||||
of controller 1: each action has up to two keys or mouse buttons, which
|
||||
Settings > Controls Setup (or `[controls]` in `config.toml`) changes. The
|
||||
game adds the input of the first gamepad to controller 1. The other
|
||||
gamepads operate controllers 2 to 4. The profile's button layout (Settings >
|
||||
Gamepads) is the gamepads' only.
|
||||
gamepads operate controllers 2 to 4. With two or more players on this
|
||||
computer (co-op, or split screen in a network game) and only one gamepad,
|
||||
that gamepad is controller 2 (player 2) and the keyboard and mouse stay
|
||||
controller 1. The gamepad changes controller only when none of its buttons
|
||||
is held. The profile's button layout (Settings > Gamepads) is the
|
||||
gamepads' only.
|
||||
|
||||
| Action | Keys and buttons (default) |
|
||||
| --- | --- |
|
||||
@@ -157,6 +162,37 @@ change. The screens open, close and move between each other as the PC
|
||||
version's. `display.menus = "xbox"` gives the Xbox's menus, which start every
|
||||
kind of game.
|
||||
|
||||
Multiplayer > CO-OP CAMPAIGN is the Xbox's cooperative play, which the PC
|
||||
version does not have: two players on this computer play the campaign in
|
||||
split screen. Player 1 is the player who chose it, on the current profile.
|
||||
Player 2 then chooses a profile with their own controller (a gamepad), and
|
||||
New Game's levels are those either profile has reached. Either player's
|
||||
controller chooses the level and the difficulty. A co-op game does
|
||||
not continue a saved game of one player.
|
||||
|
||||
Network games have split screen too: up to 4 players on each computer. In
|
||||
the game lobby, another controller presses START to join, and the new
|
||||
player's profile is chosen on the ADD PLAYER screen that opens (with any
|
||||
controller). With one gamepad, choose the lobby's ADD PLAYER button first:
|
||||
until then that gamepad shares controller 1 with the keyboard. Two players on one profile get different names from the host. A
|
||||
player's B in the lobby leaves the game alone, and the last player of the
|
||||
computer leaves it for all of them. In the game, each player's pause menu
|
||||
opens on their part of the screen, and its LEAVE GAME is theirs: their part
|
||||
of the screen stays until the game ends. A game under way shows its own
|
||||
screen before JOIN GAME: players join there the same way, START or ADD
|
||||
PLAYER then START, and JOIN GAME brings them all into the game.
|
||||
|
||||
In a multiplayer game, the pause menu (escape) has SETTINGS, which opens
|
||||
the profile's settings while the game goes on, and for the host END GAME.
|
||||
END GAME ends the game as its time limit does: the players stay, and after
|
||||
the carnage report the host picks the next map and gametype (PICK GAME).
|
||||
LEAVE GAME of the host still ends the game for everyone. The buttons go into
|
||||
the map's own pause menu, before LEAVE GAME, so the buttons of a custom map
|
||||
stay. The Xbox's pause box is drawn taller to hold them (a redraw,
|
||||
`port/assets/menus/port_svg/pause`); a custom map's box keeps its size, and
|
||||
what is below its list moves down. The few pictures of the settings that
|
||||
come from the main menu's map are not drawn there.
|
||||
|
||||
The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes
|
||||
them from the PC version's tags), which the game contains. To change them,
|
||||
put files in a `menus` folder next to `config.toml`: a file with the same
|
||||
@@ -184,26 +220,33 @@ the setting for one start of the game. It has priority over the file.
|
||||
|
||||
| Setting | Default | Environment variable | Function |
|
||||
| --- | --- | --- | --- |
|
||||
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at its desktop resolution. `"borderless"`: a window over the whole desktop. Both draw at the resolution of the display, the picture 480 lines of the game and the width of the display. `"windowed"`: a window with the 640x480 picture of the Xbox, scaled. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
|
||||
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: fullscreen at the resolution of the display. The picture has 480 lines of the game and the width of the display. `false`: a window with the 640x480 picture of the Xbox. Used when `display.mode` is empty. |
|
||||
| `display.window_scale` | `2` | `HALO_WINDOW_SCALE` | The size of the window, as a multiple of 640x480. You can change the size of the window. |
|
||||
| `display.mode` | `""` | `HALO_DISPLAY_MODE` | `"fullscreen"`: the display, taken at the mode of `display.resolution` (the nearest the display has), or at its desktop mode. `"borderless"`: a window over the whole desktop, whose mode does not change. `"windowed"`: a window of `display.window_size`. Empty: `display.fullscreen` decides (`true`: borderless). F11 changes between the window and the fullscreen mode. Video Setup sets it. |
|
||||
| `display.fullscreen` | `true` | `HALO_FULLSCREEN` | `true`: borderless, as `display.mode = "borderless"`. `false`: a window, as `display.mode = "windowed"`. Used when `display.mode` is empty. |
|
||||
| `display.resolution` | `"native"` | `HALO_RESOLUTION` | What fullscreen and borderless draw at: `"native"`, the display's own resolution, or `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. Fullscreen sets the display to it. Borderless draws at it and scales the picture to the display, where the display has room for it. The picture has 480 lines of the game and the width of the resolution's shape. A window draws at its own size instead. Video Setup's Resolution sets it, from the display's modes; it shows with Fullscreen and Borderless. |
|
||||
| `display.resolution_scaling` | `"native"` | `HALO_RESOLUTION_SCALING` | `"native"`: the game draws at the resolution of the window, or of the display (or `display.resolution`) fullscreen. `"original"`: the game draws the 640x480 picture of the Xbox and scales it up to the window or the display, whatever `display.mode` is. Video Setup sets it. |
|
||||
| `display.window_size` | `""` | `HALO_WINDOW_SIZE` | The size of the window, as `"<width>x<height>"`, such as `"1920x1080"`, 640x480 or more. You can change the size of the window; the game's picture takes its shape. Empty: `display.window_scale` decides. Video Setup's Window Size sets it, from sizes of each shape (4:3, 16:10, 16:9 and 21:9) that fit the desktop; it shows with Windowed. |
|
||||
| `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 display's resolution, 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.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 while the enemy is in sight and not camouflaged, so it never shows where an enemy hides, and only as far away as the weapon in hand turns its reticle red over an enemy (at least 20 world units, the motion sensor's reach, and at most 70). |
|
||||
| `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. |
|
||||
@@ -219,7 +262,15 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `network.join_from_clipboard` | `true` | `HALO_NET_JOIN_FROM_CLIPBOARD` | `true`: when the game comes to the front, it joins the game of an invite link on the clipboard. |
|
||||
| `network.tunnel_port` | `0` | `HALO_NET_TUNNEL_PORT` | The UDP port for internet play. `0`: the game selects a port. Refer to "Internet play". |
|
||||
| `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". |
|
||||
| `network.signalling_brokers` | three public brokers | `HALO_NET_BROKERS` | The public MQTT brokers (`host:port`, with commas between them) that let the machines of an invite find each other. |
|
||||
| `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. |
|
||||
| `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". |
|
||||
| `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. |
|
||||
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
|
||||
| `discord.application_id` | the application of the project | `HALO_DISCORD_APPLICATION` | The Discord application for invites. Empty: no Discord. |
|
||||
| `update.auto` | `true` | `HALO_UPDATE_AUTO` | `true`: at start-up, the game looks for a new version. Refer to "Updates". `false`: the game does not look. |
|
||||
@@ -402,17 +453,89 @@ When the machines connect, the game of the host shows in Multiplayer,
|
||||
System Link. Join the game as on a local network. System link on a local
|
||||
network does not need an invite.
|
||||
|
||||
### Server browser
|
||||
|
||||
Server Setup in Create Game > Internet has a LISTING row:
|
||||
|
||||
- PUBLIC (the default): the game also shows in Join Game > Server Browser
|
||||
on every machine. Anyone can see and join the game.
|
||||
- PRIVATE: only players with the invite link can join.
|
||||
|
||||
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.
|
||||
|
||||
How it operates (`src/p2p_lobby.c`):
|
||||
|
||||
- The host of a public game publishes a listing: the invite, and the name,
|
||||
map, gametype and player counts of the game. The listing goes to the
|
||||
same MQTT brokers as the invites (`network.brokers_file`), retained,
|
||||
to a topic of the host (`hceu/3/lobby/s/<hash of its key>`).
|
||||
- The key of the host signs the listing (Ed25519). The key is the key of the
|
||||
invite, so no other machine can list the invite of the host, change its
|
||||
listing, or list a game with false details.
|
||||
- The host publishes the listing again every 30 seconds, when the game
|
||||
changes, and when a browser asks. If the topic of the host is empty or
|
||||
holds another listing, the host publishes again in 5 seconds or less. Thus
|
||||
a broker that deletes the listing does not remove the game.
|
||||
- When the host stops (it stops hosting, the game becomes private, or the
|
||||
game quits), it publishes a closed listing, and then empties its topic.
|
||||
If the host loses its connection, the broker empties its topic (the will
|
||||
of the connection). A browser removes a game that it does not hear for 90
|
||||
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
|
||||
addresses. Thus anyone can learn the address of the host of a public game,
|
||||
as for any public server.
|
||||
|
||||
The brokers are in `brokers.txt` next to the executable (from
|
||||
`port/assets/network/brokers.txt`; on Android, the app writes it next to
|
||||
`config.toml` at each start), one `host:port` on each line. The game uses
|
||||
all of them at once (up to 4), so one that works is enough. An update
|
||||
replaces `brokers.txt`: to use brokers of your own, put them in another
|
||||
file and name it in `network.brokers_file`. All the players must use the
|
||||
same broker to see each other's games. The game uses
|
||||
MQTT 5 if the broker has it, else MQTT 3.1.1. A broker that does not keep
|
||||
retained messages, or does not let clients subscribe with wildcards, carries
|
||||
only invites, not listings.
|
||||
|
||||
### Security
|
||||
|
||||
Only machines with the invite can find the game:
|
||||
|
||||
- Each copy of the game makes an X25519 key pair when it starts. Its
|
||||
identifier is from the hash of its public key.
|
||||
- Each copy of the game makes an Ed25519 key pair when it starts, and from
|
||||
it an X25519 key pair (Monocypher, `port/third_party/monocypher`). Its
|
||||
identifier is from the hash of its X25519 public key. The Ed25519 key
|
||||
signs the listing of a public game.
|
||||
- The link contains a 16-byte hash of the public key of the host and a
|
||||
random 16-byte token. The identifier of the host is from the first 6
|
||||
bytes of the hash.
|
||||
- The machines exchange their public keys and addresses through public MQTT
|
||||
brokers (`network.signalling_brokers`). The topics are HMACs of the token.
|
||||
brokers (`network.brokers_file`). The topics are HMACs of the token.
|
||||
A key from the token encrypts and authenticates the messages
|
||||
(`src/p2p_signal.c`, `src/p2p_crypto.c`). The host authenticates its answer
|
||||
with a key that only it and the player can calculate. Its public key must
|
||||
@@ -444,15 +567,20 @@ 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, 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.
|
||||
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.
|
||||
- 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`. So that every player can be named, the host trims
|
||||
Refer to `NETCODE.md`. `kick <player name>` drops the player the same
|
||||
way, but keeps nothing: no line in `bans.txt`, and the player can join
|
||||
again at once. So that every player can be named, the host trims
|
||||
the spaces around a name and removes characters that draw as nothing. A
|
||||
letter with a mark is typed as the plain letter (`ban jose` for "José").
|
||||
A name with nothing left to type becomes "Player", and a name that another
|
||||
@@ -522,7 +650,8 @@ The game sends the activity only to a Discord client of the same user.
|
||||
| Game code | All 466 C files of the game. The changes are in "Game source changes". |
|
||||
| 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 display's resolution, 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. |
|
||||
| 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. |
|
||||
@@ -608,11 +737,13 @@ Other changes:
|
||||
| `networking/`, `game/`, `interface/`, `bungie_net/network/` and the pools of objects, effects and sounds | The system link limits and the memory for them. |
|
||||
| `game/`, `objects/`, `units/`, `networking/` | The distributed netcode. Refer to `NETCODE.md`. |
|
||||
| `cache/cache_files.c` | When a map's tags load and unload, the port finds the bitmaps that the high-res HUD replaces (`game/hud_hires_tags.c`), and adds the tags of the menus to the menus' map (`game/menu_tags.c`). |
|
||||
| `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. |
|
||||
| `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 display's resolution (`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. |
|
||||
| `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. |
|
||||
|
||||
The x86 inline assembly of the game is replaced by C. Thus the compiler
|
||||
can optimize that code for each processor:
|
||||
|
||||
@@ -0,0 +1,677 @@
|
||||
/*
|
||||
COOP_ENEMIES.C
|
||||
|
||||
Network co-op's extra enemies: Server Setup's EXTRA ENEMIES
|
||||
(network.coop_enemies_mode). PER PLAYER (network.coop_enemies, a
|
||||
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.
|
||||
|
||||
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
|
||||
kept: once the dropship's own riders get out (vehicle_unload, or any other
|
||||
way), they are placed beside those riders, a few ticks apart, and drop and
|
||||
fight as they do. A dropship gone first takes them with it.
|
||||
|
||||
Only the host runs the AI, so all of this is the host's.
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "ai/actor_placement.h"
|
||||
#include "ai/actors.h"
|
||||
#include "ai/ai_scenario_definitions.h"
|
||||
#include "ai/encounters.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_allegiance.h"
|
||||
#include "game/players.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "networking/network_game_manager.h"
|
||||
#include "objects/objects.h"
|
||||
#include "physics/collisions.h"
|
||||
#include "scenario/scenario.h"
|
||||
#include "scenario/scenario_definitions.h"
|
||||
#include "units/units.h"
|
||||
#include "units/unit_definitions.h"
|
||||
|
||||
#include "coop_enemies.h"
|
||||
#include "network_coop.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
/* port_config.c's */
|
||||
long config_integer(char const *name);
|
||||
char const *config_string(char const *name);
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
/* the actors a level keeps for its own placements, which the extra
|
||||
enemies never take: as many as the Xbox's whole pool */
|
||||
COOP_ENEMIES_LEVEL_ACTORS = 256,
|
||||
/* EXTRA ENEMIES' amounts, as Server Setup has them: 25 to 200% per
|
||||
player, or 2 to 32 times (none is its NONE) */
|
||||
COOP_ENEMIES_MINIMUM_PERCENT = 25,
|
||||
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,
|
||||
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,
|
||||
/* a kept rider placed every this many ticks, as the seated ones
|
||||
jumped out */
|
||||
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 */
|
||||
#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
|
||||
/* a kept rider's place beside a rider who got out, each a little apart */
|
||||
#define RIDER_SPACING 0.5f
|
||||
|
||||
/* EXTRA ENEMIES' choices (network.coop_enemies_mode's values) */
|
||||
enum
|
||||
{
|
||||
_coop_enemies_none,
|
||||
_coop_enemies_per_player,
|
||||
_coop_enemies_multiplier,
|
||||
};
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
/* a vehicle a script loaded riders into: its seated ones, and whether they
|
||||
have begun to get out */
|
||||
struct coop_riding_vehicle
|
||||
{
|
||||
long vehicle_index;
|
||||
long seated_unit_indices[MAXIMUM_SEATED_RIDERS];
|
||||
short seat_indices[MAXIMUM_SEATED_RIDERS];
|
||||
short seated_count;
|
||||
boolean releasing;
|
||||
short released_count;
|
||||
long next_release_time;
|
||||
};
|
||||
|
||||
/* a rider left without a seat, to place once its vehicle's get out */
|
||||
struct coop_kept_rider
|
||||
{
|
||||
long vehicle_index;
|
||||
long variant_definition_index;
|
||||
long encounter_index;
|
||||
short squad_index;
|
||||
};
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
static struct
|
||||
{
|
||||
/* EXTRA ENEMIES, as the game began with it: its choice, and the amount
|
||||
of PER PLAYER (a percentage) and of STATIC MULTIPLIER */
|
||||
short mode;
|
||||
short percent;
|
||||
short multiplier;
|
||||
struct coop_riding_vehicle vehicles[MAXIMUM_RIDING_VEHICLES];
|
||||
short vehicle_count;
|
||||
struct coop_kept_rider riders[MAXIMUM_KEPT_RIDERS];
|
||||
short rider_count;
|
||||
} coop_enemies;
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static boolean coop_enemies_host(
|
||||
void)
|
||||
{
|
||||
return game_connection() == _game_connection_network_server && network_coop_active();
|
||||
}
|
||||
|
||||
/* whether this game has extra enemies (it is a co-op host's, with some
|
||||
chosen) */
|
||||
static boolean coop_enemies_on(
|
||||
void)
|
||||
{
|
||||
return coop_enemies.mode != _coop_enemies_none && coop_enemies_host();
|
||||
}
|
||||
|
||||
/* the players in the game, every machine's: the network game's (a level
|
||||
places its first enemies before the players' units are made, as it loads:
|
||||
game_initialize_for_new_map) */
|
||||
static short coop_enemies_player_count(
|
||||
void)
|
||||
{
|
||||
struct network_game *game = network_game_get_game();
|
||||
short count = 0;
|
||||
short index;
|
||||
|
||||
for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++)
|
||||
{
|
||||
if (network_player_is_valid(&game->players[index]))
|
||||
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)
|
||||
{
|
||||
struct collision_result collision;
|
||||
|
||||
return collision_test_vector(SPREAD_COLLISION_FLAGS, from, 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(
|
||||
real_point3d const *point,
|
||||
real_point3d *floor)
|
||||
{
|
||||
struct collision_result collision;
|
||||
real_point3d from = *point;
|
||||
real_vector3d down = { 0.0f, 0.0f, -(2.0f * SPREAD_STEP_HEIGHT + SPREAD_FLOOR_DIFFERENCE) };
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static struct coop_riding_vehicle *coop_enemies_riding_vehicle(
|
||||
long vehicle_index,
|
||||
boolean create)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < coop_enemies.vehicle_count; index++)
|
||||
{
|
||||
if (coop_enemies.vehicles[index].vehicle_index == vehicle_index)
|
||||
return &coop_enemies.vehicles[index];
|
||||
}
|
||||
if (!create || coop_enemies.vehicle_count >= MAXIMUM_RIDING_VEHICLES)
|
||||
return NULL;
|
||||
csmemset(&coop_enemies.vehicles[coop_enemies.vehicle_count], 0, sizeof(coop_enemies.vehicles[0]));
|
||||
coop_enemies.vehicles[coop_enemies.vehicle_count].vehicle_index = vehicle_index;
|
||||
return &coop_enemies.vehicles[coop_enemies.vehicle_count++];
|
||||
}
|
||||
|
||||
/* the riders kept for a vehicle forgotten, and the vehicle with them */
|
||||
static void coop_enemies_forget_vehicle(
|
||||
short vehicle_number)
|
||||
{
|
||||
long vehicle_index = coop_enemies.vehicles[vehicle_number].vehicle_index;
|
||||
short index, kept = 0;
|
||||
|
||||
for (index = 0; index < coop_enemies.rider_count; index++)
|
||||
{
|
||||
if (coop_enemies.riders[index].vehicle_index != vehicle_index)
|
||||
coop_enemies.riders[kept++] = coop_enemies.riders[index];
|
||||
}
|
||||
coop_enemies.rider_count = kept;
|
||||
coop_enemies.vehicles[vehicle_number] = coop_enemies.vehicles[--coop_enemies.vehicle_count];
|
||||
}
|
||||
|
||||
/* whether a vehicle's seated riders have begun to get out: one alive and
|
||||
out of it (one killed in its seat is not getting out) */
|
||||
static boolean coop_enemies_riders_getting_out(
|
||||
struct coop_riding_vehicle const *vehicle)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < vehicle->seated_count; index++)
|
||||
{
|
||||
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
|
||||
_object_mask_unit);
|
||||
|
||||
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
|
||||
unit->object.parent_object_index != vehicle->vehicle_index)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* where a vehicle's `number`th kept rider is placed: beside one of the
|
||||
riders who got out of it (where it is now: out of the vehicle, in the
|
||||
open), a ring further about them each round; else at a seat's marker, else
|
||||
the vehicle's place */
|
||||
static void coop_enemies_rider_place(
|
||||
struct coop_riding_vehicle const *vehicle,
|
||||
short number,
|
||||
real_point3d *position,
|
||||
real *facing)
|
||||
{
|
||||
struct object_datum *object = object_get(vehicle->vehicle_index);
|
||||
struct unit_definition *definition = unit_definition_get(object->definition_index);
|
||||
long out_unit_indices[MAXIMUM_SEATED_RIDERS];
|
||||
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);
|
||||
for (index = 0; index < vehicle->seated_count; index++)
|
||||
{
|
||||
struct object_datum *unit = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
|
||||
_object_mask_unit);
|
||||
|
||||
if (unit && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
|
||||
unit->object.parent_object_index != vehicle->vehicle_index)
|
||||
{
|
||||
out_unit_indices[out_count++] = vehicle->seated_unit_indices[index];
|
||||
}
|
||||
}
|
||||
if (out_count > 0)
|
||||
{
|
||||
*position = object_get(out_unit_indices[number % out_count])->object.position;
|
||||
round = (short)(number / out_count);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct object_marker marker;
|
||||
|
||||
round = number;
|
||||
if (vehicle->seated_count > 0 && VALID_INDEX(vehicle->seat_indices[0], definition->unit.seats.count))
|
||||
{
|
||||
struct unit_seat *seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, vehicle->seat_indices[0],
|
||||
struct unit_seat);
|
||||
|
||||
if (seat->marker_name[0] &&
|
||||
object_get_marker_by_name(vehicle->vehicle_index, seat->marker_name, &marker, 1) > 0)
|
||||
{
|
||||
*position = marker.matrix.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void coop_enemies_new_game(
|
||||
void)
|
||||
{
|
||||
char const *mode = config_string("network.coop_enemies_mode");
|
||||
|
||||
csmemset(&coop_enemies, 0, sizeof(coop_enemies));
|
||||
/* (an amount past Server Setup's, as it shows it: its nearest choice's
|
||||
end) */
|
||||
coop_enemies.percent = (short)PIN(config_integer("network.coop_enemies"), COOP_ENEMIES_MINIMUM_PERCENT,
|
||||
COOP_ENEMIES_MAXIMUM_PERCENT);
|
||||
coop_enemies.multiplier = (short)PIN(config_integer("network.coop_enemies_multiplier"),
|
||||
COOP_ENEMIES_MINIMUM_MULTIPLIER, COOP_ENEMIES_MAXIMUM_MULTIPLIER);
|
||||
coop_enemies.mode = !strcmp(mode, "per_player") ? _coop_enemies_per_player :
|
||||
!strcmp(mode, "multiplier") ? _coop_enemies_multiplier : _coop_enemies_none;
|
||||
}
|
||||
|
||||
void coop_enemies_reset(
|
||||
void)
|
||||
{
|
||||
coop_enemies.vehicle_count = 0;
|
||||
coop_enemies.rider_count = 0;
|
||||
}
|
||||
|
||||
short coop_enemies_extra_count(
|
||||
long encounter_index,
|
||||
short count)
|
||||
{
|
||||
struct encounter_datum *encounter;
|
||||
long extra, room;
|
||||
short players;
|
||||
|
||||
if (count <= 0 || !coop_enemies_on())
|
||||
return 0;
|
||||
encounter = encounter_get(encounter_index);
|
||||
if (!game_team_is_enemy(_game_team_player, encounter->team_index))
|
||||
return 0;
|
||||
if (coop_enemies.mode == _coop_enemies_multiplier)
|
||||
extra = (long)count * (coop_enemies.multiplier - 1);
|
||||
else
|
||||
{
|
||||
players = coop_enemies_player_count();
|
||||
if (players <= 1)
|
||||
return 0;
|
||||
extra = ((long)count * coop_enemies.percent * (players - 1) + 50) / 100;
|
||||
}
|
||||
room = MAXIMUM_ACTORS - COOP_ENEMIES_LEVEL_ACTORS - actor_data->actual_count;
|
||||
return (short)PIN(extra, 0, MAX(room, 0));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
real_point3d from = *origin;
|
||||
short place = (short)((number - 1) % SPREAD_PLACES_PER_RING);
|
||||
short ring = (short)((number - 1) / SPREAD_PLACES_PER_RING);
|
||||
short attempt;
|
||||
|
||||
from.z += SPREAD_STEP_HEIGHT;
|
||||
for (attempt = 0; attempt < FALLBACK_RINGS; 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;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void coop_enemies_rider_seated(
|
||||
long vehicle_index,
|
||||
long unit_index)
|
||||
{
|
||||
struct coop_riding_vehicle *vehicle;
|
||||
struct unit_datum *unit;
|
||||
|
||||
if (!coop_enemies_on())
|
||||
return;
|
||||
vehicle = coop_enemies_riding_vehicle(vehicle_index, TRUE);
|
||||
unit = unit_get(unit_index);
|
||||
if (!vehicle)
|
||||
return;
|
||||
/* (a vehicle loaded again: the riders that got out of it before are not
|
||||
this load's) */
|
||||
{
|
||||
short index, kept = 0;
|
||||
|
||||
for (index = 0; index < vehicle->seated_count; index++)
|
||||
{
|
||||
struct object_datum *seated = object_try_and_get_and_verify_type(vehicle->seated_unit_indices[index],
|
||||
_object_mask_unit);
|
||||
|
||||
if (seated && seated->object.parent_object_index == vehicle_index)
|
||||
{
|
||||
vehicle->seated_unit_indices[kept] = vehicle->seated_unit_indices[index];
|
||||
vehicle->seat_indices[kept++] = vehicle->seat_indices[index];
|
||||
}
|
||||
}
|
||||
vehicle->seated_count = kept;
|
||||
}
|
||||
if (vehicle->seated_count >= MAXIMUM_SEATED_RIDERS)
|
||||
return;
|
||||
vehicle->seated_unit_indices[vehicle->seated_count] = unit_index;
|
||||
vehicle->seat_indices[vehicle->seated_count] = unit->unit.parent_seat_index;
|
||||
vehicle->seated_count++;
|
||||
/* (a vehicle loaded again, after its riders got out: they get out again) */
|
||||
vehicle->releasing = FALSE;
|
||||
}
|
||||
|
||||
boolean coop_enemies_rider_unseated(
|
||||
long vehicle_index,
|
||||
long unit_index)
|
||||
{
|
||||
struct unit_datum *unit;
|
||||
struct actor_datum *actor;
|
||||
struct coop_kept_rider *rider;
|
||||
struct coop_riding_vehicle *vehicle;
|
||||
|
||||
if (!coop_enemies_on() || coop_enemies.rider_count >= MAXIMUM_KEPT_RIDERS)
|
||||
return FALSE;
|
||||
unit = unit_get(unit_index);
|
||||
vehicle = coop_enemies_riding_vehicle(vehicle_index, FALSE);
|
||||
/* (kept only for a vehicle with riders seated, whose getting out places
|
||||
it: one of a load that seated nobody is left where it is) */
|
||||
if (unit->object.type != _object_type_biped || unit->unit.actor_index == NONE || !vehicle ||
|
||||
vehicle->seated_count == 0)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
actor = actor_get(unit->unit.actor_index);
|
||||
if (actor->meta.swarm || actor->meta.encounter_index == NONE ||
|
||||
!game_team_is_enemy(_game_team_player, encounter_get(actor->meta.encounter_index)->team_index))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
rider = &coop_enemies.riders[coop_enemies.rider_count++];
|
||||
rider->vehicle_index = vehicle_index;
|
||||
rider->variant_definition_index = actor->meta.variant_definition_index;
|
||||
rider->encounter_index = actor->meta.encounter_index;
|
||||
rider->squad_index = actor->meta.squad_index;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void coop_enemies_update(
|
||||
void)
|
||||
{
|
||||
long now = game_time_get();
|
||||
short vehicle_number;
|
||||
|
||||
if (!coop_enemies_host())
|
||||
return;
|
||||
for (vehicle_number = coop_enemies.vehicle_count - 1; vehicle_number >= 0; vehicle_number--)
|
||||
{
|
||||
struct coop_riding_vehicle *vehicle = &coop_enemies.vehicles[vehicle_number];
|
||||
struct object_datum *object = object_try_and_get_and_verify_type(vehicle->vehicle_index, _object_mask_unit);
|
||||
short index;
|
||||
|
||||
/* (the vehicle's next kept rider) */
|
||||
for (index = 0; index < coop_enemies.rider_count; index++)
|
||||
{
|
||||
if (coop_enemies.riders[index].vehicle_index == vehicle->vehicle_index)
|
||||
break;
|
||||
}
|
||||
/* (a vehicle gone, or destroyed, takes its kept riders with it; one
|
||||
with none kept, every one loaded seated or all placed, is done with) */
|
||||
if (!object || TEST_FLAG(object->object.damage_flags, _object_dead_bit) || index >= coop_enemies.rider_count)
|
||||
{
|
||||
coop_enemies_forget_vehicle(vehicle_number);
|
||||
continue;
|
||||
}
|
||||
if (!vehicle->releasing)
|
||||
{
|
||||
if (!coop_enemies_riders_getting_out(vehicle))
|
||||
continue;
|
||||
vehicle->releasing = TRUE;
|
||||
vehicle->released_count = 0;
|
||||
vehicle->next_release_time = now;
|
||||
}
|
||||
if (now < vehicle->next_release_time)
|
||||
continue;
|
||||
{
|
||||
struct coop_kept_rider rider = coop_enemies.riders[index];
|
||||
struct encounter_definition *encounter_definition;
|
||||
struct squad_definition *squad_definition;
|
||||
struct actor_starting_location location;
|
||||
|
||||
coop_enemies.riders[index] = coop_enemies.riders[--coop_enemies.rider_count];
|
||||
encounter_definition = TAG_BLOCK_GET_ELEMENT(&global_scenario_get()->ai_encounters,
|
||||
DATUM_INDEX_TO_ABSOLUTE_INDEX(rider.encounter_index), struct encounter_definition);
|
||||
squad_definition = TAG_BLOCK_GET_ELEMENT(&encounter_definition->squads, rider.squad_index,
|
||||
struct squad_definition);
|
||||
/* (as the squad's: its states, no command list of a starting
|
||||
location's) */
|
||||
csmemset(&location, 0, sizeof(location));
|
||||
coop_enemies_rider_place(vehicle, vehicle->released_count++, &location.position, &location.facing);
|
||||
location.cluster_index = NONE;
|
||||
location.noncombat_sequence_id = NONE;
|
||||
location.default_state = squad_definition->default_state;
|
||||
location.initial_state = squad_definition->initial_state;
|
||||
location.actor_variant_index = NONE;
|
||||
location.command_list_index = NONE;
|
||||
actor_place(rider.variant_definition_index, rider.encounter_index, rider.squad_index, &location, FALSE, 0);
|
||||
encounter_update_status(rider.encounter_index);
|
||||
}
|
||||
vehicle->next_release_time = now + RIDER_RELEASE_TICKS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
COOP_ENEMIES.H
|
||||
|
||||
Network co-op's extra enemies, for its players (coop_enemies.c).
|
||||
*/
|
||||
|
||||
#ifndef __COOP_ENEMIES_H
|
||||
#define __COOP_ENEMIES_H
|
||||
|
||||
/* network_coop.c: a game begins (its EXTRA ENEMIES read), or the host's
|
||||
game state went back (a cutscene skipped): the dropships' riders kept are
|
||||
forgotten */
|
||||
void coop_enemies_new_game(
|
||||
void);
|
||||
void coop_enemies_reset(
|
||||
void);
|
||||
|
||||
/* encounters.c: how many enemies more a squad placed with `count` gets
|
||||
(none but on a co-op host, for an encounter of the players' enemies) */
|
||||
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. */
|
||||
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);
|
||||
|
||||
/* units.c's vehicle_scripting_load_magic: a rider seated, and one left
|
||||
without a seat (the vehicle's seats taken: an extra enemy), which is kept,
|
||||
off the map, until the vehicle's riders get out (TRUE: kept, the caller
|
||||
erases it) */
|
||||
void coop_enemies_rider_seated(
|
||||
long vehicle_index,
|
||||
long unit_index);
|
||||
boolean coop_enemies_rider_unseated(
|
||||
long vehicle_index,
|
||||
long unit_index);
|
||||
|
||||
/* network_coop.c, after each of the host's ticks: the riders kept placed
|
||||
at their vehicle's seats once its own riders get out */
|
||||
void coop_enemies_update(
|
||||
void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
COOP_SPECTATE.C
|
||||
|
||||
Spectating in network co-op. A dead player watches a living teammate
|
||||
until they respawn (players.c respawns them beside a teammate once it is
|
||||
safe). The campaign's dead camera would watch the body; instead this
|
||||
follows a teammate from behind, and A (jump on the keyboard) switches to
|
||||
the next one. director.c and hud.c call in here.
|
||||
|
||||
It also draws the line telling players about the vote to skip a cutscene
|
||||
(network_coop.c counts the votes).
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
#include "camera/dead_camera.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_engine.h"
|
||||
#include "game/players.h"
|
||||
#include "input/input.h"
|
||||
#include "interface/hud_messaging.h"
|
||||
#include "objects/objects.h"
|
||||
#include "rasterizer/rasterizer.h"
|
||||
#include "render/render.h"
|
||||
#include "text/draw_string.h"
|
||||
#include "text/font_group.h"
|
||||
#include "text/unicode.h"
|
||||
#include "units/units.h"
|
||||
|
||||
#include "coop_spectate.h"
|
||||
#include "network_coop.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* camera distance behind the unit, pitch (radians), and how much of the
|
||||
turn toward the unit's facing it makes each frame */
|
||||
#define SPECTATE_DISTANCE 3.5f
|
||||
#define SPECTATE_PITCH -0.3f
|
||||
#define SPECTATE_TURN 0.15f
|
||||
|
||||
/* a passenger (riding a Pelican) is watched from in front of them, above
|
||||
and looking down, so the camera stays inside the vehicle; a wide view (the
|
||||
observer allows up to 90 degrees) takes in all of them from that close */
|
||||
#define SPECTATE_PASSENGER_DISTANCE 1.0f
|
||||
#define SPECTATE_PASSENGER_PITCH -0.5f
|
||||
#define SPECTATE_PASSENGER_FIELD_OF_VIEW DEGREES_TO_RADIANS(85.0f)
|
||||
/* field of view for every other spectate view (dead_camera.c's default) */
|
||||
#define SPECTATE_FIELD_OF_VIEW DEGREES_TO_RADIANS(70.0f)
|
||||
|
||||
/* a teammate the scripts hold (Pillar of Autumn's cryo tube) is watched from
|
||||
in front, far enough back to see all of what holds them */
|
||||
#define SPECTATE_HELD_DISTANCE 2.0f
|
||||
#define SPECTATE_HELD_PITCH -0.1f
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
/* the player each local player is watching, or NONE */
|
||||
static long coop_spectate_watched[MAXIMUM_LOCAL_PLAYERS] = { NONE, NONE, NONE, NONE };
|
||||
|
||||
/* input_abstraction.c: how many ticks the keyboard's jump key has been held */
|
||||
byte input_abstraction_port_accept(short controller_index);
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
/* The next living player after `after` (or the first, if `after` is NONE),
|
||||
skipping `self` and wrapping around. NONE if nobody else is alive. */
|
||||
static long next_living_player(long self, long after)
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
long first = NONE;
|
||||
boolean passed = after == NONE;
|
||||
|
||||
data_iterator_new(&iterator, player_data);
|
||||
while ((player = data_iterator_next(&iterator)) != NULL)
|
||||
{
|
||||
if (iterator.datum_index == after)
|
||||
{
|
||||
passed = TRUE;
|
||||
continue;
|
||||
}
|
||||
if (iterator.datum_index == self || player->unit_index == NONE)
|
||||
continue;
|
||||
if (passed)
|
||||
return iterator.datum_index;
|
||||
if (first == NONE)
|
||||
first = iterator.datum_index;
|
||||
}
|
||||
|
||||
return first;
|
||||
}
|
||||
|
||||
/* Returns TRUE once per press of A (or jump on the keyboard), however long
|
||||
it is held. The buttons count ticks, and one tick can span several frames,
|
||||
so testing "held" alone would switch more than once. */
|
||||
static boolean next_pressed(short controller_index)
|
||||
{
|
||||
static boolean held[MAXIMUM_GAMEPADS];
|
||||
struct gamepad_state const *gamepad;
|
||||
boolean down;
|
||||
boolean pressed;
|
||||
|
||||
if (controller_index < 0 || controller_index >= MAXIMUM_GAMEPADS)
|
||||
return FALSE;
|
||||
gamepad = input_get_gamepad_state(controller_index);
|
||||
down = (gamepad && gamepad->buttons[FIRST_GAMEPAD_ANALOG_BUTTON + _gamepad_analog_button_a] > 0) ||
|
||||
input_abstraction_port_accept(controller_index) > 0;
|
||||
pressed = down && !held[controller_index];
|
||||
held[controller_index] = down;
|
||||
return pressed;
|
||||
}
|
||||
|
||||
/* whether the unit rides in a vehicle it isn't driving */
|
||||
static boolean unit_is_passenger(
|
||||
long unit_index)
|
||||
{
|
||||
long vehicle_index = object_get(unit_index)->object.parent_object_index;
|
||||
|
||||
return vehicle_index != NONE && object_try_and_get_and_verify_type(vehicle_index, _object_mask_unit) &&
|
||||
unit_get(vehicle_index)->unit.driver_object_index != unit_index;
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
boolean coop_spectating(
|
||||
void)
|
||||
{
|
||||
return network_coop_active();
|
||||
}
|
||||
|
||||
long coop_spectate_unit(
|
||||
short local_player_index)
|
||||
{
|
||||
long self = local_player_get_player_index(local_player_index);
|
||||
long *watched = &coop_spectate_watched[local_player_index];
|
||||
struct player_datum *player;
|
||||
|
||||
boolean next;
|
||||
|
||||
if (self == NONE)
|
||||
return NONE;
|
||||
player = player_get(self);
|
||||
/* checked every frame so a press is never missed or counted twice */
|
||||
next = next_pressed(player->network_player_data.controller_index);
|
||||
/* alive: keep `watched`, so the next death starts on the same teammate */
|
||||
if (player->unit_index != NONE)
|
||||
return NONE;
|
||||
if (*watched == NONE || !player_try_and_get(*watched) || player_get(*watched)->unit_index == NONE || next)
|
||||
{
|
||||
*watched = next_living_player(self, *watched);
|
||||
}
|
||||
|
||||
return *watched != NONE ? player_get(*watched)->unit_index : NONE;
|
||||
}
|
||||
|
||||
boolean coop_spectate_watching_rider(
|
||||
short local_player_index)
|
||||
{
|
||||
long self = local_player_get_player_index(local_player_index);
|
||||
long watched = coop_spectate_watched[local_player_index];
|
||||
long unit_index;
|
||||
long driver_index;
|
||||
|
||||
if (self == NONE || player_get(self)->unit_index != NONE)
|
||||
return FALSE;
|
||||
/* the scripted camera may have been running since before this player
|
||||
died, so nobody has been picked to watch yet */
|
||||
if (watched == NONE || !player_try_and_get(watched) || player_get(watched)->unit_index == NONE)
|
||||
watched = next_living_player(self, NONE);
|
||||
unit_index = watched != NONE ? player_get(watched)->unit_index : NONE;
|
||||
if (unit_index == NONE || !unit_is_passenger(unit_index))
|
||||
return FALSE;
|
||||
driver_index = unit_get(object_get(unit_index)->object.parent_object_index)->unit.driver_object_index;
|
||||
return driver_index == NONE || unit_get(driver_index)->unit.player_index == NONE;
|
||||
}
|
||||
|
||||
boolean coop_spectate_nothing_to_watch(
|
||||
short local_player_index)
|
||||
{
|
||||
long self = local_player_get_player_index(local_player_index);
|
||||
|
||||
return self != NONE && player_get(self)->unit_index == NONE && next_living_player(self, NONE) == NONE;
|
||||
}
|
||||
|
||||
void coop_spectate_camera(
|
||||
struct dead_camera *camera)
|
||||
{
|
||||
struct object_datum *unit = camera->unit_index != NONE ? object_try_and_get(camera->unit_index) : NULL;
|
||||
real yaw, turn;
|
||||
|
||||
if (!unit)
|
||||
return;
|
||||
yaw = (real)atan2(unit->object.forward.j, unit->object.forward.i);
|
||||
if (unit_is_passenger(camera->unit_index))
|
||||
{
|
||||
/* fixed in front of the seat, turning with the vehicle */
|
||||
camera->facing.yaw = yaw + _pi;
|
||||
camera->facing.pitch = SPECTATE_PASSENGER_PITCH;
|
||||
camera->distance = SPECTATE_PASSENGER_DISTANCE;
|
||||
camera->field_of_view = SPECTATE_PASSENGER_FIELD_OF_VIEW;
|
||||
return;
|
||||
}
|
||||
camera->field_of_view = SPECTATE_FIELD_OF_VIEW;
|
||||
if (!player_input_enabled())
|
||||
{
|
||||
camera->facing.yaw = yaw + _pi;
|
||||
camera->facing.pitch = SPECTATE_HELD_PITCH;
|
||||
camera->distance = SPECTATE_HELD_DISTANCE;
|
||||
return;
|
||||
}
|
||||
/* ease the camera round behind the unit */
|
||||
turn = yaw - camera->facing.yaw;
|
||||
while (turn > _pi)
|
||||
turn -= 2.0f * _pi;
|
||||
while (turn < -_pi)
|
||||
turn += 2.0f * _pi;
|
||||
camera->facing.yaw += turn * SPECTATE_TURN;
|
||||
camera->facing.pitch = SPECTATE_PITCH;
|
||||
camera->distance = SPECTATE_DISTANCE;
|
||||
}
|
||||
|
||||
/* centred text near the bottom of the screen, in the HUD's font */
|
||||
static void draw_bottom_text(
|
||||
wchar_t const *text)
|
||||
{
|
||||
long font_index = hud_get_font_index();
|
||||
struct font_header *font;
|
||||
real_argb_color color = *global_real_argb_white;
|
||||
rectangle2d bounds;
|
||||
short height;
|
||||
|
||||
if (font_index == NONE)
|
||||
return;
|
||||
font = font_definition_get(font_index);
|
||||
height = (short)(font->ascending_height + font->descending_height);
|
||||
bounds = render.camera.window_bounds;
|
||||
bounds.y0 = (short)(bounds.y1 - 3 * height - 24);
|
||||
bounds.y1 = (short)(bounds.y1 - 24);
|
||||
/* draw mode 2: centered */
|
||||
draw_string_set_draw_mode(font_index, NONE, 2, 0, &color);
|
||||
rasterizer_draw_unicode_string(&bounds, NULL, NULL, 0, text);
|
||||
}
|
||||
|
||||
void coop_spectate_draw(
|
||||
short local_player_index)
|
||||
{
|
||||
long watched = coop_spectate_watched[local_player_index];
|
||||
wchar_t text[96];
|
||||
wchar_t const *hint;
|
||||
|
||||
/* nobody to watch yet (still joining): network_coop.c shows the host's
|
||||
view from behind instead */
|
||||
if (watched == NONE || !player_try_and_get(watched))
|
||||
{
|
||||
if (coop_spectate_nothing_to_watch(local_player_index))
|
||||
draw_bottom_text(L"JOINING THE GAME\r\nYou spawn beside a teammate once it is safe");
|
||||
return;
|
||||
}
|
||||
if (!players_coop_waiting_to_start(local_player_get_player_index(local_player_index)))
|
||||
hint = L"You come back beside them once it is safe";
|
||||
else
|
||||
hint = L"You join them when they're on foot";
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"SPECTATING %.12s (A: NEXT)\r\n%s", player_get(watched)->name, hint);
|
||||
text[NUMBEROF(text) - 1] = 0;
|
||||
draw_bottom_text(text);
|
||||
}
|
||||
|
||||
void coop_skip_vote_draw(
|
||||
short local_player_index)
|
||||
{
|
||||
short votes, voters;
|
||||
boolean voted;
|
||||
wchar_t text[96];
|
||||
|
||||
/* once, for the first local player */
|
||||
if (local_player_index != local_player_get_next(NONE) || !network_coop_skip_vote_status(&votes, &voters, &voted))
|
||||
return;
|
||||
if (voted)
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"VOTED TO SKIP %d OF %d", votes, voters);
|
||||
else
|
||||
usnprintf(text, NUMBEROF(text) - 1, L"PRESS SPACE OR A TO VOTE TO SKIP %d OF %d", votes, voters);
|
||||
text[NUMBEROF(text) - 1] = 0;
|
||||
draw_bottom_text(text);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
COOP_SPECTATE.H
|
||||
|
||||
Spectating in network co-op, and the cutscene skip vote's line
|
||||
(coop_spectate.c).
|
||||
*/
|
||||
|
||||
#ifndef __COOP_SPECTATE_H
|
||||
#define __COOP_SPECTATE_H
|
||||
|
||||
struct dead_camera;
|
||||
|
||||
/* whether this is network co-op: a network game with no game engine (a campaign map) */
|
||||
boolean coop_spectating(
|
||||
void);
|
||||
|
||||
/* the unit a dead local player watches (A switches teammate), or NONE if
|
||||
the player is alive or nobody else is */
|
||||
long coop_spectate_unit(
|
||||
short local_player_index);
|
||||
|
||||
/* TRUE if the teammate this local player is spectating (while dead or not
|
||||
yet spawned) rides in an AI-driven vehicle, such as an insertion Pelican.
|
||||
network_coop.c then shows that seat instead of the scripted camera. */
|
||||
boolean coop_spectate_watching_rider(
|
||||
short local_player_index);
|
||||
|
||||
/* whether a local player has nothing to look at: no unit of its own and
|
||||
no living teammate to watch (network_coop.c shows it the host's view) */
|
||||
boolean coop_spectate_nothing_to_watch(
|
||||
short local_player_index);
|
||||
|
||||
/* keeps the dead camera behind the watched unit as it turns */
|
||||
void coop_spectate_camera(
|
||||
struct dead_camera *camera);
|
||||
|
||||
/* the HUD line saying who a dead local player is watching */
|
||||
void coop_spectate_draw(
|
||||
short local_player_index);
|
||||
|
||||
/* during a skippable cutscene in network co-op, the vote count and how to vote */
|
||||
void coop_skip_vote_draw(
|
||||
short local_player_index);
|
||||
|
||||
#endif
|
||||
@@ -23,6 +23,11 @@ game keeps its own menus.
|
||||
The tag table cannot grow where it is (the tags' names follow it), so it
|
||||
is copied, with ours after it; every existing tag keeps its index. All of it
|
||||
is let go in scenario_tags_unload, before the next map's tags load.
|
||||
|
||||
A multiplayer map gets them too, for its in-game pause menu's SETTINGS
|
||||
(pause_patch): there the map's tags they name that it has not (ui.map's
|
||||
pictures, sounds and screens) are none, a missing picture drawn blank, and
|
||||
no screen pauses the game (a network game's clock runs on).
|
||||
*/
|
||||
|
||||
#include "cseries.h"
|
||||
@@ -40,6 +45,11 @@ is let go in scenario_tags_unload, before the next map's tags load.
|
||||
/* the platform layer's (port/linux/src) */
|
||||
void platform_log(char const *format, ...);
|
||||
char const *config_string(char const *name);
|
||||
int platform_display_resolutions(long *widths, long *heights, int maximum);
|
||||
int platform_window_sizes(long *widths, long *heights, int maximum);
|
||||
|
||||
/* the game's network server (this machine hosts: network_game_globals.c) */
|
||||
void *global_network_game_server_get(void);
|
||||
|
||||
/* cache_files.c's (port) */
|
||||
void *cache_files_tag_instances(long *count);
|
||||
@@ -59,6 +69,13 @@ boolean pc_menu_tag(long tag_index);
|
||||
/* ---------- constants */
|
||||
|
||||
#define PC_MENU_TAG_PREFIX "pc\\"
|
||||
/* the multiplayer maps' widget collection, whose pause screens pause_patch
|
||||
adds to */
|
||||
#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
|
||||
/* the tags pause_patch adds: two buttons and their texts */
|
||||
#define PAUSE_PATCH_TAGS 4
|
||||
/* what a frame of ui.map's is drawn as in a game map, which has not got it */
|
||||
#define BLANK_PNG "ce/port/blank__0.png"
|
||||
#define UI_WIDGET_DEFINITION_TAG 'DeLa'
|
||||
#define UNICODE_STRING_LIST_TAG 'ustr'
|
||||
#define FONT_TAG 'font'
|
||||
@@ -326,6 +343,12 @@ static char const *const port_function_names[] =
|
||||
"mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game",
|
||||
"port lobby preview join",
|
||||
"port setup edit",
|
||||
"port map list back",
|
||||
"port pause end game",
|
||||
"port coop begin", "port coop player 2 list initialize", "port coop player 2",
|
||||
"port lobby open", "port lobby add player", "port lobby join", "port lobby leave",
|
||||
"port lobby player list initialize", "port lobby player choose",
|
||||
"port lobby preview add", "port lobby preview leave",
|
||||
/* (the gametype editor's: the Xbox's walk their rows by place, which the
|
||||
PC version's screens changed) */
|
||||
"mp profile begin editing", "mp profile save changes", "request del playlist profile", "final del playlist profile",
|
||||
@@ -335,6 +358,11 @@ static char const *const port_function_names[] =
|
||||
"mp profile init indicator opts", "mp profile set ctf rules", "mp profile set koth rules",
|
||||
"mp profile set slayer rules", "mp profile set oddball rules", "mp profile set racing rules",
|
||||
"mp profile set player options", "mp profile set item options", "mp profile set indicator opts",
|
||||
/* (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
|
||||
@@ -352,6 +380,7 @@ 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
|
||||
@@ -384,6 +413,9 @@ static struct
|
||||
long *text_tags;
|
||||
long *spinner_tags;
|
||||
boolean failed;
|
||||
/* a multiplayer map's build (the pause menu's SETTINGS): what the map
|
||||
has not got is none, not a failure */
|
||||
boolean game_map;
|
||||
} build;
|
||||
|
||||
/* ---------- private code */
|
||||
@@ -497,7 +529,8 @@ static long map_tag(long group_tag, char const *name, char const *file, long lin
|
||||
{
|
||||
long index = tag_loaded(group_tag, name);
|
||||
|
||||
if (index == NONE)
|
||||
/* (a game map has few of ui.map's: those are none) */
|
||||
if (index == NONE && !build.game_map)
|
||||
problem(file, line, "the map has no tag", name);
|
||||
return index;
|
||||
}
|
||||
@@ -674,6 +707,41 @@ static long split(char const *text, char const **pieces)
|
||||
return count;
|
||||
}
|
||||
|
||||
/* a spinner's strings, or its values, split: its own, but Video Setup's
|
||||
sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set
|
||||
"1920x1080": its resolutions after Resolution's own (NATIVE), and the
|
||||
window sizes that fit it in place of Window Size's own */
|
||||
static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces)
|
||||
{
|
||||
long count = split(values ? widget->values : widget->strings, pieces);
|
||||
long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS];
|
||||
long added, index;
|
||||
|
||||
if (widget->setting && !strcmp(widget->setting, "display.resolution"))
|
||||
added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count));
|
||||
else if (widget->setting && !strcmp(widget->setting, "display.window_size"))
|
||||
{
|
||||
added = platform_window_sizes(widths, heights, MAXIMUM_STRINGS);
|
||||
if (added > 0)
|
||||
count = 0;
|
||||
}
|
||||
else
|
||||
return count;
|
||||
for (index = 0; index < added; index++)
|
||||
{
|
||||
char text[32];
|
||||
char *piece;
|
||||
|
||||
snprintf(text, sizeof(text), values ? "%ldx%ld" : "%ld x %ld", widths[index], heights[index]);
|
||||
piece = allocate((long)strlen(text) + 1);
|
||||
if (!piece)
|
||||
break;
|
||||
strcpy(piece, text);
|
||||
pieces[count++] = piece;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
|
||||
{
|
||||
struct bitmap_group *group = allocate(sizeof(struct bitmap_group));
|
||||
@@ -698,10 +766,18 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
|
||||
{
|
||||
struct halo_menu_frame const *data = &build.menus->frames[source->first_frame + frame];
|
||||
struct bitmap_data *bitmap = &bitmaps[frame];
|
||||
char const *png = data->png;
|
||||
long width = data->width, height = data->height;
|
||||
|
||||
/* (a frame of the map's: a copy of its bitmap, which the texture cache
|
||||
loads from the map as its own) */
|
||||
if (data->map)
|
||||
loads from the map as its own; in a game map that has not got it,
|
||||
nothing) */
|
||||
if (data->map && build.game_map && tag_loaded(BITMAP_GROUP_TAG, data->map) == NONE)
|
||||
{
|
||||
png = BLANK_PNG;
|
||||
width = height = PLACEHOLDER_SIZE;
|
||||
}
|
||||
else if (data->map)
|
||||
{
|
||||
long group_index = map_tag(BITMAP_GROUP_TAG, data->map, source->file, source->line);
|
||||
struct bitmap_group *group_source;
|
||||
@@ -720,9 +796,9 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data->width <= 0 || data->height <= 0 || data->width > 2048 || data->height > 2048)
|
||||
if (width <= 0 || height <= 0 || width > 2048 || height > 2048)
|
||||
{
|
||||
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", data->png);
|
||||
problem(source->file, source->line, "a frame is 1 to 2048 wide and high:", png);
|
||||
return group;
|
||||
}
|
||||
/* (its D3D texture is a small one, which its PNG is drawn for: the
|
||||
@@ -741,13 +817,13 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
|
||||
problem(source->file, source->line, "could not make the texture of bitmap", source->name);
|
||||
return group;
|
||||
}
|
||||
bitmap->width = (short)data->width;
|
||||
bitmap->height = (short)data->height;
|
||||
bitmap->width = (short)width;
|
||||
bitmap->height = (short)height;
|
||||
menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps));
|
||||
menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap;
|
||||
/* (none without a renderer: debug.null_renderer) */
|
||||
if (bitmap->hardware_format)
|
||||
halo_menus_art_register(bitmap->hardware_format, data->png);
|
||||
halo_menus_art_register(bitmap->hardware_format, png);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
@@ -880,7 +956,7 @@ static void setting_add(struct halo_menu_widget const *source, long definition_i
|
||||
setting->definition_index = definition_index;
|
||||
setting->setting = source->setting;
|
||||
setting->loaded_index = NONE;
|
||||
setting->value_count = split(source->values, setting->values);
|
||||
setting->value_count = spinner_split(source, TRUE, setting->values);
|
||||
}
|
||||
|
||||
/* a font: large, small, terminal, or the map's by its path */
|
||||
@@ -928,6 +1004,9 @@ static void *widget_build(long widget_index)
|
||||
definition->bounds.x1 = (short)(source->left + (source->has_width ? source->width : 640));
|
||||
definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names),
|
||||
source->file, source->line);
|
||||
/* (in a network game the clock runs on) */
|
||||
if (build.game_map)
|
||||
definition->flags &= ~FLAG(1); /* pause_game */
|
||||
definition->milliseconds_to_auto_close = source->auto_close;
|
||||
definition->auto_close_fade_time = source->auto_close_fade;
|
||||
reference_clear(&definition->background_bitmap, BITMAP_GROUP_TAG);
|
||||
@@ -1170,6 +1249,14 @@ 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;
|
||||
@@ -1250,6 +1337,281 @@ static void menu_tags_release(void)
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
/* ---------- the in-game pause menu (a multiplayer map's)
|
||||
|
||||
Its list (RESUME GAME, LEAVE GAME; a custom map's own buttons too) gets
|
||||
SETTINGS, the menus' settings (as the main menu's SETTINGS opens them), and
|
||||
for the host END GAME, which ends the game as its time limit would: its
|
||||
players stay, and the host picks the next map after the carnage report.
|
||||
They are copies of the map's LEAVE GAME button (its look), put before it.
|
||||
The map's pause screens move to fit them: on the Xbox's own (its box of
|
||||
pausebox2 pieces), the box is the port's taller one (pause/pausebox_*:
|
||||
tools/port_settings.py), centred where the old one was; on a custom map's,
|
||||
what is below the list moves down. */
|
||||
|
||||
#define PAUSE_SETTINGS_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/player_profile_edit_screen"
|
||||
#define PAUSE_BUTTON_SPACING 35
|
||||
#define PAUSE_BOX_FIRST_BUTTONS 3
|
||||
|
||||
static long ui_function_named(char const *name)
|
||||
{
|
||||
long index;
|
||||
|
||||
for (index = 0; index < 512; index++)
|
||||
{
|
||||
char const *function = ui_widget_event_handler_function_name(index);
|
||||
|
||||
if (!function)
|
||||
break;
|
||||
if (!strcmp(function, name))
|
||||
return index;
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
static boolean tag_name_ends(long tag_index, char const *end)
|
||||
{
|
||||
char const *name = tag_index == NONE ? NULL : tag_get_name(tag_index);
|
||||
size_t length = name ? strlen(name) : 0, end_length = strlen(end);
|
||||
|
||||
return name && length >= end_length && !strcmp(name + length - end_length, end);
|
||||
}
|
||||
|
||||
/* the index of the list's button that runs the function (LEAVE GAME's),
|
||||
else NONE */
|
||||
static long pause_quit_button(struct ui_widget_definition const *list, long quit_function)
|
||||
{
|
||||
struct ui_widget_child_reference const *children = list->child_widgets.address;
|
||||
long child;
|
||||
|
||||
for (child = 0; child < list->child_widgets.count; child++)
|
||||
{
|
||||
struct ui_widget_definition const *button;
|
||||
struct ui_widget_event_handler_reference const *handlers;
|
||||
long handler;
|
||||
|
||||
if (children[child].widget_tag.index == NONE)
|
||||
continue;
|
||||
button = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
|
||||
handlers = button->event_handlers.address;
|
||||
for (handler = 0; handler < button->event_handlers.count; handler++)
|
||||
{
|
||||
if (handlers[handler].function == quit_function &&
|
||||
TEST_FLAG(handlers[handler].flags, _event_handler_run_function_bit))
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
/* a button like the model (its look), its own text, each of the model's
|
||||
events (its A and Start) running the function and opening the widget (if
|
||||
not NONE) */
|
||||
static void *pause_button(struct cache_file_tag_instance *instances, struct ui_widget_definition const *model,
|
||||
char const *name, char const *text, long function, long widget_tag)
|
||||
{
|
||||
struct ui_widget_definition *button = allocate(sizeof(*button));
|
||||
struct ui_widget_event_handler_reference *handlers =
|
||||
allocate(model->event_handlers.count * sizeof(struct ui_widget_event_handler_reference));
|
||||
long text_tag = next_tag(), handler;
|
||||
|
||||
if (!button || !handlers)
|
||||
return NULL;
|
||||
memcpy(button, model, sizeof(*button));
|
||||
instance_set(instances, UNICODE_STRING_LIST_TAG, text_tag, name, " text", string_list_build(&text, 1));
|
||||
reference_set(&button->text_label_string_list, UNICODE_STRING_LIST_TAG, text_tag);
|
||||
button->string_list_index = 0;
|
||||
button->game_data_inputs.count = 0;
|
||||
button->game_data_inputs.address = NULL;
|
||||
/* (the game's memcpy asserts on a NULL source, even for nothing) */
|
||||
if (model->event_handlers.count)
|
||||
memcpy(handlers, model->event_handlers.address, model->event_handlers.count * sizeof(*handlers));
|
||||
for (handler = 0; handler < model->event_handlers.count; handler++)
|
||||
{
|
||||
handlers[handler].flags = FLAG(_event_handler_run_function_bit);
|
||||
handlers[handler].function = (short)function;
|
||||
handlers[handler].script[0] = 0;
|
||||
reference_set(&handlers[handler].widget_tag, UI_WIDGET_DEFINITION_TAG, widget_tag);
|
||||
if (widget_tag != NONE)
|
||||
{
|
||||
handlers[handler].flags |= FLAG(_event_handler_open_widget_bit) |
|
||||
FLAG(_event_handler_try_to_branch_on_failure_bit);
|
||||
}
|
||||
}
|
||||
button->event_handlers.address = handlers;
|
||||
return button;
|
||||
}
|
||||
|
||||
/* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE
|
||||
GAME (quit); returns how many were added */
|
||||
static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit,
|
||||
boolean host)
|
||||
{
|
||||
struct ui_widget_child_reference *children = list->child_widgets.address;
|
||||
long count = list->child_widgets.count, added = host ? 2 : 1, child;
|
||||
struct ui_widget_child_reference *grown = allocate((count + added) * sizeof(struct ui_widget_child_reference));
|
||||
struct ui_widget_definition const *model = tag_get(UI_WIDGET_DEFINITION_TAG, children[quit].widget_tag.index);
|
||||
long settings_widget = widget_named(PAUSE_SETTINGS_SCREEN);
|
||||
short spacing = count >= 2 ? (short)(children[count - 1].vertical_offset - children[count - 2].vertical_offset) :
|
||||
PAUSE_BUTTON_SPACING;
|
||||
void *buttons[2];
|
||||
long tags[2];
|
||||
|
||||
if (!grown || settings_widget == NONE || spacing <= 0)
|
||||
return 0;
|
||||
tags[0] = next_tag();
|
||||
buttons[0] = pause_button(instances, model, "pause/settings_button", "SETTINGS",
|
||||
function_index("profile set edit begin", "pause", 0), build.widget_tags[settings_widget]);
|
||||
tags[1] = host ? next_tag() : NONE;
|
||||
buttons[1] = host ? pause_button(instances, model, "pause/end_game_button", "END GAME",
|
||||
function_index("port pause end game", "pause", 0), NONE) : NULL;
|
||||
if (!buttons[0] || (host && !buttons[1]))
|
||||
return 0;
|
||||
for (child = 0; child < added; child++)
|
||||
{
|
||||
instance_set(instances, UI_WIDGET_DEFINITION_TAG, tags[child],
|
||||
child ? "pause/end_game_button" : "pause/settings_button", "", buttons[child]);
|
||||
}
|
||||
if (quit)
|
||||
memcpy(grown, children, quit * sizeof(*grown));
|
||||
for (child = 0; child < added; child++)
|
||||
{
|
||||
grown[quit + child] = children[quit];
|
||||
reference_set(&grown[quit + child].widget_tag, UI_WIDGET_DEFINITION_TAG, tags[child]);
|
||||
grown[quit + child].vertical_offset = (short)(children[quit].vertical_offset + child * spacing);
|
||||
}
|
||||
for (child = quit; child < count; child++)
|
||||
{
|
||||
grown[child + added] = children[child];
|
||||
grown[child + added].vertical_offset = (short)(children[child].vertical_offset + added * spacing);
|
||||
}
|
||||
list->child_widgets.address = grown;
|
||||
list->child_widgets.count = count + added;
|
||||
/* (the list draws within its bounds) */
|
||||
list->bounds.y1 = (short)(list->bounds.y1 + added * spacing);
|
||||
return added;
|
||||
}
|
||||
|
||||
/* the Xbox's own box: a widget of the three pausebox2 pieces */
|
||||
static boolean pause_box_stock(struct ui_widget_definition const *box)
|
||||
{
|
||||
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
|
||||
|
||||
return box->child_widgets.count == 3 && tag_name_ends(pieces[0].widget_tag.index, "\\pausebox2_left") &&
|
||||
tag_name_ends(pieces[1].widget_tag.index, "\\pausebox2_left_center") &&
|
||||
tag_name_ends(pieces[2].widget_tag.index, "\\pausebox2_right");
|
||||
}
|
||||
|
||||
/* the stock box's pieces drawn with the port's taller box: its frame for
|
||||
the number of buttons */
|
||||
static void pause_box_redraw(struct ui_widget_definition const *box, long buttons)
|
||||
{
|
||||
static char const *const names[] = { "pause/pausebox_left", "pause/pausebox_center", "pause/pausebox_right" };
|
||||
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
|
||||
long piece;
|
||||
|
||||
for (piece = 0; piece < 3; piece++)
|
||||
{
|
||||
struct ui_widget_definition *definition = tag_get(UI_WIDGET_DEFINITION_TAG, pieces[piece].widget_tag.index);
|
||||
long bitmap = find(names[piece], build.menus->bitmap_count, bitmap_name);
|
||||
struct bitmap_group *group;
|
||||
struct bitmap_group_sequence *sequence;
|
||||
|
||||
if (bitmap == NONE)
|
||||
continue;
|
||||
group = bitmap_group_get(build.bitmap_tags[bitmap]);
|
||||
sequence = group->sequences.address;
|
||||
/* (one frame: the one for the number of buttons) */
|
||||
sequence->first_bitmap_index = (short)PIN(buttons - PAUSE_BOX_FIRST_BUTTONS, 0, group->bitmaps.count - 1);
|
||||
sequence->bitmap_count = 1;
|
||||
reference_set(&definition->background_bitmap, BITMAP_GROUP_TAG, build.bitmap_tags[bitmap]);
|
||||
definition->bounds.y1 = (short)(definition->bounds.y0 + 256);
|
||||
}
|
||||
}
|
||||
|
||||
static void pause_patch(struct cache_file_tag_instance *instances)
|
||||
{
|
||||
long collection = tag_loaded('Soul', MULTIPLAYER_COLLECTION);
|
||||
long quit_function = ui_function_named("mp game player quit");
|
||||
boolean host = global_network_game_server_get() != NULL;
|
||||
struct tag_block const *screens;
|
||||
long patched_list = NONE, added = 0, buttons = 0, screen;
|
||||
boolean box_redrawn = FALSE;
|
||||
|
||||
if (collection == NONE || quit_function == NONE)
|
||||
return;
|
||||
screens = tag_get('Soul', collection);
|
||||
for (screen = 0; screen < screens->count; screen++)
|
||||
{
|
||||
long screen_tag = ((struct tag_reference const *)screens->address)[screen].index;
|
||||
struct ui_widget_definition *definition;
|
||||
struct ui_widget_child_reference *children;
|
||||
long child, list_child = NONE, box_child = NONE;
|
||||
short grow, list_top;
|
||||
|
||||
if (screen_tag == NONE)
|
||||
continue;
|
||||
definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen_tag);
|
||||
children = definition->child_widgets.address;
|
||||
for (child = 0; child < definition->child_widgets.count && list_child == NONE; child++)
|
||||
{
|
||||
struct ui_widget_definition *list;
|
||||
long quit;
|
||||
|
||||
if (children[child].widget_tag.index == NONE)
|
||||
continue;
|
||||
list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
|
||||
if (list->type != _widget_type_column_list)
|
||||
continue;
|
||||
/* (one list, which the screens share: patched once) */
|
||||
if (children[child].widget_tag.index == patched_list)
|
||||
{
|
||||
list_child = child;
|
||||
continue;
|
||||
}
|
||||
quit = pause_quit_button(list, quit_function);
|
||||
if (quit == NONE || patched_list != NONE)
|
||||
continue;
|
||||
added = pause_list_patch(instances, list, quit, host);
|
||||
if (!added)
|
||||
return;
|
||||
buttons = list->child_widgets.count;
|
||||
patched_list = children[child].widget_tag.index;
|
||||
list_child = child;
|
||||
}
|
||||
if (list_child == NONE)
|
||||
continue;
|
||||
for (child = 0; child < definition->child_widgets.count; child++)
|
||||
{
|
||||
if (child != list_child && children[child].widget_tag.index != NONE &&
|
||||
pause_box_stock(tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index)))
|
||||
{
|
||||
box_child = child;
|
||||
}
|
||||
}
|
||||
/* the stock box: taller, centred where it was, the list with it, and
|
||||
what is below it moved down; else only what is below the list */
|
||||
grow = (short)(added * PAUSE_BUTTON_SPACING);
|
||||
list_top = children[list_child].vertical_offset;
|
||||
for (child = 0; child < definition->child_widgets.count; child++)
|
||||
{
|
||||
if (box_child != NONE && (child == box_child || child == list_child))
|
||||
children[child].vertical_offset -= grow / 2;
|
||||
else if (children[child].vertical_offset > list_top)
|
||||
children[child].vertical_offset += box_child != NONE ? grow - grow / 2 : grow;
|
||||
}
|
||||
if (box_child != NONE && !box_redrawn)
|
||||
{
|
||||
pause_box_redraw(tag_get(UI_WIDGET_DEFINITION_TAG, children[box_child].widget_tag.index), buttons);
|
||||
box_redrawn = TRUE;
|
||||
}
|
||||
}
|
||||
if (patched_list != NONE)
|
||||
platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : "");
|
||||
}
|
||||
|
||||
void menu_tags_loaded(
|
||||
char const *map_name)
|
||||
{
|
||||
@@ -1257,17 +1619,23 @@ void menu_tags_loaded(
|
||||
struct cache_file_tag_instance *instances;
|
||||
long widget_count, own_lists = 0, total, index;
|
||||
|
||||
if (strcmp(map_name, "ui") || strcmp(config_string("display.menus"), "pc"))
|
||||
boolean game_map = strcmp(map_name, "ui") != 0;
|
||||
|
||||
/* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */
|
||||
if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) ||
|
||||
strcmp(config_string("display.menus"), "pc"))
|
||||
{
|
||||
return;
|
||||
/* (in VR, the Xbox's: the VR port's join codes, game list, host options
|
||||
and keyboard pointer are built into its screens) */
|
||||
if (halo_vr_active())
|
||||
}
|
||||
/* (in VR, the Xbox's unless vr.menus asks for the PC version's) */
|
||||
if (halo_vr_active() && strcmp(config_string("vr.menus"), "pc"))
|
||||
return;
|
||||
menus = halo_menus_load();
|
||||
if (!menus)
|
||||
return;
|
||||
memset(&build, 0, sizeof(build));
|
||||
build.menus = menus;
|
||||
build.game_map = game_map;
|
||||
widget_count = menus->widget_count;
|
||||
build.widget_tags = malloc((widget_count + 1) * sizeof(long));
|
||||
build.text_tags = malloc((widget_count + 1) * sizeof(long));
|
||||
@@ -1282,7 +1650,8 @@ void menu_tags_loaded(
|
||||
problem(menus->widgets[index].file, menus->widgets[index].line, "two widgets are named",
|
||||
menus->widgets[index].name);
|
||||
}
|
||||
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count;
|
||||
total = widget_count + own_lists + menus->string_list_count + menus->bitmap_count +
|
||||
(game_map ? PAUSE_PATCH_TAGS : 0);
|
||||
instances = build.failed ? NULL : instances_grow(total, &build.first_index, &build.first_salt);
|
||||
if (!instances)
|
||||
goto failed;
|
||||
@@ -1323,7 +1692,7 @@ void menu_tags_loaded(
|
||||
if (widget->strings)
|
||||
{
|
||||
char const *pieces[MAXIMUM_STRINGS];
|
||||
long count = split(widget->strings, pieces);
|
||||
long count = spinner_split(widget, FALSE, pieces);
|
||||
|
||||
instance_set(instances, UNICODE_STRING_LIST_TAG, build.spinner_tags[index], widget->name, " strings",
|
||||
string_list_build(pieces, count));
|
||||
@@ -1332,12 +1701,27 @@ void menu_tags_loaded(
|
||||
}
|
||||
if (build.failed)
|
||||
goto failed;
|
||||
/* (pause_patch's, until it makes them: nothing a name finds) */
|
||||
for (index = build.first_index + build.next; index < build.first_index + total; index++)
|
||||
{
|
||||
instances[index].group_tag = NONE;
|
||||
instances[index].tag_index = NONE;
|
||||
instances[index].name = "";
|
||||
}
|
||||
cache_files_set_tag_instances(instances, build.first_index + total);
|
||||
for (index = 0; index < widget_count && !build.failed; index++)
|
||||
instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(build.widget_tags[index])].base_address = widget_build(index);
|
||||
if (build.failed)
|
||||
goto failed;
|
||||
if (widget_named(menus->root) != NONE)
|
||||
if (game_map)
|
||||
{
|
||||
pause_patch(instances);
|
||||
if (build.failed)
|
||||
goto failed;
|
||||
/* (those it made) */
|
||||
cache_files_set_tag_instances(instances, build.first_index + build.next);
|
||||
}
|
||||
else if (widget_named(menus->root) != NONE)
|
||||
{
|
||||
snprintf(menu_tags.root, sizeof(menu_tags.root), "%s%s", PC_MENU_TAG_PREFIX, menus->root);
|
||||
for (index = 0; menu_tags.root[index]; index++)
|
||||
|
||||
@@ -0,0 +1,901 @@
|
||||
/*
|
||||
NETWORK_ACTORS.C
|
||||
|
||||
AI sync (port/linux/NETCODE.md).
|
||||
|
||||
Only the host runs the AI (game.c skips ai_update on clients). An actor
|
||||
drives its unit the way a player does, through unit_control. The clients
|
||||
have the units (network_objects.c); this file makes them move like the
|
||||
host's.
|
||||
|
||||
Each tick the host records the control every actor gave its unit (a hook
|
||||
in unit_control, and the bursts it holds the trigger for) and sends it to
|
||||
each client with the unit's state: position, seat, shields and health,
|
||||
camouflage, and whether it flees in a panic. Units near a client's players
|
||||
are sent every tick, others less often, but a unit starting a one-tick
|
||||
action (a jump, a grenade) goes to everyone that tick. Each tick a client
|
||||
feeds the latest control into unit_control, the same path that drives a
|
||||
remote player's unit, so the unit walks, aims, fires and throws as it does
|
||||
on the host. The state corrects any drift.
|
||||
|
||||
What the AI starts once is numbered, so a client plays each once however
|
||||
many times it receives it: animation impulses (dives, vaults), melee
|
||||
attacks and leaps (which the AI starts directly, not through the control),
|
||||
speech, and custom animations (outside co-op, whose events carry them).
|
||||
Pain and death sounds aren't sent: a client plays those itself as it
|
||||
replays the host's damage.
|
||||
|
||||
A client creates no actors of its own (actors.c): the host's are the only
|
||||
ones. Deaths need nothing here: network_damage.c kills a client's copy of
|
||||
an AI unit the same way it kills a player's.
|
||||
*/
|
||||
|
||||
/* ---------- headers */
|
||||
|
||||
#include "cseries.h"
|
||||
#include "game/game.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/damage.h"
|
||||
#include "sound/game_sound.h"
|
||||
#include "sound/sound_definitions.h"
|
||||
#include "units/units.h"
|
||||
#include "units/unit_control_data.h"
|
||||
#include "network_coop.h"
|
||||
#include "network_distributed.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
enum
|
||||
{
|
||||
/* AI units tracked at once: every actor (halo_port_capacity.h's
|
||||
HALO_PORT_MAXIMUM_ACTORS) and the units the cutscenes' recorded
|
||||
animations drive */
|
||||
MAXIMUM_NETWORK_ACTORS = HALO_PORT_MAXIMUM_ACTORS + 32,
|
||||
MAXIMUM_ENTRIES_PER_MESSAGE = 64,
|
||||
/* A client keeps applying a control for this long after the host last
|
||||
sent one. After that the unit is left alone: the actor let go of it, or
|
||||
the host went quiet. */
|
||||
CONTROL_HELD_TICKS = 2 * TICKS_PER_SECOND,
|
||||
/* an impulse is repeated in this many entries, in case one is lost */
|
||||
IMPULSE_REPEAT_TICKS = 3,
|
||||
/* the number of unit_start_animation_impulse impulses (private to units.c) */
|
||||
NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14,
|
||||
NO_IMPULSE = 0xFF,
|
||||
NO_TEAM = 0xFF,
|
||||
|
||||
/* An entry's impulse is one of the animation impulses or, past them, an
|
||||
action the AI starts directly: a melee attack (unit_melee_attack_begin)
|
||||
or a leap (unit_leap_begin). */
|
||||
_actor_action_melee = NUMBER_OF_UNIT_ANIMATION_IMPULSES,
|
||||
_actor_action_leap,
|
||||
NUMBER_OF_ACTOR_ACTIONS,
|
||||
|
||||
/* control flags a unit acts on in one tick: a unit that sets one goes to
|
||||
every client that tick, however far away, so the action isn't missed */
|
||||
ONE_SHOT_CONTROL_FLAGS = FLAG(_unit_control_jump_bit) | FLAG(_unit_control_action_bit) |
|
||||
FLAG(_unit_control_weapon_reload_bit) | FLAG(_unit_control_throw_grenade_bit) |
|
||||
FLAG(_unit_control_swap_weapons_bit),
|
||||
};
|
||||
|
||||
/* struct distributed_actor_state flags */
|
||||
enum
|
||||
{
|
||||
_distributed_actor_rides_bit = 0,
|
||||
_distributed_actor_shield_depleted_bit,
|
||||
_distributed_actor_shield_charging_bit,
|
||||
_distributed_actor_shield_over_charging_bit,
|
||||
_distributed_actor_camouflaged_bit,
|
||||
_distributed_actor_super_camouflaged_bit,
|
||||
/* the impulse turns the unit to impulse_alignment */
|
||||
_distributed_actor_impulse_aligned_bit,
|
||||
/* fleeing in a panic, toward run_blindly_angle (unit_start_running_blindly) */
|
||||
_distributed_actor_running_blindly_bit,
|
||||
};
|
||||
|
||||
/* facing, aiming and looking vectors are sent as yaw and pitch */
|
||||
enum
|
||||
{
|
||||
_angle_yaw,
|
||||
_angle_pitch,
|
||||
NUMBER_OF_ANGLES
|
||||
};
|
||||
|
||||
/* ---------- structures */
|
||||
|
||||
/* an AI unit's control and state at the end of the host's tick (like
|
||||
struct distributed_unit_state for players) */
|
||||
struct distributed_actor_state
|
||||
{
|
||||
long unit_index;
|
||||
long vehicle_index;
|
||||
short seat_index;
|
||||
byte flags;
|
||||
/* the impulse's sequence number; impulse is NO_IMPULSE when there is none */
|
||||
byte impulse_number;
|
||||
byte impulse;
|
||||
byte animation_state;
|
||||
byte aiming_speed;
|
||||
byte primary_trigger;
|
||||
word control_flags;
|
||||
signed char throttle[3];
|
||||
byte active_camouflage;
|
||||
/* the speech's sequence number; 0 when there is none */
|
||||
byte speech_number;
|
||||
/* the unit's team (NO_TEAM for none): the AI sets it after the unit is
|
||||
made, so a client's copy would keep the team it was made with */
|
||||
byte team;
|
||||
short facing[NUMBER_OF_ANGLES];
|
||||
short aiming[NUMBER_OF_ANGLES];
|
||||
short looking[NUMBER_OF_ANGLES];
|
||||
short impulse_alignment;
|
||||
short run_blindly_angle;
|
||||
word body_vitality;
|
||||
word shield_vitality;
|
||||
/* the sound the unit started saying (a sound tag) */
|
||||
long speech_sound;
|
||||
/* the custom animation it started (unit_start_user_animation), and its
|
||||
sequence number; the graph is NONE when there is none */
|
||||
long user_animation_graph;
|
||||
short user_animation;
|
||||
byte user_animation_number;
|
||||
byte user_animation_interpolate;
|
||||
real_point3d position;
|
||||
struct distributed_vector velocity;
|
||||
struct distributed_vector forward;
|
||||
struct distributed_vector up;
|
||||
};
|
||||
|
||||
/* the damage an AI unit is taking: its shield's flare and its body's wounds
|
||||
are drawn by it */
|
||||
struct distributed_actor_damage
|
||||
{
|
||||
long unit_index;
|
||||
word current_shield_damage;
|
||||
word recent_shield_damage;
|
||||
word current_body_damage;
|
||||
word recent_body_damage;
|
||||
};
|
||||
|
||||
struct distributed_actor_damage_message
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
struct distributed_actor_damage units[MAXIMUM_ENTRIES_PER_MESSAGE];
|
||||
};
|
||||
|
||||
struct distributed_actor_state_message
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
struct distributed_actor_state states[MAXIMUM_ENTRIES_PER_MESSAGE];
|
||||
};
|
||||
|
||||
/* host: what an actor gave its unit this tick */
|
||||
struct host_actor
|
||||
{
|
||||
long unit_index;
|
||||
/* got a control since the last send (noted during game_tick, sent after it) */
|
||||
boolean noted;
|
||||
/* its damage was sent last tick (and once more when it ends) */
|
||||
boolean damaged;
|
||||
struct unit_control_data control;
|
||||
/* the control flags of the last tick's entry */
|
||||
word sent_control_flags;
|
||||
/* the latest impulse, how many more entries carry it, and its number */
|
||||
short impulse;
|
||||
short impulse_sends;
|
||||
byte impulse_number;
|
||||
boolean impulse_aligned;
|
||||
real_vector2d impulse_alignment;
|
||||
/* the latest speech, how many more entries carry it, and its number */
|
||||
long speech_sound;
|
||||
short speech_sends;
|
||||
byte speech_number;
|
||||
/* the latest custom animation, the same way */
|
||||
long user_animation_graph;
|
||||
short user_animation;
|
||||
boolean user_animation_interpolate;
|
||||
short user_animation_sends;
|
||||
byte user_animation_number;
|
||||
};
|
||||
|
||||
/* client: the latest the host sent about an AI unit */
|
||||
struct client_actor
|
||||
{
|
||||
long unit_index;
|
||||
long received_time;
|
||||
struct unit_control_data control;
|
||||
/* the impulse to play, and the number of the last one played */
|
||||
short impulse;
|
||||
byte impulse_number;
|
||||
byte played_impulse_number;
|
||||
boolean impulse_aligned;
|
||||
real_vector2d impulse_alignment;
|
||||
/* the speech to play (NONE when there is none), and the number of the last played */
|
||||
long speech_sound;
|
||||
byte speech_number;
|
||||
byte played_speech_number;
|
||||
/* the custom animation to play (graph NONE when there is none), and the
|
||||
number of the last played */
|
||||
long user_animation_graph;
|
||||
short user_animation;
|
||||
boolean user_animation_interpolate;
|
||||
byte user_animation_number;
|
||||
byte played_user_animation_number;
|
||||
boolean driven;
|
||||
};
|
||||
|
||||
/* ---------- globals */
|
||||
|
||||
static struct host_actor host_actors[MAXIMUM_NETWORK_ACTORS];
|
||||
static short host_actor_count;
|
||||
/* host: the numbers of every unit's impulses, speech and animations, from
|
||||
one counter, so a unit that drops out of the table and comes back can't
|
||||
repeat the number a client last played */
|
||||
static byte host_event_number;
|
||||
|
||||
static struct client_actor client_actors[MAXIMUM_NETWORK_ACTORS];
|
||||
static short client_actor_count;
|
||||
|
||||
/* ---------- private code */
|
||||
|
||||
static void angles_pack(
|
||||
real_vector3d const *vector,
|
||||
short *angles)
|
||||
{
|
||||
angles[_angle_yaw] = distributed_angle_pack((real)atan2(vector->j, vector->i));
|
||||
angles[_angle_pitch] = distributed_angle_pack((real)asin(PIN(vector->k, -1.0f, 1.0f)));
|
||||
}
|
||||
|
||||
static void angles_unpack(
|
||||
short const *angles,
|
||||
real_vector3d *vector)
|
||||
{
|
||||
real yaw = distributed_angle_unpack(angles[_angle_yaw], FALSE);
|
||||
real pitch = distributed_angle_unpack(angles[_angle_pitch], TRUE);
|
||||
|
||||
vector->i = (real)(cos(pitch) * cos(yaw));
|
||||
vector->j = (real)(cos(pitch) * sin(yaw));
|
||||
vector->k = (real)sin(pitch);
|
||||
}
|
||||
|
||||
/* host: the next event's number (never 0, which a client's new entry has
|
||||
as played) */
|
||||
static byte next_event_number(
|
||||
void)
|
||||
{
|
||||
if (++host_event_number == 0)
|
||||
host_event_number = 1;
|
||||
return host_event_number;
|
||||
}
|
||||
|
||||
/* the unit's entry, adding one if needed; NULL if the table is full */
|
||||
static struct host_actor *host_actor_for(
|
||||
long unit_index)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < host_actor_count; index++)
|
||||
{
|
||||
if (host_actors[index].unit_index == unit_index)
|
||||
return &host_actors[index];
|
||||
}
|
||||
if (host_actor_count >= MAXIMUM_NETWORK_ACTORS)
|
||||
return NULL;
|
||||
csmemset(&host_actors[host_actor_count], 0, sizeof(host_actors[0]));
|
||||
host_actors[host_actor_count].unit_index = unit_index;
|
||||
host_actors[host_actor_count].impulse = NONE;
|
||||
host_actors[host_actor_count].speech_sound = NONE;
|
||||
host_actors[host_actor_count].user_animation_graph = NONE;
|
||||
return &host_actors[host_actor_count++];
|
||||
}
|
||||
|
||||
static struct client_actor *client_actor_for(
|
||||
long unit_index)
|
||||
{
|
||||
short index;
|
||||
|
||||
for (index = 0; index < client_actor_count; index++)
|
||||
{
|
||||
if (client_actors[index].unit_index == unit_index)
|
||||
return &client_actors[index];
|
||||
}
|
||||
if (client_actor_count >= MAXIMUM_NETWORK_ACTORS)
|
||||
return NULL;
|
||||
csmemset(&client_actors[client_actor_count], 0, sizeof(client_actors[0]));
|
||||
client_actors[client_actor_count].unit_index = unit_index;
|
||||
client_actors[client_actor_count].impulse = NONE;
|
||||
client_actors[client_actor_count].speech_sound = NONE;
|
||||
client_actors[client_actor_count].user_animation_graph = NONE;
|
||||
return &client_actors[client_actor_count++];
|
||||
}
|
||||
|
||||
/* whether the unit exists, is alive, and isn't a player's */
|
||||
static boolean actor_unit_valid(
|
||||
long unit_index)
|
||||
{
|
||||
struct unit_datum *unit;
|
||||
|
||||
if (!object_try_and_get_and_verify_type(unit_index, _object_mask_unit))
|
||||
return FALSE;
|
||||
unit = unit_get(unit_index);
|
||||
return !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) && unit->unit.player_index == NONE;
|
||||
}
|
||||
|
||||
/* host: builds the entry to send for an AI unit */
|
||||
static void actor_state_from_unit(
|
||||
struct host_actor const *actor,
|
||||
struct distributed_actor_state *state)
|
||||
{
|
||||
struct unit_datum *unit = unit_get(actor->unit_index);
|
||||
struct damage_network_state damage;
|
||||
real camouflage = PIN(unit->unit.active_camouflage, 0.0f, 1.0f);
|
||||
|
||||
csmemset(state, 0, sizeof(*state));
|
||||
state->unit_index = actor->unit_index;
|
||||
state->vehicle_index = NONE;
|
||||
state->seat_index = NONE;
|
||||
if (unit->object.parent_object_index != NONE && unit->unit.parent_seat_index != NONE)
|
||||
{
|
||||
SET_FLAG(state->flags, _distributed_actor_rides_bit, TRUE);
|
||||
state->vehicle_index = unit->object.parent_object_index;
|
||||
state->seat_index = unit->unit.parent_seat_index;
|
||||
}
|
||||
state->animation_state = (byte)actor->control.animation_state;
|
||||
state->aiming_speed = (byte)actor->control.aiming_speed;
|
||||
state->primary_trigger = (byte)(long)floor(PIN(actor->control.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
state->control_flags = actor->control.control_flags;
|
||||
/* for a burst the AI holds the trigger for (unit_persistent_control),
|
||||
send the flags and trigger the unit's update produced this tick */
|
||||
if (unit->unit.persistent_control_timer > 0)
|
||||
{
|
||||
state->control_flags = (word)(unit->unit.control_flags & (FLAG(NUMBER_OF_UNIT_CONTROL_FLAGS) - 1));
|
||||
state->primary_trigger = (byte)(long)floor(PIN(unit->unit.primary_trigger, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
}
|
||||
state->throttle[0] = (signed char)(long)floor(PIN(actor->control.throttle.i, -1.0f, 1.0f) * 127.0f + 0.5f);
|
||||
state->throttle[1] = (signed char)(long)floor(PIN(actor->control.throttle.j, -1.0f, 1.0f) * 127.0f + 0.5f);
|
||||
state->throttle[2] = (signed char)(long)floor(PIN(actor->control.throttle.k, -1.0f, 1.0f) * 127.0f + 0.5f);
|
||||
angles_pack(&actor->control.facing_vector, state->facing);
|
||||
angles_pack(&actor->control.aiming_vector, state->aiming);
|
||||
angles_pack(&actor->control.looking_vector, state->looking);
|
||||
state->impulse = NO_IMPULSE;
|
||||
if (actor->impulse != NONE && actor->impulse_sends > 0)
|
||||
{
|
||||
state->impulse = (byte)actor->impulse;
|
||||
state->impulse_number = actor->impulse_number;
|
||||
SET_FLAG(state->flags, _distributed_actor_impulse_aligned_bit, actor->impulse_aligned);
|
||||
if (actor->impulse_aligned)
|
||||
{
|
||||
state->impulse_alignment = distributed_angle_pack(
|
||||
(real)atan2(actor->impulse_alignment.j, actor->impulse_alignment.i));
|
||||
}
|
||||
}
|
||||
state->speech_sound = NONE;
|
||||
if (actor->speech_sound != NONE && actor->speech_sends > 0)
|
||||
{
|
||||
state->speech_sound = actor->speech_sound;
|
||||
state->speech_number = actor->speech_number;
|
||||
}
|
||||
state->user_animation_graph = NONE;
|
||||
if (actor->user_animation_graph != NONE && actor->user_animation_sends > 0)
|
||||
{
|
||||
state->user_animation_graph = actor->user_animation_graph;
|
||||
state->user_animation = actor->user_animation;
|
||||
state->user_animation_interpolate = (byte)actor->user_animation_interpolate;
|
||||
state->user_animation_number = actor->user_animation_number;
|
||||
}
|
||||
if (TEST_FLAG(unit->unit.flags, _unit_running_blindly_bit))
|
||||
{
|
||||
SET_FLAG(state->flags, _distributed_actor_running_blindly_bit, TRUE);
|
||||
state->run_blindly_angle = distributed_angle_pack(unit->unit.run_blindly_angle);
|
||||
}
|
||||
damage_get_network_state(actor->unit_index, &damage);
|
||||
SET_FLAG(state->flags, _distributed_actor_shield_depleted_bit, damage.shield_depleted);
|
||||
SET_FLAG(state->flags, _distributed_actor_shield_charging_bit, damage.shield_charging);
|
||||
SET_FLAG(state->flags, _distributed_actor_shield_over_charging_bit, damage.shield_over_charging);
|
||||
state->body_vitality = distributed_vitality_pack(damage.body_vitality);
|
||||
state->shield_vitality = distributed_vitality_pack(damage.shield_vitality);
|
||||
SET_FLAG(state->flags, _distributed_actor_camouflaged_bit,
|
||||
TEST_FLAG(unit->unit.flags, _unit_active_camouflaged_bit));
|
||||
SET_FLAG(state->flags, _distributed_actor_super_camouflaged_bit,
|
||||
TEST_FLAG(unit->unit.flags, _unit_super_camouflaged_bit));
|
||||
state->active_camouflage = (byte)(long)floor(camouflage * 255.0f + 0.5f);
|
||||
state->team = unit->object.owner_team_index >= 0 && unit->object.owner_team_index < NO_TEAM ?
|
||||
(byte)unit->object.owner_team_index : NO_TEAM;
|
||||
state->position = unit->object.position;
|
||||
distributed_vector_pack(&unit->object.translational_velocity, DISTRIBUTED_VELOCITY_SCALE, &state->velocity);
|
||||
distributed_vector_pack(&unit->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward);
|
||||
distributed_vector_pack(&unit->object.up, DISTRIBUTED_UNIT_SCALE, &state->up);
|
||||
}
|
||||
|
||||
/* Client: stores the entry's control and applies its state to the unit.
|
||||
Returns FALSE for an entry with invalid values. */
|
||||
static boolean actor_state_apply(
|
||||
struct distributed_actor_state const *state,
|
||||
long now)
|
||||
{
|
||||
struct client_actor *actor;
|
||||
struct unit_datum *unit;
|
||||
struct damage_network_state damage;
|
||||
real_vector3d velocity;
|
||||
real_vector3d forward;
|
||||
real_vector3d up;
|
||||
|
||||
if (!distributed_object_index_valid(state->unit_index) || !network_objects_client_has(state->unit_index))
|
||||
return FALSE;
|
||||
/* (a body the host's unit got back up from: a Flood combat form that
|
||||
feigned death, which it does only once its body is at rest. Left dead,
|
||||
the ragdoll was dragged about by the host's positions. The host sends
|
||||
only living units.) */
|
||||
if (object_try_and_get_and_verify_type(state->unit_index, _object_mask_unit) &&
|
||||
TEST_FLAG(unit_get(state->unit_index)->object.damage_flags, _object_dead_bit) &&
|
||||
TEST_FLAG(unit_get(state->unit_index)->object.flags, _object_at_rest_bit) &&
|
||||
unit_get(state->unit_index)->unit.player_index == NONE && state->body_vitality > 0)
|
||||
{
|
||||
unit_port_resurrect(state->unit_index);
|
||||
}
|
||||
if (!actor_unit_valid(state->unit_index) ||
|
||||
state->animation_state >= NUMBER_OF_UNIT_ANIMATION_STATES ||
|
||||
state->aiming_speed >= NUMBER_OF_UNIT_AIMING_SPEEDS ||
|
||||
!VALID_FLAGS(state->control_flags, NUMBER_OF_UNIT_CONTROL_FLAGS) ||
|
||||
(state->impulse != NO_IMPULSE && state->impulse >= NUMBER_OF_ACTOR_ACTIONS) ||
|
||||
(state->speech_sound != NONE && !distributed_tag_of_group(state->speech_sound, SOUND_DEFINITION_TAG)) ||
|
||||
(state->user_animation_graph != NONE &&
|
||||
!distributed_graph_animation(state->user_animation_graph, state->user_animation)) ||
|
||||
!distributed_point_valid(&state->position, UNIT_WORLD_BOUND))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
distributed_vector_unpack(&state->velocity, DISTRIBUTED_VELOCITY_SCALE, &velocity);
|
||||
distributed_vector_unpack(&state->forward, DISTRIBUTED_UNIT_SCALE, &forward);
|
||||
distributed_vector_unpack(&state->up, DISTRIBUTED_UNIT_SCALE, &up);
|
||||
if (!distributed_axes_make_valid(&forward, &up))
|
||||
return FALSE;
|
||||
actor = client_actor_for(state->unit_index);
|
||||
if (!actor)
|
||||
return TRUE;
|
||||
|
||||
/* the control, applied every tick until the next entry */
|
||||
actor->received_time = now;
|
||||
actor->control.animation_state = (char)state->animation_state;
|
||||
actor->control.aiming_speed = (char)state->aiming_speed;
|
||||
actor->control.control_flags = state->control_flags;
|
||||
actor->control.weapon_index = NONE;
|
||||
actor->control.grenade_index = NONE;
|
||||
actor->control.zoom_level = NONE;
|
||||
actor->control.throttle.i = (real)state->throttle[0] / 127.0f;
|
||||
actor->control.throttle.j = (real)state->throttle[1] / 127.0f;
|
||||
actor->control.throttle.k = (real)state->throttle[2] / 127.0f;
|
||||
actor->control.primary_trigger = (real)state->primary_trigger / 255.0f;
|
||||
angles_unpack(state->facing, &actor->control.facing_vector);
|
||||
angles_unpack(state->aiming, &actor->control.aiming_vector);
|
||||
angles_unpack(state->looking, &actor->control.looking_vector);
|
||||
if (state->impulse != NO_IMPULSE && state->impulse_number != actor->played_impulse_number)
|
||||
{
|
||||
real yaw = distributed_angle_unpack(state->impulse_alignment, FALSE);
|
||||
|
||||
actor->impulse = state->impulse;
|
||||
actor->impulse_number = state->impulse_number;
|
||||
actor->impulse_aligned = TEST_FLAG(state->flags, _distributed_actor_impulse_aligned_bit);
|
||||
actor->impulse_alignment.i = (real)cos(yaw);
|
||||
actor->impulse_alignment.j = (real)sin(yaw);
|
||||
}
|
||||
|
||||
if (state->speech_sound != NONE && state->speech_number != actor->played_speech_number)
|
||||
{
|
||||
actor->speech_sound = state->speech_sound;
|
||||
actor->speech_number = state->speech_number;
|
||||
}
|
||||
if (state->user_animation_graph != NONE && state->user_animation_number != actor->played_user_animation_number)
|
||||
{
|
||||
actor->user_animation_graph = state->user_animation_graph;
|
||||
actor->user_animation = state->user_animation;
|
||||
actor->user_animation_interpolate = state->user_animation_interpolate != 0;
|
||||
actor->user_animation_number = state->user_animation_number;
|
||||
}
|
||||
|
||||
/* the state: seat, shields and health, camouflage, running blindly, and
|
||||
position (a unit in a vehicle goes where the vehicle goes) */
|
||||
unit = unit_get(state->unit_index);
|
||||
if (TEST_FLAG(state->flags, _distributed_actor_rides_bit))
|
||||
{
|
||||
if (unit->object.parent_object_index != state->vehicle_index ||
|
||||
unit->unit.parent_seat_index != state->seat_index)
|
||||
{
|
||||
network_objects_set_seat(state->unit_index, state->vehicle_index, state->seat_index);
|
||||
}
|
||||
}
|
||||
else if (unit->object.parent_object_index != NONE)
|
||||
{
|
||||
network_objects_set_seat(state->unit_index, NONE, NONE);
|
||||
}
|
||||
damage_get_network_state(state->unit_index, &damage);
|
||||
damage.shield_depleted = TEST_FLAG(state->flags, _distributed_actor_shield_depleted_bit);
|
||||
damage.shield_charging = TEST_FLAG(state->flags, _distributed_actor_shield_charging_bit);
|
||||
damage.shield_over_charging = TEST_FLAG(state->flags, _distributed_actor_shield_over_charging_bit);
|
||||
damage.body_vitality = distributed_vitality_unpack(state->body_vitality);
|
||||
damage.shield_vitality = distributed_vitality_unpack(state->shield_vitality);
|
||||
damage_set_network_state(state->unit_index, &damage);
|
||||
SET_FLAG(unit->unit.flags, _unit_active_camouflaged_bit,
|
||||
TEST_FLAG(state->flags, _distributed_actor_camouflaged_bit));
|
||||
SET_FLAG(unit->unit.flags, _unit_super_camouflaged_bit,
|
||||
TEST_FLAG(state->flags, _distributed_actor_super_camouflaged_bit));
|
||||
unit->unit.active_camouflage = (real)state->active_camouflage / 255.0f;
|
||||
if (state->team != NO_TEAM)
|
||||
unit->object.owner_team_index = state->team;
|
||||
SET_FLAG(unit->unit.flags, _unit_running_blindly_bit,
|
||||
TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit));
|
||||
if (TEST_FLAG(state->flags, _distributed_actor_running_blindly_bit))
|
||||
unit->unit.run_blindly_angle = distributed_angle_unpack(state->run_blindly_angle, FALSE);
|
||||
/* vehicles are placed only by the host's object states (network_objects.c) */
|
||||
if (unit->object.parent_object_index == NONE && unit->object.type != _object_type_vehicle)
|
||||
{
|
||||
real dx = state->position.x - unit->object.position.x;
|
||||
real dy = state->position.y - unit->object.position.y;
|
||||
real dz = state->position.z - unit->object.position.z;
|
||||
|
||||
if (dx * dx + dy * dy + dz * dz > REMOTE_CORRECTION_TOLERANCE * REMOTE_CORRECTION_TOLERANCE &&
|
||||
network_objects_reconcile(state->unit_index, &state->position, &forward, &up, &velocity, NULL,
|
||||
REMOTE_BLEND_DISTANCE))
|
||||
{
|
||||
distributed_count_correction();
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Client: plays a sound at the unit's head, as unit_dialogue_update starts
|
||||
a unit's speech. */
|
||||
static void speech_play(
|
||||
long unit_index,
|
||||
long sound_definition_index)
|
||||
{
|
||||
struct object_marker marker;
|
||||
real_point3d position = *global_origin3d;
|
||||
real_vector3d forward = *global_forward3d;
|
||||
short node_index = 0;
|
||||
|
||||
if (object_get_marker_by_name(unit_index, "head", &marker, 1))
|
||||
{
|
||||
node_index = marker.node_index;
|
||||
position = marker.node_matrix.position;
|
||||
forward = marker.node_matrix.forward;
|
||||
}
|
||||
object_impulse_sound_new(unit_index, sound_definition_index, node_index, &position, &forward, 1.0f);
|
||||
}
|
||||
|
||||
/* ---------- public code */
|
||||
|
||||
void network_actors_new_game(
|
||||
void)
|
||||
{
|
||||
host_actor_count = 0;
|
||||
client_actor_count = 0;
|
||||
}
|
||||
|
||||
/* host: unit_control calls this when an actor drives its unit */
|
||||
void network_actors_note_control(
|
||||
long unit_index,
|
||||
struct unit_control_data const *control_data)
|
||||
{
|
||||
struct host_actor *actor;
|
||||
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
actor = host_actor_for(unit_index);
|
||||
if (!actor)
|
||||
return;
|
||||
actor->noted = TRUE;
|
||||
actor->control = *control_data;
|
||||
}
|
||||
|
||||
/* host: an impulse or action for an AI unit, sent in the next few entries */
|
||||
static void note_action(
|
||||
long unit_index,
|
||||
short action,
|
||||
real_vector2d const *alignment_vector)
|
||||
{
|
||||
struct host_actor *actor;
|
||||
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
actor = host_actor_for(unit_index);
|
||||
if (!actor)
|
||||
return;
|
||||
actor->impulse = action;
|
||||
actor->impulse_sends = IMPULSE_REPEAT_TICKS;
|
||||
actor->impulse_number = next_event_number();
|
||||
actor->impulse_aligned = alignment_vector != NULL;
|
||||
if (alignment_vector)
|
||||
actor->impulse_alignment = *alignment_vector;
|
||||
}
|
||||
|
||||
/* host: unit_start_animation_impulse calls this for an AI unit */
|
||||
void network_actors_note_impulse(
|
||||
long unit_index,
|
||||
short animation_impulse,
|
||||
real_vector2d const *alignment_vector)
|
||||
{
|
||||
note_action(unit_index, animation_impulse, alignment_vector);
|
||||
}
|
||||
|
||||
/* host: unit_melee_attack_begin calls this when an AI unit starts a melee attack */
|
||||
void network_actors_note_melee(
|
||||
long unit_index,
|
||||
real_vector2d const *alignment_vector)
|
||||
{
|
||||
note_action(unit_index, _actor_action_melee, alignment_vector);
|
||||
}
|
||||
|
||||
/* host: unit_leap_begin calls this when an AI unit leaps */
|
||||
void network_actors_note_leap(
|
||||
long unit_index,
|
||||
real_vector2d const *alignment_vector)
|
||||
{
|
||||
note_action(unit_index, _actor_action_leap, alignment_vector);
|
||||
}
|
||||
|
||||
/* host: unit_dialogue_update calls this when an AI unit starts saying
|
||||
something (not a pain or death sound, which clients play themselves) */
|
||||
void network_actors_note_speech(
|
||||
long unit_index,
|
||||
long sound_definition_index)
|
||||
{
|
||||
struct host_actor *actor;
|
||||
|
||||
if (game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
actor = host_actor_for(unit_index);
|
||||
if (!actor)
|
||||
return;
|
||||
actor->speech_sound = sound_definition_index;
|
||||
actor->speech_sends = IMPULSE_REPEAT_TICKS;
|
||||
actor->speech_number = next_event_number();
|
||||
}
|
||||
|
||||
/* host: unit_start_user_animation calls this when an AI unit starts a
|
||||
custom animation (co-op's events carry them in co-op) */
|
||||
void network_actors_note_user_animation(
|
||||
long unit_index,
|
||||
long animation_graph_index,
|
||||
short animation_index,
|
||||
boolean interpolate)
|
||||
{
|
||||
struct host_actor *actor;
|
||||
|
||||
if (game_connection() != _game_connection_network_server || network_coop_active())
|
||||
return;
|
||||
actor = host_actor_for(unit_index);
|
||||
if (!actor)
|
||||
return;
|
||||
actor->user_animation_graph = animation_graph_index;
|
||||
actor->user_animation = animation_index;
|
||||
actor->user_animation_interpolate = interpolate;
|
||||
actor->user_animation_sends = IMPULSE_REPEAT_TICKS;
|
||||
actor->user_animation_number = next_event_number();
|
||||
}
|
||||
|
||||
/* host: the damage of the AI units taking some, to every client (and once
|
||||
more when it ends, so a flare goes) */
|
||||
static void host_send_damage(
|
||||
void)
|
||||
{
|
||||
struct distributed_actor_damage_message message;
|
||||
short count = 0;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < host_actor_count; index++)
|
||||
{
|
||||
struct host_actor *actor = &host_actors[index];
|
||||
struct damage_network_state damage;
|
||||
boolean damaged;
|
||||
|
||||
if (!actor_unit_valid(actor->unit_index))
|
||||
continue;
|
||||
damage_get_network_state(actor->unit_index, &damage);
|
||||
damaged = damage.current_shield_damage > 0.0f || damage.recent_shield_damage > 0.0f ||
|
||||
damage.current_body_damage > 0.0f || damage.recent_body_damage > 0.0f;
|
||||
if (!damaged && !actor->damaged)
|
||||
continue;
|
||||
actor->damaged = damaged;
|
||||
message.units[count].unit_index = actor->unit_index;
|
||||
message.units[count].current_shield_damage = distributed_vitality_pack(damage.current_shield_damage);
|
||||
message.units[count].recent_shield_damage = distributed_vitality_pack(damage.recent_shield_damage);
|
||||
message.units[count].current_body_damage = distributed_vitality_pack(damage.current_body_damage);
|
||||
message.units[count].recent_body_damage = distributed_vitality_pack(damage.recent_body_damage);
|
||||
if (++count == MAXIMUM_ENTRIES_PER_MESSAGE)
|
||||
{
|
||||
distributed_send(&message, _distributed_message_actor_damage, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
if (count)
|
||||
{
|
||||
distributed_send(&message, _distributed_message_actor_damage, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(message.units[0])), _distributed_to_clients);
|
||||
}
|
||||
}
|
||||
|
||||
/* Host, after each tick: sends each client the AI units driven this tick.
|
||||
Units near the client's players go every tick, others less often. A unit
|
||||
with a fresh impulse, action, speech or animation goes to everyone in each
|
||||
of the next IMPULSE_REPEAT_TICKS ticks, and one starting a one-tick action
|
||||
goes that tick. A unit no actor drove this tick is dropped from the table,
|
||||
unless it is fleeing: a unit running blindly moves by itself, without its
|
||||
actor's control, and dropped it stood running in place on the clients. */
|
||||
void network_actors_host_tick(
|
||||
void)
|
||||
{
|
||||
static struct distributed_actor_state states[MAXIMUM_NETWORK_ACTORS];
|
||||
/* sent to every client this tick, whatever its period */
|
||||
static boolean urgent[MAXIMUM_NETWORK_ACTORS];
|
||||
struct distributed_actor_state_message message;
|
||||
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
|
||||
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
|
||||
long now = game_time_get();
|
||||
short state_count = 0;
|
||||
short index;
|
||||
short machine_number;
|
||||
|
||||
for (index = 0; index < host_actor_count; index++)
|
||||
{
|
||||
struct host_actor *actor = &host_actors[index];
|
||||
|
||||
if (!actor_unit_valid(actor->unit_index) ||
|
||||
(!actor->noted && !TEST_FLAG(unit_get(actor->unit_index)->unit.flags, _unit_running_blindly_bit)))
|
||||
{
|
||||
host_actors[index--] = host_actors[--host_actor_count];
|
||||
continue;
|
||||
}
|
||||
actor_state_from_unit(actor, &states[state_count]);
|
||||
urgent[state_count] = states[state_count].impulse != NO_IMPULSE || states[state_count].speech_sound != NONE ||
|
||||
states[state_count].user_animation_graph != NONE ||
|
||||
(states[state_count].control_flags & ~actor->sent_control_flags & ONE_SHOT_CONTROL_FLAGS) != 0;
|
||||
actor->sent_control_flags = states[state_count].control_flags;
|
||||
state_count++;
|
||||
actor->noted = FALSE;
|
||||
if (actor->impulse_sends > 0)
|
||||
actor->impulse_sends--;
|
||||
if (actor->speech_sends > 0)
|
||||
actor->speech_sends--;
|
||||
if (actor->user_animation_sends > 0)
|
||||
actor->user_animation_sends--;
|
||||
}
|
||||
host_send_damage();
|
||||
for (machine_number = 0; machine_number < machine_count; machine_number++)
|
||||
{
|
||||
long machine_index = machine_indices[machine_number];
|
||||
short count = 0;
|
||||
|
||||
for (index = 0; index < state_count; index++)
|
||||
{
|
||||
struct distributed_actor_state const *state = &states[index];
|
||||
short period = network_objects_send_period(machine_index, &state->position);
|
||||
|
||||
if (!urgent[index] && (now + DATUM_INDEX_TO_ABSOLUTE_INDEX(state->unit_index)) % period != 0)
|
||||
continue;
|
||||
message.states[count++] = *state;
|
||||
if (count == MAXIMUM_ENTRIES_PER_MESSAGE || count == DATAGRAM_ENTRIES(struct distributed_actor_state))
|
||||
{
|
||||
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
if (count)
|
||||
{
|
||||
distributed_send_to_machine(machine_index, &message, _distributed_message_actor_states, count,
|
||||
(word)(sizeof(message.header) + count * sizeof(struct distributed_actor_state)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
word network_actors_damage_entry_size(
|
||||
void)
|
||||
{
|
||||
return sizeof(struct distributed_actor_damage);
|
||||
}
|
||||
|
||||
/* client: the damage the host's AI units are taking */
|
||||
void network_actors_handle_damage(
|
||||
void const *entries,
|
||||
short count)
|
||||
{
|
||||
struct distributed_actor_damage const *units = entries;
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
{
|
||||
struct damage_network_state damage;
|
||||
|
||||
if (!distributed_object_index_valid(units[index].unit_index) ||
|
||||
!network_objects_client_has(units[index].unit_index) || !actor_unit_valid(units[index].unit_index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
damage_get_network_state(units[index].unit_index, &damage);
|
||||
damage.current_shield_damage = distributed_vitality_unpack(units[index].current_shield_damage);
|
||||
damage.recent_shield_damage = distributed_vitality_unpack(units[index].recent_shield_damage);
|
||||
damage.current_body_damage = distributed_vitality_unpack(units[index].current_body_damage);
|
||||
damage.recent_body_damage = distributed_vitality_unpack(units[index].recent_body_damage);
|
||||
damage_set_network_state(units[index].unit_index, &damage);
|
||||
}
|
||||
}
|
||||
|
||||
word network_actors_entry_size(
|
||||
void)
|
||||
{
|
||||
return sizeof(struct distributed_actor_state);
|
||||
}
|
||||
|
||||
/* client: entries from the host */
|
||||
void network_actors_handle_states(
|
||||
void const *entries,
|
||||
short count)
|
||||
{
|
||||
struct distributed_actor_state const *states = entries;
|
||||
long now = game_time_get();
|
||||
short index;
|
||||
|
||||
for (index = 0; index < count; index++)
|
||||
actor_state_apply(&states[index], now);
|
||||
}
|
||||
|
||||
/* Client: called at the point in the tick where the host runs the AI.
|
||||
Drives each AI unit with the last control the host sent. */
|
||||
void network_actors_drive(
|
||||
void)
|
||||
{
|
||||
long now = game_time_get();
|
||||
short index;
|
||||
|
||||
for (index = 0; index < client_actor_count; index++)
|
||||
{
|
||||
struct client_actor *actor = &client_actors[index];
|
||||
|
||||
if (!actor_unit_valid(actor->unit_index) || now - actor->received_time > CONTROL_HELD_TICKS)
|
||||
{
|
||||
if (actor->driven && object_try_and_get_and_verify_type(actor->unit_index, _object_mask_unit))
|
||||
unit_set_actively_controlled(actor->unit_index, FALSE);
|
||||
client_actors[index--] = client_actors[--client_actor_count];
|
||||
continue;
|
||||
}
|
||||
/* vehicles follow the host's transform instead (network_objects.c) */
|
||||
if (object_get(actor->unit_index)->object.type == _object_type_vehicle)
|
||||
continue;
|
||||
if (!actor->driven)
|
||||
{
|
||||
unit_set_actively_controlled(actor->unit_index, TRUE);
|
||||
actor->driven = TRUE;
|
||||
}
|
||||
unit_control(actor->unit_index, &actor->control);
|
||||
if (actor->impulse != NONE && actor->impulse_number != actor->played_impulse_number)
|
||||
{
|
||||
real_vector2d *alignment = actor->impulse_aligned ? &actor->impulse_alignment : NULL;
|
||||
|
||||
if (actor->impulse == _actor_action_melee)
|
||||
unit_melee_attack_begin(actor->unit_index, FALSE, alignment);
|
||||
else if (actor->impulse == _actor_action_leap)
|
||||
unit_leap_begin(actor->unit_index, alignment);
|
||||
else
|
||||
unit_start_animation_impulse(actor->unit_index, actor->impulse, alignment);
|
||||
actor->played_impulse_number = actor->impulse_number;
|
||||
actor->impulse = NONE;
|
||||
}
|
||||
if (actor->speech_sound != NONE)
|
||||
{
|
||||
speech_play(actor->unit_index, actor->speech_sound);
|
||||
actor->played_speech_number = actor->speech_number;
|
||||
actor->speech_sound = NONE;
|
||||
}
|
||||
if (actor->user_animation_graph != NONE)
|
||||
{
|
||||
unit_port_play_user_animation(actor->unit_index, actor->user_animation_graph, actor->user_animation,
|
||||
actor->user_animation_interpolate, 0);
|
||||
actor->played_user_animation_number = actor->user_animation_number;
|
||||
actor->user_animation_graph = NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
NETWORK_COOP.H
|
||||
|
||||
The calls Halo's own code (source/) makes into network_coop.c. On a co-op
|
||||
host the note_ functions send what the scripts just did to the clients; on
|
||||
any other machine they do nothing, so the clients can call the same engine
|
||||
functions to apply what they receive.
|
||||
*/
|
||||
|
||||
#ifndef __NETWORK_COOP_H
|
||||
#define __NETWORK_COOP_H
|
||||
|
||||
/* ---------- constants */
|
||||
|
||||
/* network_coop_note_hud */
|
||||
enum
|
||||
{
|
||||
_coop_hud_help_text,
|
||||
_coop_hud_objective,
|
||||
_coop_hud_help_flash,
|
||||
_coop_hud_messages_clear,
|
||||
_coop_hud_checkpoint,
|
||||
};
|
||||
|
||||
/* network_coop_note_player_effect */
|
||||
enum
|
||||
{
|
||||
_coop_player_effect_translation,
|
||||
_coop_player_effect_rotation,
|
||||
_coop_player_effect_start,
|
||||
_coop_player_effect_stop,
|
||||
};
|
||||
|
||||
/* network_coop_note_nav_point */
|
||||
enum
|
||||
{
|
||||
_coop_nav_point_team_flag,
|
||||
_coop_nav_point_team_object,
|
||||
_coop_nav_point_unit_flag,
|
||||
_coop_nav_point_unit_object,
|
||||
};
|
||||
|
||||
/* ---------- prototypes/NETWORK_COOP.C */
|
||||
|
||||
/* 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 */
|
||||
void network_coop_note_device_snap(short group_index);
|
||||
/* unit_scripting_commands.c: a script set a unit's maximum or current
|
||||
vitality; TRUE if it was a co-op player's, now set on every player's */
|
||||
boolean network_coop_set_players_vitality(long unit_index, boolean maximum, real body, real shield);
|
||||
/* host: whether the player's machine has the host's structure BSP loaded
|
||||
(always outside co-op, and for the host's own players). Until a client has,
|
||||
the host takes none of its players' movement, which falls there with no
|
||||
floor, and lets them trigger no BSP switch (players.c). */
|
||||
boolean network_coop_player_has_structure_bsp(long player_index);
|
||||
/* network_distributed.c: the structure BSP a client's input says it has */
|
||||
void network_coop_note_player_structure_bsp(short player_index, short structure_bsp_index);
|
||||
/* units.c: a vehicle left the vehicle carrying it (a Pelican's Warthog drop) */
|
||||
void network_coop_vehicle_dropped(long vehicle_index, long carrier_index);
|
||||
|
||||
/* cinematics.c: a chapter title */
|
||||
void network_coop_note_title(short title_index, real delay);
|
||||
/* hud_messaging.c, hud.c: value is a message index, or TRUE/FALSE */
|
||||
void network_coop_note_hud(short kind, short value);
|
||||
/* player_effects.c: the script screen shake (a, b, c as the function takes them) */
|
||||
void network_coop_note_player_effect(short kind, real a, real b, real c);
|
||||
/* hud_nav_points.c: activate (or, with nav_index NONE, deactivate) a nav point.
|
||||
target is the team or the unit; marker is the cutscene flag or the object. */
|
||||
void network_coop_note_nav_point(short kind, short nav_index, long target, long marker, real vertical_offset);
|
||||
|
||||
/* units.c: a unit started a custom animation (animation_index NONE: stopped) */
|
||||
void network_coop_note_unit_animation(long unit_index, long animation_graph_index, short animation_index,
|
||||
boolean interpolate);
|
||||
/* units.c: a unit (a dropship) opened or closed (unit_open, unit_close) */
|
||||
void network_coop_note_unit_open(long unit_index, boolean open);
|
||||
/* units.c: unit_custom_animation_at_frame moved it to a frame */
|
||||
void network_coop_note_unit_animation_frame(long unit_index, short frame_index);
|
||||
/* scenery.c: a scenery animation started */
|
||||
/* the scripts' effects (hs_library_external.c): at a cutscene flag, or on an
|
||||
object's marker */
|
||||
void network_coop_note_effect(long effect_definition_index, short cutscene_flag_index);
|
||||
/* the scripts' objects_attach and objects_detach (objects.c) */
|
||||
void network_coop_note_attach(long parent_index, char const *parent_marker_name, long child_index,
|
||||
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);
|
||||
|
||||
/* player_control.c: whether the skip key should do anything, and the press.
|
||||
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);
|
||||
/* the vote count to show, if a skippable cinematic is playing */
|
||||
boolean network_coop_skip_vote_status(short *votes, short *voters, boolean *voted);
|
||||
|
||||
#endif
|
||||
@@ -16,8 +16,9 @@ being hit looks and feels like on the clients).
|
||||
- A client deals no damage itself. What its own players' shots, grenades,
|
||||
melee and vehicles hit it reports to the host, which deals it once it has
|
||||
checked it: the report is of that machine's player, the damage one that
|
||||
player's weapons (now or lately), grenades or the vehicle they drove
|
||||
deal, of the shape the game gives it (at a point, a melee blow from the
|
||||
player's weapons (now or lately), a grenade the host saw them throw
|
||||
(each throw's explosion once) or the vehicle they drove deal, of the
|
||||
shape the game gives it (at a point, a melee blow from the
|
||||
striker, a collision from the vehicle), the target about where the host
|
||||
had it when the shooter saw it (the host keeps a second of where
|
||||
players' units and vehicles were, and looks back as far as the report
|
||||
@@ -33,9 +34,14 @@ 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"
|
||||
#include "cseries/errors.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_globals.h"
|
||||
#include "game/players.h"
|
||||
@@ -57,6 +63,8 @@ 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 */
|
||||
@@ -81,6 +89,13 @@ enum
|
||||
died or dropped the launcher) */
|
||||
MAXIMUM_RECENT_WEAPONS = 8,
|
||||
RECENT_WEAPON_TICKS = 10 * TICKS_PER_SECOND,
|
||||
/* the grenades a player has thrown lately, as the host's own game threw
|
||||
them, whose damage the host takes from them: each goes off once, within
|
||||
RECENT_WEAPON_TICKS of its throw. Each player has their own (all of
|
||||
MAXIMUM_TRACKED_PLAYERS); one that hits nothing is kept the whole time,
|
||||
so this is more than a player throws in it, infinite grenades and all
|
||||
(a throw takes most of a second); past it the oldest gives way */
|
||||
MAXIMUM_GRENADE_THROWS = 32,
|
||||
/* a player's hits: a bucket of this many seconds of fire, filling at a
|
||||
second a second, a hit taking what one of its weapon's projectiles
|
||||
takes to fire (at twice the weapon's fastest rate: a shotgun's
|
||||
@@ -133,8 +148,14 @@ 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
|
||||
@@ -341,8 +362,14 @@ static struct
|
||||
{
|
||||
long definition_indices[MAXIMUM_RECENT_WEAPONS];
|
||||
long times[MAXIMUM_RECENT_WEAPONS];
|
||||
long grenade_times[NUMBER_OF_UNIT_GRENADE_TYPES];
|
||||
/* (the grenades thrown, not yet gone off: NONE for none) */
|
||||
long throw_times[MAXIMUM_GRENADE_THROWS];
|
||||
short throw_types[MAXIMUM_GRENADE_THROWS];
|
||||
long driven_time;
|
||||
/* (the blow of the player's unit with no weapon: units.c's
|
||||
unit_unarmed_melee_damage) */
|
||||
long unarmed_melee_damage_index;
|
||||
long unarmed_time;
|
||||
real hit_seconds;
|
||||
long hit_seconds_time;
|
||||
} damage_players[MAXIMUM_TRACKED_PLAYERS];
|
||||
@@ -452,7 +479,8 @@ 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);
|
||||
result->flags = damage->flags;
|
||||
/* (the flags the game has) */
|
||||
result->flags = damage->flags & (FLAG(NUMBER_OF_DAMAGE_DATA_FLAGS) - 1);
|
||||
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) &&
|
||||
@@ -463,11 +491,22 @@ static boolean distributed_damage_to_data(
|
||||
result->scale = damage->scale;
|
||||
result->multiplier = damage->multiplier;
|
||||
result->material_effect_scale = damage->material_effect_scale;
|
||||
result->material_type = damage->material_type;
|
||||
/* (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;
|
||||
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:
|
||||
@@ -614,7 +653,8 @@ 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) &&
|
||||
if (distributed_player_is_local(damage->owner_player_index) &&
|
||||
(network_objects_client_has(object_index) || static_target(object_index)) &&
|
||||
damage_report_count < MAXIMUM_HIT_REPORTS_PER_TICK)
|
||||
{
|
||||
struct distributed_hit_report *report = &damage_reports[damage_report_count++];
|
||||
@@ -702,6 +742,33 @@ 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,
|
||||
@@ -721,6 +788,11 @@ 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)
|
||||
@@ -734,22 +806,15 @@ 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)));
|
||||
if (damage_event_count >= MAXIMUM_DAMAGE_EVENTS_PER_TICK - (kill ? 0 : RESERVED_KILL_EVENTS))
|
||||
event = damage_event_new(_damage_event_aftermath, object_index, damage, node_index, region_index, material_index,
|
||||
kill);
|
||||
if (!event)
|
||||
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)
|
||||
{
|
||||
@@ -1074,21 +1139,74 @@ static real distributed_source_deals(
|
||||
return distributed_source_deals_uncached(source_index, damage_index, grenade, kinds, reach);
|
||||
}
|
||||
|
||||
/* the oldest grenade of the type the host saw the player throw within
|
||||
RECENT_WEAPON_TICKS that has not gone off yet (network_damage_note_grenade),
|
||||
NONE for none */
|
||||
static short distributed_grenade_throw(
|
||||
short player_index,
|
||||
short grenade_type)
|
||||
{
|
||||
short index;
|
||||
short oldest = NONE;
|
||||
|
||||
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
|
||||
{
|
||||
long time = damage_players[player_index].throw_times[index];
|
||||
|
||||
if (time != NONE && damage_players[player_index].throw_types[index] == grenade_type &&
|
||||
game_time_get() - time <= RECENT_WEAPON_TICKS &&
|
||||
(oldest == NONE || time < damage_players[player_index].throw_times[oldest]))
|
||||
{
|
||||
oldest = index;
|
||||
}
|
||||
}
|
||||
return oldest;
|
||||
}
|
||||
|
||||
/* whether an explosion of the damage is among the player's this tick
|
||||
(distributed_report_paid): at the epicenter, or (NULL) anywhere */
|
||||
static boolean distributed_explosion_this_tick(
|
||||
short player_index,
|
||||
long damage_index,
|
||||
real_point3d const *epicenter)
|
||||
{
|
||||
short index;
|
||||
|
||||
if (damage_explosions[player_index].time != game_time_get())
|
||||
return FALSE;
|
||||
for (index = 0; index < damage_explosions[player_index].count; index++)
|
||||
{
|
||||
if (damage_explosions[player_index].explosions[index].definition_index == damage_index &&
|
||||
(!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x &&
|
||||
damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y &&
|
||||
damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z)))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* how many hits a second the player could deal of the damage: their
|
||||
weapons, lately, their grenades, the vehicle they drove lately; 0 when none
|
||||
of them deals it; how they deal it (the _damage_source flags); and how far
|
||||
from them what they fire deals it */
|
||||
weapons, lately, a grenade the host saw them throw that has not gone off
|
||||
(or one going off this tick, hitting more than one object), the vehicle
|
||||
they drove lately; 0 when none of them deals it; how they deal it (the
|
||||
_damage_source flags); how far from them what they fire deals it; and the
|
||||
type of grenade when nothing but a grenade of theirs deals it, else NONE */
|
||||
static real distributed_player_deals(
|
||||
short player_index,
|
||||
long damage_index,
|
||||
byte *kinds,
|
||||
struct damage_reach *reach)
|
||||
struct damage_reach *reach,
|
||||
short *grenade_type)
|
||||
{
|
||||
struct game_globals *globals = scenario_get_game_globals();
|
||||
short index;
|
||||
real rate = 0.0f;
|
||||
real grenade_rate = 0.0f;
|
||||
|
||||
*kinds = 0;
|
||||
*grenade_type = NONE;
|
||||
reach->distance = 0.0f;
|
||||
reach->ticks = 0.0f;
|
||||
reach->carried_ticks = 0.0f;
|
||||
@@ -1108,24 +1226,12 @@ static real distributed_player_deals(
|
||||
*kinds |= weapon_kinds;
|
||||
}
|
||||
}
|
||||
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
|
||||
/* (a blow with no weapon: the unit's own, units.c) */
|
||||
if (damage_players[player_index].unarmed_melee_damage_index == damage_index && damage_index != NONE &&
|
||||
game_time_get() - damage_players[player_index].unarmed_time <= RECENT_WEAPON_TICKS)
|
||||
{
|
||||
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
|
||||
struct game_globals_grenade);
|
||||
|
||||
if (game_time_get() - damage_players[player_index].grenade_times[index] <= RECENT_WEAPON_TICKS &&
|
||||
grenade->projectile.index != NONE)
|
||||
{
|
||||
byte grenade_kinds;
|
||||
struct damage_reach grenade_reach;
|
||||
real grenade_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &grenade_kinds,
|
||||
&grenade_reach);
|
||||
|
||||
if (grenade_rate > 0.0f)
|
||||
distributed_reach_combine(reach, &grenade_reach);
|
||||
rate = MAX(rate, grenade_rate);
|
||||
*kinds |= grenade_kinds;
|
||||
}
|
||||
rate = MAX(rate, MELEE_HITS_PER_SECOND);
|
||||
SET_FLAG(*kinds, _damage_source_melee_bit, TRUE);
|
||||
}
|
||||
/* (a vehicle's collision is its driver's, physics.c; the damage it deals
|
||||
a unit it kills is no one's) */
|
||||
@@ -1141,6 +1247,36 @@ static real distributed_player_deals(
|
||||
SET_FLAG(*kinds, _damage_source_collision_bit, TRUE);
|
||||
}
|
||||
}
|
||||
/* (a grenade's damage only from one thrown: holding grenades deals
|
||||
nothing) */
|
||||
for (index = 0; index < globals->grenades.count && index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
|
||||
{
|
||||
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&globals->grenades, index,
|
||||
struct game_globals_grenade);
|
||||
boolean thrown = distributed_grenade_throw(player_index, index) != NONE;
|
||||
|
||||
if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) &&
|
||||
grenade->projectile.index != NONE)
|
||||
{
|
||||
byte this_kinds;
|
||||
struct damage_reach this_reach;
|
||||
real this_rate = distributed_source_deals(grenade->projectile.index, damage_index, TRUE, &this_kinds,
|
||||
&this_reach);
|
||||
|
||||
if (this_rate > 0.0f)
|
||||
{
|
||||
distributed_reach_combine(reach, &this_reach);
|
||||
/* (one with a throw left before one only going off now) */
|
||||
if (*grenade_type == NONE || thrown)
|
||||
*grenade_type = index;
|
||||
}
|
||||
grenade_rate = MAX(grenade_rate, this_rate);
|
||||
*kinds |= this_kinds;
|
||||
}
|
||||
}
|
||||
if (rate > 0.0f)
|
||||
*grenade_type = NONE;
|
||||
rate = MAX(rate, grenade_rate);
|
||||
return rate * HIT_REPORT_RATE_MARGIN;
|
||||
}
|
||||
|
||||
@@ -1409,6 +1545,8 @@ static void distributed_note_weapons(
|
||||
|
||||
if (player_index >= MAXIMUM_TRACKED_PLAYERS || unit_index == NONE)
|
||||
continue;
|
||||
damage_players[player_index].unarmed_melee_damage_index = unit_unarmed_melee_damage(unit_index);
|
||||
damage_players[player_index].unarmed_time = game_time_get();
|
||||
units[0] = unit_index;
|
||||
units[1] = object_get(unit_index)->object.parent_object_index;
|
||||
/* (a vehicle's collisions are its driver's, physics.c) */
|
||||
@@ -1419,16 +1557,6 @@ static void distributed_note_weapons(
|
||||
if (vehicle && vehicle->unit.driver_object_index == unit_index)
|
||||
damage_players[player_index].driven_time = game_time_get();
|
||||
}
|
||||
{
|
||||
struct unit_datum *unit = unit_get(unit_index);
|
||||
short grenade_index;
|
||||
|
||||
for (grenade_index = 0; grenade_index < NUMBER_OF_UNIT_GRENADE_TYPES; grenade_index++)
|
||||
{
|
||||
if (unit->unit.grenade_counts[grenade_index] > 0)
|
||||
damage_players[player_index].grenade_times[grenade_index] = game_time_get();
|
||||
}
|
||||
}
|
||||
for (unit_number = 0; unit_number < 2; unit_number++)
|
||||
{
|
||||
struct unit_datum *unit = units[unit_number] != NONE ?
|
||||
@@ -1626,6 +1754,8 @@ static boolean distributed_report_valid(
|
||||
real reach;
|
||||
struct damage_reach fired_reach;
|
||||
short seen;
|
||||
short grenade_type;
|
||||
short throw_index = NONE;
|
||||
|
||||
/* that machine's player */
|
||||
if (player_index == NO_PLAYER || player_index >= MAXIMUM_TRACKED_PLAYERS ||
|
||||
@@ -1639,6 +1769,8 @@ 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 */
|
||||
@@ -1658,7 +1790,8 @@ static boolean distributed_report_valid(
|
||||
return FALSE;
|
||||
ticks = MIN(now - report->host_time + TARGET_HISTORY_SLACK_TICKS, TARGET_HISTORY_TICKS - 1);
|
||||
/* damage that player could deal, and how they deal it */
|
||||
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach);
|
||||
rate = distributed_player_deals(player_index, report->damage.definition_index, &kinds, &fired_reach,
|
||||
&grenade_type);
|
||||
if (rate <= 0.0f)
|
||||
return FALSE;
|
||||
/* ... of its shape: at a point, else a melee blow, else a collision; and
|
||||
@@ -1700,12 +1833,26 @@ static boolean distributed_report_valid(
|
||||
reach += definition->cutoff_radius;
|
||||
if (!(distributed_distance_squared(&report->damage.origin, &report->target_position) <= reach * reach))
|
||||
return FALSE;
|
||||
/* what only a grenade of theirs deals, from one the host saw them throw
|
||||
that has not gone off: its explosion once (the explosion's other objects
|
||||
that tick come free, distributed_report_paid), what it hits at a point
|
||||
while it is about */
|
||||
if (grenade_type != NONE &&
|
||||
!distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter))
|
||||
{
|
||||
throw_index = distributed_grenade_throw(player_index, grenade_type);
|
||||
if (throw_index == NONE)
|
||||
return FALSE;
|
||||
}
|
||||
/* no more than the weapon fires */
|
||||
if (!distributed_report_paid(player_index, report, rate, area && !melee && !collision,
|
||||
definition->cutoff_radius > 0.0f))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
/* (the grenade gone off: its throw spent) */
|
||||
if (throw_index != NONE && area && !melee && !collision)
|
||||
damage_players[player_index].throw_times[throw_index] = NONE;
|
||||
/* a melee blow from where the host had the player (its origin their
|
||||
head, its epicenter their body); a collision from the vehicle they
|
||||
drove */
|
||||
@@ -1926,6 +2073,26 @@ static boolean distributed_event_goes_to(
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* How often the host logs how many client hits it accepted and refused
|
||||
(only when it refused some). Tells "my shots don't register" apart from
|
||||
"I missed". */
|
||||
#define REPORT_LOG_TICKS (30 * TICKS_PER_SECOND)
|
||||
|
||||
static void distributed_log_reports(
|
||||
void)
|
||||
{
|
||||
static long logged_dealt;
|
||||
static long logged_rejected;
|
||||
|
||||
if (game_time_get() % REPORT_LOG_TICKS != 0 || damage_rejected_reports == logged_rejected)
|
||||
return;
|
||||
error(_error_silent, "damage: clients' hits in the last %d seconds: %ld dealt, %ld refused",
|
||||
REPORT_LOG_TICKS / TICKS_PER_SECOND, damage_dealt_reports - logged_dealt,
|
||||
damage_rejected_reports - logged_rejected);
|
||||
logged_dealt = damage_dealt_reports;
|
||||
logged_rejected = damage_rejected_reports;
|
||||
}
|
||||
|
||||
void network_damage_host_tick(
|
||||
void)
|
||||
{
|
||||
@@ -1936,6 +2103,7 @@ void network_damage_host_tick(
|
||||
distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES) : 0;
|
||||
short machine_number;
|
||||
|
||||
distributed_log_reports();
|
||||
distributed_note_weapons();
|
||||
distributed_note_targets();
|
||||
distributed_note_event_destinations();
|
||||
@@ -2044,6 +2212,15 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2089,6 +2266,41 @@ void network_damage_statistics(
|
||||
*replayed_events = damage_replayed_events;
|
||||
}
|
||||
|
||||
/* the host: a grenade a player's unit threw (units.c), as the host's own
|
||||
game threw it, which that player's machine reports the damage of
|
||||
(distributed_report_valid) */
|
||||
void network_damage_note_grenade(
|
||||
long unit_index,
|
||||
short grenade_type)
|
||||
{
|
||||
struct unit_datum *unit = unit_get(unit_index);
|
||||
long player_index;
|
||||
short index;
|
||||
short slot = 0;
|
||||
|
||||
if (game_connection() != _game_connection_network_server || unit->unit.player_index == NONE ||
|
||||
grenade_type < 0 || grenade_type >= NUMBER_OF_UNIT_GRENADE_TYPES)
|
||||
{
|
||||
return;
|
||||
}
|
||||
player_index = DATUM_INDEX_TO_ABSOLUTE_INDEX(unit->unit.player_index);
|
||||
if (player_index >= MAXIMUM_TRACKED_PLAYERS)
|
||||
return;
|
||||
/* (in a free place, else the oldest's) */
|
||||
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
|
||||
{
|
||||
if (damage_players[player_index].throw_times[index] == NONE)
|
||||
{
|
||||
slot = index;
|
||||
break;
|
||||
}
|
||||
if (damage_players[player_index].throw_times[index] < damage_players[player_index].throw_times[slot])
|
||||
slot = index;
|
||||
}
|
||||
damage_players[player_index].throw_times[slot] = game_time_get();
|
||||
damage_players[player_index].throw_types[slot] = grenade_type;
|
||||
}
|
||||
|
||||
void network_damage_new_game(
|
||||
void)
|
||||
{
|
||||
@@ -2107,8 +2319,9 @@ void network_damage_new_game(
|
||||
|
||||
for (index = 0; index < MAXIMUM_RECENT_WEAPONS; index++)
|
||||
damage_players[player_index].definition_indices[index] = NONE;
|
||||
for (index = 0; index < NUMBER_OF_UNIT_GRENADE_TYPES; index++)
|
||||
damage_players[player_index].grenade_times[index] = -RECENT_WEAPON_TICKS - 1;
|
||||
damage_players[player_index].unarmed_melee_damage_index = NONE;
|
||||
for (index = 0; index < MAXIMUM_GRENADE_THROWS; index++)
|
||||
damage_players[player_index].throw_times[index] = NONE;
|
||||
damage_players[player_index].driven_time = -RECENT_WEAPON_TICKS - 1;
|
||||
damage_players[player_index].hit_seconds = HIT_REPORT_BURST_SECONDS;
|
||||
damage_explosions[player_index].time = NONE;
|
||||
|
||||
@@ -53,18 +53,24 @@ machine (their datum identifiers need not be).
|
||||
|
||||
#include "cseries.h"
|
||||
#include "cseries/errors.h"
|
||||
#include "cache/cache_files.h"
|
||||
#include "models/model_animation_definitions.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_globals.h"
|
||||
#include "game/players.h"
|
||||
#include "game/game_engine.h"
|
||||
#include "main/main.h"
|
||||
#include "game/player_queues_new.h"
|
||||
#include "networking/network_game_globals.h"
|
||||
#include "objects/objects.h"
|
||||
#include "objects/damage.h"
|
||||
#include "scenario/scenario.h"
|
||||
#include "scenario/scenario_definitions.h"
|
||||
#include "structures/structure_bsp_definitions.h"
|
||||
#include "units/units.h"
|
||||
#include "units/biped_definitions.h"
|
||||
#include "units/bipeds.h"
|
||||
#include "network_coop.h"
|
||||
#include "network_distributed.h"
|
||||
|
||||
#include <limits.h>
|
||||
@@ -89,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);
|
||||
void network_game_server_kick_machine(long machine_index, boolean kept_out);
|
||||
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);
|
||||
@@ -238,11 +244,8 @@ before the host takes it no longer, and before the client is put where the
|
||||
host has it (no further than that: between the two they would disagree for
|
||||
good, the host's player somewhere its own is not) */
|
||||
#define HOST_ACCEPT_TOLERANCE 3.5f
|
||||
#define REMOTE_CORRECTION_TOLERANCE 0.05f
|
||||
#define LOCAL_CORRECTION_TOLERANCE 3.0f
|
||||
/* how far from the origin a unit is (world units), and how fast a client's
|
||||
own player's unit moves at most (world units a tick) */
|
||||
#define UNIT_WORLD_BOUND 32768.0f
|
||||
/* how fast a client's own player's unit moves at most (world units a tick) */
|
||||
#define MAXIMUM_PREDICTED_SPEED 2.0f
|
||||
/* ... on foot: this many times as fast as a player runs and jumps, or as
|
||||
fast as the host's ticks threw its copy lately, whichever is more; and how
|
||||
@@ -317,7 +320,9 @@ ticks before it */
|
||||
struct distributed_player_input
|
||||
{
|
||||
byte player_index;
|
||||
byte pad[3];
|
||||
/* co-op: the structure BSP the client has loaded */
|
||||
byte structure_bsp_index;
|
||||
byte pad[2];
|
||||
/* the client's tick */
|
||||
long tick;
|
||||
/* the latest host tick the client has had a message of (the host
|
||||
@@ -583,7 +588,10 @@ 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 */
|
||||
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 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];
|
||||
@@ -597,7 +605,11 @@ 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 */
|
||||
@@ -666,6 +678,40 @@ boolean distributed_point_valid(
|
||||
return fabsf(point->x) <= bound && fabsf(point->y) <= bound && fabsf(point->z) <= bound;
|
||||
}
|
||||
|
||||
/* whether a tag index from the host really is a tag of that group */
|
||||
boolean distributed_tag_of_group(
|
||||
long tag_index,
|
||||
unsigned long group_tag)
|
||||
{
|
||||
struct tag_iterator iterator;
|
||||
long index;
|
||||
|
||||
tag_iterator_new(&iterator, group_tag);
|
||||
while ((index = tag_iterator_next(&iterator)) != NONE)
|
||||
{
|
||||
if (index == tag_index)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* the graph's animation, if the graph tag and index are valid */
|
||||
struct animation *distributed_graph_animation(
|
||||
long animation_graph_index,
|
||||
short animation_index)
|
||||
{
|
||||
struct animation_graph *graph;
|
||||
|
||||
if (!distributed_tag_of_group(animation_graph_index, ANIMATION_GRAPH_TAG))
|
||||
return NULL;
|
||||
graph = animation_graph_definition_get(animation_graph_index);
|
||||
if (animation_index < 0 || animation_index >= graph->animations.count)
|
||||
return NULL;
|
||||
|
||||
return TAG_BLOCK_GET_ELEMENT(&graph->animations, animation_index, struct animation);
|
||||
}
|
||||
|
||||
boolean distributed_object_index_valid(
|
||||
long object_index)
|
||||
{
|
||||
@@ -749,7 +795,7 @@ void distributed_unit_vector_unpack(
|
||||
}
|
||||
}
|
||||
|
||||
static word distributed_vitality_pack(
|
||||
word distributed_vitality_pack(
|
||||
real value)
|
||||
{
|
||||
value *= VITALITY_SCALE;
|
||||
@@ -757,15 +803,13 @@ static word distributed_vitality_pack(
|
||||
return (word)(long)floor(value + 0.5f);
|
||||
}
|
||||
|
||||
static real distributed_vitality_unpack(
|
||||
real distributed_vitality_unpack(
|
||||
word value)
|
||||
{
|
||||
return (real)value / VITALITY_SCALE;
|
||||
}
|
||||
|
||||
/* an angle as a 16-bit fraction of a turn, and back (yaw from 0 to 2 pi,
|
||||
pitch from -pi to pi) */
|
||||
static short distributed_angle_pack(
|
||||
short distributed_angle_pack(
|
||||
real angle)
|
||||
{
|
||||
real turns = angle / (2.0f * _pi);
|
||||
@@ -774,7 +818,7 @@ static short distributed_angle_pack(
|
||||
return (short)(word)((long)floor(turns * 65536.0f + 0.5f) & 0xFFFF);
|
||||
}
|
||||
|
||||
static real distributed_angle_unpack(
|
||||
real distributed_angle_unpack(
|
||||
short value,
|
||||
boolean signed_angle)
|
||||
{
|
||||
@@ -1585,6 +1629,14 @@ 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)
|
||||
{
|
||||
@@ -2048,9 +2100,15 @@ static void distributed_apply_predictions(
|
||||
{
|
||||
distributed_predictions[player_index].valid = FALSE;
|
||||
distributed_predictions[player_index].taken_host_time = NONE;
|
||||
/* (of the unit it has now: not one of a life before) */
|
||||
if (unit_index != NONE && unit_index == state->unit_index)
|
||||
/* (of the unit it has now: not one of a life before; and not
|
||||
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);
|
||||
}
|
||||
}
|
||||
/* (what the host's next tick starts from) */
|
||||
if (unit_index != NONE)
|
||||
@@ -2222,6 +2280,7 @@ static void distributed_client_send_inputs(
|
||||
if (!update_client_distributed_input(local_player_index, &input->tick, &input->action, input->control_flags))
|
||||
continue;
|
||||
input->player_index = distributed_player_to_byte(player_index);
|
||||
input->structure_bsp_index = (byte)global_structure_bsp_index_get();
|
||||
input->host_time = distributed_host_time;
|
||||
count++;
|
||||
}
|
||||
@@ -2232,6 +2291,37 @@ static void distributed_client_send_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host, in co-op) where each client player's input last faced, by
|
||||
absolute index: how far it turns between inputs is its looking around */
|
||||
static struct
|
||||
{
|
||||
boolean valid;
|
||||
real_euler_angles2d facing;
|
||||
} distributed_input_facings[MAXIMUM_TRACKED_PLAYERS];
|
||||
|
||||
/* (the host, in co-op) a client player's input counted in the scripts'
|
||||
action tests, as a local player's is (player_control_action_test_note) */
|
||||
static void distributed_note_input_actions(
|
||||
short player_index,
|
||||
struct player_action const *action)
|
||||
{
|
||||
real_euler_angles2d turn = { 0.0f, 0.0f };
|
||||
|
||||
if (distributed_input_facings[player_index].valid)
|
||||
{
|
||||
turn.yaw = action->desired_facing.yaw - distributed_input_facings[player_index].facing.yaw;
|
||||
turn.pitch = action->desired_facing.pitch - distributed_input_facings[player_index].facing.pitch;
|
||||
/* (the short way round) */
|
||||
if (turn.yaw > _pi)
|
||||
turn.yaw -= 2.0f * _pi;
|
||||
else if (turn.yaw < -_pi)
|
||||
turn.yaw += 2.0f * _pi;
|
||||
}
|
||||
distributed_input_facings[player_index].valid = TRUE;
|
||||
distributed_input_facings[player_index].facing = action->desired_facing;
|
||||
player_control_action_test_note(action->control_flags, &turn, &action->throttle, action->primary_trigger);
|
||||
}
|
||||
|
||||
/* (the host) a client's players' input, and from it (once a message) how
|
||||
long a message takes that client and back */
|
||||
static void distributed_handle_inputs(
|
||||
@@ -2261,6 +2351,10 @@ static void distributed_handle_inputs(
|
||||
action.throttle.i = PIN(action.throttle.i, -1.0f, 1.0f);
|
||||
action.throttle.j = PIN(action.throttle.j, -1.0f, 1.0f);
|
||||
action.primary_trigger = PIN(action.primary_trigger, 0.0f, 1.0f);
|
||||
/* (a spectator's buttons pick whom it watches: not counted) */
|
||||
if (network_coop_active() && player->unit_index != NONE)
|
||||
distributed_note_input_actions(input->player_index, &action);
|
||||
network_coop_note_player_structure_bsp(input->player_index, input->structure_bsp_index);
|
||||
update_server_handle_distributed_input(DATUM_INDEX_NEW(input->player_index, player->identifier), input->tick,
|
||||
&action, input->control_flags, DISTRIBUTED_INPUT_HISTORY);
|
||||
if (input->host_time != NONE && (host_time == NONE || input->host_time > host_time))
|
||||
@@ -2931,6 +3025,52 @@ static void distributed_send_pings(
|
||||
}
|
||||
}
|
||||
|
||||
/* Co-op: the host's structure BSP. Clients switch BSP only when told
|
||||
(their own trigger volumes don't, players.c). A switch is sent at once and
|
||||
reliably; it is also resent regularly so a client that joined late catches
|
||||
up. */
|
||||
#define STRUCTURE_BSP_INTERVAL_TICKS (TICKS_PER_SECOND / 2)
|
||||
|
||||
static short distributed_sent_structure_bsp_index = NONE;
|
||||
|
||||
struct distributed_structure_bsp
|
||||
{
|
||||
short structure_bsp_index;
|
||||
short pad;
|
||||
};
|
||||
|
||||
struct distributed_structure_bsp_message
|
||||
{
|
||||
struct distributed_message_header header;
|
||||
struct distributed_structure_bsp structure_bsp;
|
||||
};
|
||||
|
||||
static void distributed_send_structure_bsp(
|
||||
void)
|
||||
{
|
||||
struct distributed_structure_bsp_message message;
|
||||
short index = global_structure_bsp_index_get();
|
||||
boolean switched = index != distributed_sent_structure_bsp_index;
|
||||
|
||||
message.structure_bsp.structure_bsp_index = index;
|
||||
message.structure_bsp.pad = 0;
|
||||
distributed_send(&message, _distributed_message_structure_bsp, 1, (word)sizeof(message),
|
||||
switched ? _distributed_to_clients_reliably : _distributed_to_clients);
|
||||
distributed_sent_structure_bsp_index = index;
|
||||
}
|
||||
|
||||
static void distributed_handle_structure_bsp(
|
||||
struct distributed_structure_bsp const *structure_bsp)
|
||||
{
|
||||
short index = structure_bsp->structure_bsp_index;
|
||||
|
||||
if (network_coop_active() && index >= 0 && index < global_scenario_get()->structure_bsp_references.count &&
|
||||
index != global_structure_bsp_index_get())
|
||||
{
|
||||
main_switch_structure_bsp(index);
|
||||
}
|
||||
}
|
||||
|
||||
/* the players' statistics that changed since they were last sent, and when
|
||||
refreshing, STATISTICS_REFRESH_PLAYERS more whatever they are, round them
|
||||
all (a client that lost a change has it again within eight seconds) */
|
||||
@@ -3076,6 +3216,7 @@ void network_distributed_new_game(
|
||||
csmemset(distributed_accepted, 0, sizeof(distributed_accepted));
|
||||
csmemset(distributed_host_speeds, 0, sizeof(distributed_host_speeds));
|
||||
csmemset(distributed_round_trips, 0, sizeof(distributed_round_trips));
|
||||
csmemset(distributed_input_facings, 0, sizeof(distributed_input_facings));
|
||||
csmemset(distributed_client_clocks, 0, sizeof(distributed_client_clocks));
|
||||
csmemset(distributed_client_identities, 0, sizeof(distributed_client_identities));
|
||||
distributed_identity_sent = FALSE;
|
||||
@@ -3136,6 +3277,8 @@ void network_distributed_new_game(
|
||||
network_damage_new_game();
|
||||
network_vr_new_game();
|
||||
bots_new_game();
|
||||
network_actors_new_game();
|
||||
network_coop_new_game();
|
||||
}
|
||||
|
||||
/* after each tick (game_time.c) */
|
||||
@@ -3178,9 +3321,18 @@ void network_distributed_tick(
|
||||
distributed_statistics_due = FALSE;
|
||||
if (game_time_get() % PING_INTERVAL_TICKS == 0)
|
||||
distributed_send_pings();
|
||||
/* (before the players, so clients load a new BSP before they hear
|
||||
where the host moved everyone into it) */
|
||||
if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index ||
|
||||
game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0))
|
||||
{
|
||||
distributed_send_structure_bsp();
|
||||
}
|
||||
distributed_host_send_players();
|
||||
network_vr_tick();
|
||||
bots_tick();
|
||||
network_actors_host_tick();
|
||||
network_coop_host_tick();
|
||||
distributed_send_pickups();
|
||||
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
|
||||
distributed_send_game_state(NONE);
|
||||
@@ -3193,6 +3345,7 @@ void network_distributed_tick(
|
||||
distributed_client_send_predictions();
|
||||
network_objects_client_tick();
|
||||
network_damage_client_tick();
|
||||
network_coop_client_tick();
|
||||
}
|
||||
distributed_batches_flush();
|
||||
distributed_machines.in_tick = FALSE;
|
||||
@@ -3221,6 +3374,19 @@ static boolean distributed_message_stale(
|
||||
case _distributed_message_damage_events:
|
||||
case _distributed_message_vr_poses:
|
||||
case _distributed_message_pings:
|
||||
case _distributed_message_actor_states:
|
||||
case _distributed_message_structure_bsp:
|
||||
case _distributed_message_coop_presentation:
|
||||
case _distributed_message_coop_device_groups:
|
||||
case _distributed_message_coop_object_names:
|
||||
case _distributed_message_coop_skip_vote:
|
||||
case _distributed_message_coop_events:
|
||||
case _distributed_message_coop_object_transforms:
|
||||
case _distributed_message_actor_damage:
|
||||
case _distributed_message_coop_object_looks:
|
||||
case _distributed_message_damage_animations:
|
||||
case _distributed_message_coop_screen_effect:
|
||||
case _distributed_message_coop_device_states:
|
||||
break;
|
||||
default:
|
||||
return FALSE;
|
||||
@@ -3523,6 +3689,27 @@ void network_distributed_ban(
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host: network_server_manager.c, its kick command) players kicked by
|
||||
the host, which may join again: every machine told, nothing kept (no line
|
||||
in BANS_FILE) */
|
||||
void network_distributed_kick(
|
||||
char const *names)
|
||||
{
|
||||
char kept_names[64];
|
||||
char notice[MAXIMUM_NOTICE_LENGTH];
|
||||
|
||||
distributed_printable(kept_names, sizeof(kept_names), names);
|
||||
snprintf(notice, sizeof(notice), "%s kicked by the host", kept_names);
|
||||
/* (to every client in the game: in the lobby, the host's own) */
|
||||
if (game_in_progress())
|
||||
distributed_send_notice(notice);
|
||||
else
|
||||
{
|
||||
console_warning("%s", notice);
|
||||
error(_error_log, "%s", notice);
|
||||
}
|
||||
}
|
||||
|
||||
/* (the host) a client machine's tick, which one of its messages is
|
||||
stamped with: its clock measured, each window, against the host's; one
|
||||
whose game runs fast (distributed_client_clock) has its players'
|
||||
@@ -3538,6 +3725,8 @@ 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;
|
||||
@@ -3559,6 +3748,7 @@ static void distributed_note_client_clock(
|
||||
if (!clock->fast)
|
||||
{
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
else if (++clock->fast_windows == 1)
|
||||
{
|
||||
@@ -3575,7 +3765,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[64];
|
||||
char reason[96];
|
||||
char text[MAXIMUM_NOTICE_LENGTH];
|
||||
|
||||
distributed_machine_player_names(machine_index, names, sizeof(names));
|
||||
@@ -3585,14 +3775,30 @@ 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)", rate);
|
||||
snprintf(reason, sizeof(reason), "speed hack (game ran %.2f times as fast)%s", rate,
|
||||
clock->ahead_on_stream ? "" : " (its datagrams only: not banned)");
|
||||
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);
|
||||
distributed_log_cheater(machine_index, names, reason);
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
network_game_server_kick_machine(machine_index);
|
||||
network_game_server_kick_machine(machine_index, clock->ahead_on_stream);
|
||||
clock->fast_windows = 0;
|
||||
clock->ahead_on_stream = FALSE;
|
||||
}
|
||||
}
|
||||
clock->window_tick = clock->latest_tick;
|
||||
@@ -3658,6 +3864,19 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_unit_states: entry_size = DISTRIBUTED_UNIT_STATE_MINIMUM_SIZE; break;
|
||||
case _distributed_message_player_statistics: entry_size = sizeof(struct distributed_player_statistics); break;
|
||||
case _distributed_message_pings: entry_size = sizeof(struct distributed_player_ping); break;
|
||||
case _distributed_message_actor_states: entry_size = network_actors_entry_size(); break;
|
||||
case _distributed_message_structure_bsp: entry_size = sizeof(struct distributed_structure_bsp); break;
|
||||
case _distributed_message_coop_presentation: entry_size = network_coop_presentation_entry_size(); break;
|
||||
case _distributed_message_coop_events: entry_size = network_coop_event_entry_size(); break;
|
||||
case _distributed_message_coop_skip_vote: entry_size = network_coop_skip_vote_entry_size(); break;
|
||||
case _distributed_message_coop_device_groups: entry_size = network_coop_device_group_entry_size(); break;
|
||||
case _distributed_message_coop_object_names: entry_size = network_coop_object_names_entry_size(); break;
|
||||
case _distributed_message_coop_object_transforms: entry_size = network_coop_object_transform_entry_size(); break;
|
||||
case _distributed_message_actor_damage: entry_size = network_actors_damage_entry_size(); break;
|
||||
case _distributed_message_coop_object_looks: entry_size = network_coop_object_look_entry_size(); break;
|
||||
case _distributed_message_damage_animations: entry_size = network_objects_damage_animation_entry_size(); break;
|
||||
case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break;
|
||||
case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break;
|
||||
case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break;
|
||||
case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break;
|
||||
case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break;
|
||||
@@ -3676,6 +3895,19 @@ void network_distributed_handle_message(
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* (the kinds whose handlers read one entry: not without it) */
|
||||
switch (header.type)
|
||||
{
|
||||
case _distributed_message_structure_bsp:
|
||||
case _distributed_message_coop_presentation:
|
||||
case _distributed_message_coop_object_names:
|
||||
case _distributed_message_coop_skip_vote:
|
||||
if (header.count < 1)
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
distributed_statistics.received++;
|
||||
|
||||
/* (each kind from the host, or from a client; a VR player's hands
|
||||
@@ -3699,6 +3931,7 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_hit_reports:
|
||||
case _distributed_message_vehicle_prediction:
|
||||
case _distributed_message_player_inputs:
|
||||
case _distributed_message_coop_skip_vote:
|
||||
if (machine_index == NONE || game_connection() != _game_connection_network_server)
|
||||
return;
|
||||
break;
|
||||
@@ -3735,6 +3968,45 @@ void network_distributed_handle_message(
|
||||
case _distributed_message_unit_states:
|
||||
distributed_handle_unit_states((byte const *)entries, (byte const *)message + size, header.count);
|
||||
break;
|
||||
case _distributed_message_actor_states:
|
||||
network_actors_handle_states(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_structure_bsp:
|
||||
distributed_handle_structure_bsp((struct distributed_structure_bsp const *)entries);
|
||||
break;
|
||||
case _distributed_message_coop_presentation:
|
||||
network_coop_handle_presentation(entries, header.game_time);
|
||||
break;
|
||||
case _distributed_message_coop_events:
|
||||
network_coop_handle_events(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_coop_skip_vote:
|
||||
network_coop_handle_skip_vote(machine_index, entries);
|
||||
break;
|
||||
case _distributed_message_coop_device_groups:
|
||||
network_coop_handle_device_groups(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_coop_object_names:
|
||||
network_coop_handle_object_names(entries);
|
||||
break;
|
||||
case _distributed_message_coop_object_transforms:
|
||||
network_coop_handle_object_transforms(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_actor_damage:
|
||||
network_actors_handle_damage(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_coop_object_looks:
|
||||
network_coop_handle_object_looks(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_damage_animations:
|
||||
network_objects_handle_damage_animations(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_coop_screen_effect:
|
||||
network_coop_handle_screen_effect(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_coop_device_states:
|
||||
network_coop_handle_device_states(entries, header.count);
|
||||
break;
|
||||
case _distributed_message_player_statistics:
|
||||
{
|
||||
/* the host's count of kills, deaths, ... */
|
||||
@@ -3864,3 +4136,16 @@ 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;
|
||||
}
|
||||