Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e9de9a11c | ||
|
|
ce95f7deed | ||
|
|
a20ced034c | ||
|
|
fc22e8467d | ||
|
|
038252b1ae | ||
|
|
61621e6513 | ||
|
|
1d79584ea4 | ||
|
|
577f718a63 | ||
|
|
b68d259c81 | ||
|
|
600edf7bb9 | ||
|
|
35554cd243 | ||
|
|
a0f22cbba7 | ||
|
|
9f04a51259 | ||
|
|
47981601da | ||
|
|
a4e4bd8317 | ||
|
|
d4687b24e2 | ||
|
|
9044950d78 | ||
|
|
9d7436bd64 | ||
|
|
cdce33647a |
No files matched your search
@@ -1,5 +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
|
||||
url: https://github.com/Cesarus85/mjolnir-vr#install-on-meta-quest-3--3s
|
||||
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
|
||||
@@ -13,7 +13,7 @@ name: Build
|
||||
# - a newer push to the same branch cancels a run still going;
|
||||
# - pushes that only change documentation do not build;
|
||||
# - Windows, whose minutes cost twice Linux's, builds only Windows;
|
||||
# - Ringfall: Mjolnir VR builds only Android (the VR app): the Linux and
|
||||
# - Mjolnir VR builds only Android (the VR app): the Linux and
|
||||
# Windows jobs are kept, off (if: false), so that upstream's changes to
|
||||
# them still merge;
|
||||
# - artifacts are kept for three days.
|
||||
@@ -24,7 +24,7 @@ name: Build
|
||||
# gives the latest build; the last five are kept. Its web build is then the
|
||||
# site on GitHub Pages (https://halocombatevolved.com/, port/web/README.md).
|
||||
on:
|
||||
# (Ringfall: Mjolnir VR: main only; its branches are tried on the headsets
|
||||
# (Mjolnir VR: main only; its branches are tried on the headsets
|
||||
# from local builds)
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -52,7 +52,7 @@ env:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
# (off: Ringfall: Mjolnir VR ships the Android app only)
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# Arch Linux has what the build needs as packages: clang 22 (for the
|
||||
# profile-guided optimisation in pgo/), and CMake, pkg-config and
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
windows:
|
||||
# (off: Ringfall: Mjolnir VR ships the Android app only)
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# The runner image has LLVM, Python and Visual Studio's x86 libraries and
|
||||
# Windows SDK.
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
retention-days: 3
|
||||
|
||||
web:
|
||||
# (off: Ringfall: Mjolnir VR ships the Android app only)
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# Emscripten (emsdk, the version below, cached) builds the web port's
|
||||
# site (port/web/README.md); the tests check its graph, the page's
|
||||
@@ -389,18 +389,26 @@ jobs:
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir release
|
||||
# (Ringfall: Mjolnir VR publishes its Android builds only, under
|
||||
# (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" .)
|
||||
zip -qr "$GITHUB_WORKSPACE/release/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
|
||||
cp artifacts/halo-android-release/app-release.apk release/MjolnirVR.apk
|
||||
# the same under the names before "Mjolnir VR" (Ringfall: Mjolnir
|
||||
# VR, builds 302 to 313): their updaters (Updater.java, the Frame's
|
||||
# launcher) and the links already shared ask for them
|
||||
cp release/mjolnir-vr-android-debug.zip release/ringfall-mjolnir-vr-android-debug.zip
|
||||
cp release/mjolnir-vr-android-release.zip release/ringfall-mjolnir-vr-android-release.zip
|
||||
cp release/mjolnir-vr-steam-frame.zip release/ringfall-mjolnir-vr-steam-frame.zip
|
||||
cp release/Install-Mjolnir-VR.desktop release/Install-Ringfall-Mjolnir-VR.desktop
|
||||
cp release/MjolnirVR.apk release/RingfallMjolnirVR.apk
|
||||
ls -l release
|
||||
|
||||
- name: Publish
|
||||
@@ -415,15 +423,15 @@ jobs:
|
||||
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 '**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 '| Meta Quest 3 / 3S | `MjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
|
||||
printf '| Steam Frame | `Install-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
|
||||
printf 'The other files are for the built-in updater and the Steam Frame installer (those named `ringfall-…` / `Ringfall…`: the same, for the builds before the name Mjolnir VR).\n\n'
|
||||
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
|
||||
} > notes.md
|
||||
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
|
||||
--title "Ringfall: Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
--title "Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The Windows builds' executables and PDBs to Sentry, which turns the
|
||||
@@ -465,7 +473,7 @@ jobs:
|
||||
done
|
||||
|
||||
pages:
|
||||
# (off: Ringfall: Mjolnir VR ships the Android app only)
|
||||
# (off: Mjolnir VR ships the Android app only)
|
||||
if: ${{ false }}
|
||||
# The release's web build to GitHub Pages, which serves it at
|
||||
# https://halocombatevolved.com/ (port/web/README.md, "Hosting"). Only
|
||||
@@ -586,7 +594,7 @@ jobs:
|
||||
title="Build failed"; color=15548997
|
||||
fi
|
||||
latest="$SERVER/$REPOSITORY/releases/latest"
|
||||
downloads="[Quest]($latest/download/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
downloads="[Quest]($latest/download/mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Mjolnir-VR.desktop) ([all builds]($latest))"
|
||||
if [ -n "$RELEASE_TAG" ]; then
|
||||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||||
if [ "$PAGES" = success ]; then
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
weapons' reticles): redraws by MrBruh (`port/assets/hud`), based on
|
||||
reference photos by Aerocatia. The sniper rifle's zoom labels (2x, 10x)
|
||||
are set in Overpass (`port/assets/fonts`).
|
||||
- **The menus' mouse cursor** (the blue arrow the menus show for the
|
||||
mouse): by pugzysixtyfour (`port/assets/cursor`).
|
||||
|
||||
## Third-party art
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<p align="center">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Ringfall: Mjolnir VR">
|
||||
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Mjolnir VR">
|
||||
</p>
|
||||
|
||||
# Ringfall: Mjolnir VR
|
||||
# Mjolnir VR
|
||||
|
||||
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
|
||||
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
|
||||
multiplayer in full 6DoF virtual reality, with your hands in the game.
|
||||
|
||||
Ringfall: Mjolnir VR runs on your headset by itself, without a PC. It
|
||||
Mjolnir VR runs on your headset by itself, without a PC. It
|
||||
brings no game files: it uses a disc image of **your own, purchased copy**
|
||||
of the game.
|
||||
|
||||
> **Unofficial fan project.** Ringfall: Mjolnir VR is a free,
|
||||
> **Unofficial fan project.** Mjolnir VR is a free,
|
||||
> non-commercial fan project. It is not affiliated with, endorsed,
|
||||
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
|
||||
> Bungie. See [Legal](#legal).
|
||||
|
||||
Until build 313 it was called *Ringfall: Mjolnir VR*. Installed builds
|
||||
update to the new name by themselves, and the old links lead here.
|
||||
|
||||
## Contents
|
||||
|
||||
- [What you get](#what-you-get)
|
||||
@@ -86,14 +89,14 @@ that it updates itself.
|
||||
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
|
||||
3. **Download `MjolnirVR.apk`** from the
|
||||
[latest release](../../releases/latest) and drag it onto the SideQuest
|
||||
window. It installs the game.
|
||||
4. **Copy your disc image** to the Quest's `Download` folder: in
|
||||
SideQuest, open the file manager (the folder icon), go to `Download`
|
||||
and drag the `.iso` file in.
|
||||
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
|
||||
start **Ringfall: Mjolnir VR**. Select **Choose disc image** and pick
|
||||
start **Mjolnir VR**. Select **Choose disc image** and pick
|
||||
your file in `Download`. The game copies what it needs (about 1.8 GB,
|
||||
a minute or two) and starts.
|
||||
|
||||
@@ -108,14 +111,14 @@ No computer and no keyboard needed.
|
||||
*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
|
||||
3. **Download `Install-Mjolnir-VR.desktop`** from the
|
||||
[latest release](../../releases/latest) with the web browser.
|
||||
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
|
||||
If you are asked whether to run it, choose to continue.
|
||||
5. **Answer the dialogs.** The installer finds your disc image, copies
|
||||
what the game needs, adds **Ringfall: Mjolnir VR** to your Steam
|
||||
what the game needs, adds **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*).
|
||||
6. Start **Mjolnir VR** from your library (under *Non-Steam*).
|
||||
|
||||
The game updates itself: when a new version is out, it is downloaded
|
||||
while you play and used from the next start on. Your settings, saves and
|
||||
@@ -155,7 +158,7 @@ telemetry.
|
||||
|
||||
## Credits
|
||||
|
||||
Ringfall: Mjolnir VR stands on the work of others:
|
||||
Mjolnir VR stands on the work of others:
|
||||
|
||||
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
|
||||
Community Edition): the port of the decompiled game to Linux, Windows
|
||||
@@ -186,7 +189,7 @@ port it is built on.
|
||||
|
||||
## Legal
|
||||
|
||||
Ringfall: Mjolnir VR is an unofficial, free, non-commercial fan project.
|
||||
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
|
||||
@@ -201,7 +204,7 @@ their games.
|
||||
|
||||
### Licence
|
||||
|
||||
Ringfall: Mjolnir VR is free software: you can give it to others and change
|
||||
Mjolnir VR is free software: you can give it to others and change
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, version 3 of the License or (at your option)
|
||||
any later version (`AGPL-3.0-or-later`), as OpenCE, which it builds on, is.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Third-party notices
|
||||
|
||||
Ringfall: Mjolnir VR is released under the GNU Affero General Public
|
||||
Mjolnir VR is released under the GNU Affero General Public
|
||||
License, version 3 or later ([LICENSE](LICENSE)); releases before build 312
|
||||
were released under [CC0 1.0](LICENSES/CC0-1.0.txt). It includes or builds
|
||||
on the following, under their own licenses; each license's full text is in
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
|
||||
> the port Ringfall: Mjolnir VR is built on, kept for its build and
|
||||
> the port Mjolnir VR is built on, kept for its build and
|
||||
> development notes. Its download links are OpenCE's. For the VR mod, see
|
||||
> the [main README](../README.md).
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Android
|
||||
|
||||
The app of Ringfall: Mjolnir VR (application id `io.github.cesarus85.ringfall`;
|
||||
The app of Mjolnir VR (application id `io.github.cesarus85.ringfall`, from
|
||||
the name before, "Ringfall: Mjolnir VR", kept so that installed apps update;
|
||||
its Java classes keep the decompilation's package, `com.halo.decomp`).
|
||||
`ninja android` builds the game for 64-bit ARM Android (arm64-v8a).
|
||||
`ninja android_apk` makes an app from it:
|
||||
|
||||
@@ -10,10 +10,10 @@ 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
|
||||
2. Download `Install-Mjolnir-VR.desktop` from the latest release and open it
|
||||
(double-click; confirm running it).
|
||||
3. Answer the dialogs: the installer finds the disc image (or asks for it),
|
||||
copies the game data out of it, adds "Ringfall: Mjolnir VR" to the Steam library
|
||||
copies the game data out of it, adds "Mjolnir VR" to the Steam library
|
||||
and switches back to Gaming Mode. Start it from the library there.
|
||||
|
||||
The game updates itself: while it runs, its launcher looks for a newer
|
||||
@@ -23,18 +23,20 @@ release, fetches it and puts it in place as the next start begins
|
||||
too (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
|
||||
release's `mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
|
||||
the release job of `.github/workflows/build.yml`) and runs its
|
||||
`install.sh` (`steam_frame_install.sh`), with zenity dialogs:
|
||||
- `~/Documents/RingfallMjolnirVR`: the launcher (`ringfall-mjolnir-vr.sh`)
|
||||
and the release APK (`RingfallMjolnirVR.apk`); the folder of the builds
|
||||
before the name, `~/Documents/HaloCE`, renamed to it, and their "Halo VR"
|
||||
shortcut replaced;
|
||||
shortcut replaced. (These keep the name of builds 302 to 313, "Ringfall:
|
||||
Mjolnir VR": the Steam shortcut of an installed game starts the launcher
|
||||
there, and the launcher's update looks for them in the bundle.);
|
||||
- the maps folder out of the disc image (`steam_frame_xiso.py`, the app's
|
||||
`XisoExtractor.java` in Python), unless `maps/ui.map` is there;
|
||||
- the Steam shortcut, unless it is there: added by Steam itself
|
||||
(`steam://addnonsteamgame`, as SteamOS's "Add to Steam" does) from
|
||||
`Ringfall Mjolnir VR.desktop` beside the game, then the library pictures
|
||||
`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
|
||||
@@ -83,7 +85,7 @@ dialogs, for testing over adb; it leaves Steam and its shortcuts alone.
|
||||
|
||||
## Starting it on the Frame
|
||||
|
||||
A button in the Steam library ("Ringfall: Mjolnir VR", a non-Steam game)
|
||||
A button in the Steam library ("Mjolnir VR", a non-Steam game)
|
||||
runs `~/Documents/RingfallMjolnirVR/ringfall-mjolnir-vr.sh` (`port/android/steam_frame_launch.sh`),
|
||||
which starts the game as Steam starts an Android title of its own:
|
||||
`lepton waitforexitandrun -- ~/Documents/RingfallMjolnirVR/RingfallMjolnirVR.apk`, in a container
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# VR roadmap
|
||||
|
||||
What Ringfall: Mjolnir VR (Quest 3, Steam Frame) has, and what comes next, in the
|
||||
What Mjolnir VR (Quest 3, Steam Frame) has, and what comes next, in the
|
||||
agreed order. Updated with every step: a step is **done** once the user has
|
||||
tried it in the headset and it is merged into `main`; **built** means in a
|
||||
branch, not yet tried in the headset.
|
||||
@@ -46,6 +46,7 @@ named where code is taken over.
|
||||
| 4 | Later: disc image import in the launcher; moving by swinging the arms (in place of the stick, never faster than it: online too), and a sprint by swinging them harder (faster than the game allows: offline only) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Arm swinging and its offline sprint still open |
|
||||
| 5 | Later: multiview again (both eyes in one pass, half the drawing's CPU cost): find why its eyes do not fuse in big online games on the Quest 3 (and now and then on the Steam Frame), then turn it back on | open: off by default since 2026-10-16 (`debug.multiview`); findings in the open points |
|
||||
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
|
||||
| R2 | Renamed **Mjolnir VR** ("Ringfall" is another game's name): app and Steam names, logo (`tools/mjolnir_art.py`), release assets (also under the old names for the updaters of builds 302 to 313), updaters, README; the repository renamed `Cesarus85/mjolnir-vr` (GitHub forwards the old links). Kept: the app id, the signing key's alias, the Frame's `~/Documents/RingfallMjolnirVR` and the names inside its bundle (installed games and their shortcuts) | in `feature/rename-mjolnir-vr` (2026-10-21) |
|
||||
|
||||
## Step 2, first part (done)
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ public class LauncherActivity extends Activity {
|
||||
layout.setBackgroundColor(Color.rgb(12, 16, 20));
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText("Ringfall: Mjolnir VR needs game data");
|
||||
title.setText("Mjolnir VR needs game data");
|
||||
title.setTextColor(Color.WHITE);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
|
||||
@@ -36,7 +36,7 @@ import java.util.zip.ZipInputStream;
|
||||
* the latest release when it starts, on a thread of its own, and if it is
|
||||
* newer asks the player whether to update:
|
||||
*
|
||||
* - Yes: the release's app (ringfall-mjolnir-vr-android-release.zip or
|
||||
* - Yes: the release's app (mjolnir-vr-android-release.zip or
|
||||
* -debug.zip) is
|
||||
* downloaded and handed to Android's package installer, which replaces the
|
||||
* game (closing it) and offers to open the new version.
|
||||
@@ -53,8 +53,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 APP_NAME = "Ringfall: Mjolnir VR";
|
||||
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
|
||||
private static final String APP_NAME = "Mjolnir VR";
|
||||
private static final String REPOSITORY = "Cesarus85/mjolnir-vr";
|
||||
private static final String USER_AGENT = "halo-ce-universal-updater";
|
||||
private static final int TIMEOUT_MILLISECONDS = 20000;
|
||||
/** the most a download (a release's zip, about 35 MB) or the app in it may be */
|
||||
@@ -240,7 +240,7 @@ final class Updater {
|
||||
/* ---------- updating */
|
||||
|
||||
private static void update(Activity activity, int latest) {
|
||||
String asset = "ringfall-mjolnir-vr-android-" + (BuildConfig.DEBUG ? "debug" : "release") + ".zip";
|
||||
String asset = "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);
|
||||
|
||||
@@ -153,7 +153,7 @@ final class XisoExtractor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// (Ringfall: a European disc in two languages, Spanish and Italian,
|
||||
// (Mjolnir VR: a European disc in two languages, Spanish and Italian,
|
||||
// has no maps folder but one for each, maps_es and maps_it: the
|
||||
// device's language's is taken, else English's, else the first, and
|
||||
// its language written to maps/language.txt, which the game asks the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Ringfall: Mjolnir VR</string>
|
||||
<string name="app_name">Mjolnir VR</string>
|
||||
</resources>
|
||||
@@ -65,7 +65,7 @@ void host_fatal(const char *format, ...)
|
||||
vsnprintf(message, sizeof(message), format, arguments);
|
||||
va_end(arguments);
|
||||
__android_log_write(ANDROID_LOG_FATAL, "halo", message);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Ringfall: Mjolnir VR", message, NULL);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Mjolnir VR", message, NULL);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
@@ -540,7 +540,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
host_logf(HOST_LOG_INFO, "Ringfall: Mjolnir VR starting");
|
||||
host_logf(HOST_LOG_INFO, "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");
|
||||
|
||||
@@ -1256,7 +1256,7 @@ with vr.log_performance: host_main.c) */
|
||||
android.applicationVM = machine;
|
||||
android.applicationActivity = activity;
|
||||
instance.next = &android;
|
||||
strcpy(instance.applicationInfo.applicationName, "Ringfall Mjolnir VR");
|
||||
strcpy(instance.applicationInfo.applicationName, "Mjolnir VR");
|
||||
instance.applicationInfo.applicationVersion = 1;
|
||||
strcpy(instance.applicationInfo.engineName, "halo-ce-universal");
|
||||
instance.applicationInfo.engineVersion = 1;
|
||||
@@ -2275,7 +2275,7 @@ void host_xr_test(void)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||
window = SDL_CreateWindow("Ringfall: Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
window = SDL_CreateWindow("Mjolnir VR", 640, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN);
|
||||
context = window ? SDL_GL_CreateContext(window) : NULL;
|
||||
if (!context || !SDL_GL_MakeCurrent(window, context))
|
||||
host_fatal("VR test: no OpenGL ES context: %s", SDL_GetError());
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 70 KiB |
@@ -1,6 +1,6 @@
|
||||
#!/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
|
||||
# Mjolnir VR's installer for the Steam Frame, run in its Desktop
|
||||
# Mode from the bundle the release's "Install Mjolnir VR" desktop
|
||||
# file downloads (steam_frame_installer.desktop; the bundle: this script,
|
||||
# RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh = steam_frame_launch.sh,
|
||||
# steam_frame_xiso.py and the library pictures in grid/). Everything is
|
||||
@@ -19,28 +19,31 @@
|
||||
# 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
|
||||
# MJOLNIR_UNATTENDED=1 answers every question with its default and prints
|
||||
# instead of showing dialogs (MJOLNIR_IMAGE: the disc image to use), for
|
||||
# testing over adb; it never closes Steam.
|
||||
|
||||
BUNDLE="$(cd "$(dirname "$0")" && pwd)"
|
||||
GAME_DIR="$HOME/Documents/RingfallMjolnirVR"
|
||||
STEAM_DIR="$HOME/.local/share/Steam"
|
||||
LEPTON="$STEAM_DIR/steamapps/common/Lepton/lepton"
|
||||
TITLE="Ringfall: Mjolnir VR"
|
||||
NAME="Ringfall: Mjolnir VR"
|
||||
TITLE="Mjolnir VR"
|
||||
NAME="Mjolnir VR"
|
||||
APK_NAME="RingfallMjolnirVR.apk"
|
||||
LAUNCHER="ringfall-mjolnir-vr.sh"
|
||||
EXE="\"$GAME_DIR/$LAUNCHER\""
|
||||
# (the desktop file Steam adds the game from)
|
||||
NAME_FILE="Ringfall Mjolnir VR"
|
||||
NAME_FILE="Mjolnir VR"
|
||||
# the builds before the name: their folder and Steam shortcut
|
||||
OLD_DIR="$HOME/Documents/HaloCE"
|
||||
OLD_NAME="Halo VR"
|
||||
# the name of builds 302 to 313, which their shortcut keeps (Steam writes
|
||||
# its shortcuts back from memory: the player renames it)
|
||||
FORMER_NAME="Ringfall: Mjolnir VR"
|
||||
|
||||
# what the installer did, for looking into problems: written to /tmp while
|
||||
# it runs, then kept beside the game as install.log
|
||||
LOG="/tmp/ringfall-mjolnir-vr-install.log"
|
||||
LOG="/tmp/mjolnir-vr-install.log"
|
||||
: > "$LOG"
|
||||
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
|
||||
keep_log() { [ -d "$GAME_DIR" ] && cp -f "$LOG" "$GAME_DIR/install.log" 2>/dev/null; }
|
||||
@@ -59,7 +62,7 @@ steam_running()
|
||||
pgrep -f 'ubuntu12_32/steam( |$)' >/dev/null || pgrep -x steam >/dev/null
|
||||
}
|
||||
|
||||
unattended() { [ "${RINGFALL_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
|
||||
unattended() { [ "${MJOLNIR_UNATTENDED:-0}" = 1 ] || ! command -v zenity >/dev/null; }
|
||||
|
||||
info()
|
||||
{
|
||||
@@ -185,7 +188,7 @@ PYTHON
|
||||
# must run), from a desktop file beside the game; then its pictures
|
||||
add_shortcut()
|
||||
{
|
||||
local desktop="$GAME_DIR/$NAME_FILE.desktop" url file id
|
||||
local desktop="$GAME_DIR/$NAME_FILE.desktop" url
|
||||
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=%s\nPath=%s\nIcon=steam\nTerminal=false\n' \
|
||||
"$NAME" "$GAME_DIR/$LAUNCHER" "$GAME_DIR" > "$desktop" && chmod 755 "$desktop" || return 1
|
||||
url="steam://addnonsteamgame/$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$desktop")"
|
||||
@@ -195,6 +198,16 @@ add_shortcut()
|
||||
steam "$url" >/dev/null 2>&1 &
|
||||
for _ in $(seq 30); do have_shortcut && break; sleep 1; done
|
||||
have_shortcut || return 1
|
||||
add_pictures
|
||||
return 0
|
||||
}
|
||||
|
||||
# the library pictures of the game's shortcut, for every Steam user who has
|
||||
# it (again on an update: the builds before the name "Mjolnir VR" had
|
||||
# Ringfall's)
|
||||
add_pictures()
|
||||
{
|
||||
local file id
|
||||
while read -r file; do
|
||||
id="$(shortcut_id "$file" "$EXE")" || continue
|
||||
log "shortcut $id in $file"
|
||||
@@ -204,7 +217,6 @@ add_shortcut()
|
||||
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
|
||||
@@ -245,7 +257,7 @@ rm -rf "$GAME_DIR/update"
|
||||
# --- the game data
|
||||
|
||||
if [ ! -f "$GAME_DIR/maps/ui.map" ]; then
|
||||
image="${RINGFALL_IMAGE:-}"
|
||||
image="${MJOLNIR_IMAGE:-}"
|
||||
if [ -z "$image" ]; then
|
||||
mapfile -t images < <(find_images)
|
||||
for candidate in "${images[@]}"; do
|
||||
@@ -271,6 +283,7 @@ fi
|
||||
|
||||
if have_shortcut; then
|
||||
log "the Steam shortcut is there"
|
||||
add_pictures
|
||||
elif unattended; then
|
||||
echo "info: the Steam shortcut is missing (not added unattended)"
|
||||
elif ! steam_running; then
|
||||
@@ -286,6 +299,9 @@ fi
|
||||
if have_shortcut_to "$OLD_DIR/halo-vr.sh"; then
|
||||
info "The library still shows the old \"$OLD_NAME\" from before the game's new name. It no longer starts: remove it in Steam (its gear or Manage > Remove non-Steam game)."
|
||||
fi
|
||||
if have_shortcut_to "$FORMER_NAME"; then
|
||||
info "The library shows the game by its former name, \"$FORMER_NAME\". To call it $NAME there: its gear > Properties, then its name at the top."
|
||||
fi
|
||||
if ask "$NAME is installed.${image_note:-}\n\nSwitch to Gaming Mode now? Start $NAME from your library there (its pictures may show once Steam starts again)." yes; then
|
||||
steamos-session-select gamescope >/dev/null 2>&1 ||
|
||||
info "Switch to Gaming Mode yourself (the \"Return to Gaming Mode\" icon or the power menu)."
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
[Desktop Entry]
|
||||
# Ringfall: Mjolnir VR's installer for the Steam Frame
|
||||
# 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
|
||||
# "Install-Mjolnir-VR.desktop", opened in Desktop Mode. It downloads
|
||||
# the latest release's bundle (tools/steam_frame_bundle.py, which fills in
|
||||
# the repository) and runs its installer, steam_frame_install.sh.
|
||||
# (no "$", "\" or "%" in Exec: the desktop entry format would need them
|
||||
# escaped)
|
||||
Type=Application
|
||||
Name=Install Ringfall: Mjolnir VR
|
||||
Comment=Installs Ringfall: Mjolnir VR for the Steam Frame, or updates it
|
||||
Name=Install Mjolnir VR
|
||||
Comment=Installs Mjolnir VR for the Steam Frame, or updates it
|
||||
Icon=steam
|
||||
Terminal=false
|
||||
Exec=bash -c "if curl -fL --retry 3 -o /tmp/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"
|
||||
Exec=bash -c "if curl -fL --retry 3 -o /tmp/mjolnir-vr-steam-frame.zip https://github.com/@REPOSITORY@/releases/latest/download/mjolnir-vr-steam-frame.zip && rm -rf /tmp/mjolnir-vr-install && python3 -m zipfile -e /tmp/mjolnir-vr-steam-frame.zip /tmp/mjolnir-vr-install; then exec bash /tmp/mjolnir-vr-install/install.sh; else zenity --error --width=460 --title='Mjolnir VR' --text='Mjolnir VR could not be downloaded. Is the Steam Frame online?'; fi"
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Ringfall: Mjolnir VR on the Steam Frame: Lepton runs the APK in a container of its own,
|
||||
# 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
|
||||
@@ -121,7 +121,7 @@ update_check()
|
||||
log "update: build $latest found (this is $installed), fetching it"
|
||||
rm -rf "$UPDATE" && mkdir -p "$UPDATE" || return 0
|
||||
if curl -fsSL --retry 2 --max-time 900 -o "$UPDATE/bundle.zip" \
|
||||
"https://github.com/$REPOSITORY/releases/download/build-$latest/ringfall-mjolnir-vr-steam-frame.zip" &&
|
||||
"https://github.com/$REPOSITORY/releases/download/build-$latest/mjolnir-vr-steam-frame.zip" &&
|
||||
python3 -I -m zipfile -e "$UPDATE/bundle.zip" "$UPDATE/bundle" &&
|
||||
[ "$(cat "$UPDATE/bundle/build.txt" 2>/dev/null)" = "$latest" ] &&
|
||||
[ -s "$UPDATE/bundle/RingfallMjolnirVR.apk" ] && [ -s "$UPDATE/bundle/ringfall-mjolnir-vr.sh" ]
|
||||
|
||||
@@ -42,7 +42,7 @@ name that no file before it had (ignoring case).
|
||||
|
||||
A European disc in two languages (Spanish and Italian) has no maps folder
|
||||
but one for each, maps_es and maps_it: the one for the language asked for
|
||||
(RINGFALL_LANGUAGE, else the system's) is taken, else English's, else the
|
||||
(MJOLNIR_LANGUAGE, else the system's) is taken, else English's, else the
|
||||
first, and its language written to maps/language.txt, which the game takes
|
||||
as the Xbox's language while game.language is unset.
|
||||
"""
|
||||
@@ -161,7 +161,7 @@ def choose_maps(folders):
|
||||
if folder[0].lower() == "maps":
|
||||
return folder, None
|
||||
languages = [(maps_language(folder[0]), folder) for folder in folders if maps_language(folder[0])]
|
||||
wanted = (os.environ.get("RINGFALL_LANGUAGE") or os.environ.get("LC_ALL") or
|
||||
wanted = (os.environ.get("MJOLNIR_LANGUAGE") or os.environ.get("LC_ALL") or
|
||||
os.environ.get("LC_MESSAGES") or os.environ.get("LANG") or "")[:2].lower()
|
||||
for preferred in (wanted, "en"):
|
||||
for language, folder in languages:
|
||||
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
@@ -47,6 +47,11 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
|
||||
host's is once that stops, or if they drift apart while it moves;
|
||||
- which named objects exist, so scripted creates and deletes match.
|
||||
|
||||
Script actions naming player0 also reach the players beyond player1.
|
||||
They use the same reverse datum order as the script's `(players)` list,
|
||||
skipping players without units, so a scripted BSP transition does not
|
||||
leave the extra players in the unloaded part of the level.
|
||||
|
||||
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'
|
||||
|
||||
@@ -263,7 +263,9 @@ second get a numeric suffix so previous screenshots are preserved.
|
||||
One movement of the mouse wheel changes the weapon one time. A second
|
||||
movement after a short pause changes it again.
|
||||
|
||||
In the menus, the mouse moves a pointer:
|
||||
In the menus, the mouse moves a pointer, a blue arrow
|
||||
(`port/assets/cursor/menu_cursor.png`) in place of the system's, sized to
|
||||
the window as Halo PC's was (32 of the menus' 480 lines tall):
|
||||
|
||||
- The item below the pointer gets the focus.
|
||||
- A left click selects the item. On a setting with values, a click on the
|
||||
|
||||
@@ -103,33 +103,11 @@ boolean coop_scripts_any_player_will_do(
|
||||
return any;
|
||||
}
|
||||
|
||||
void coop_scripts_gather_in_volume(
|
||||
short trigger_volume_index,
|
||||
boolean coop_scripts_empty_player_list(
|
||||
long object_list_index)
|
||||
{
|
||||
long reference_index;
|
||||
long object_index;
|
||||
long inside_index = NONE;
|
||||
|
||||
for (object_index = object_list_get_first(object_list_index, &reference_index);
|
||||
object_index != NONE && inside_index == NONE;
|
||||
object_index = object_list_get_next(object_list_index, &reference_index))
|
||||
{
|
||||
if (scenario_trigger_volume_test_object(trigger_volume_index, object_index))
|
||||
inside_index = object_index;
|
||||
}
|
||||
if (inside_index == NONE)
|
||||
return;
|
||||
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 (!scenario_trigger_volume_test_object(trigger_volume_index, object_index))
|
||||
{
|
||||
player_teleport(player_index_from_unit_index(object_index), inside_index,
|
||||
&object_get(inside_index)->object.bounding_sphere_center);
|
||||
}
|
||||
}
|
||||
return coop_scripts_host() && (object_list_index == NONE ||
|
||||
(object_list_is_players(object_list_index) && object_list_count(object_list_index) == 0));
|
||||
}
|
||||
|
||||
/* the units of the players following player0, if `unit_index` is
|
||||
@@ -141,22 +119,29 @@ static short players_following(
|
||||
{
|
||||
struct data_iterator iterator;
|
||||
struct player_datum *player;
|
||||
short position = 0;
|
||||
long players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
|
||||
short player_count = 0;
|
||||
short position;
|
||||
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) */
|
||||
/* hs_players adds each unit to the head of a reference list. Its
|
||||
player0 is the LAST live unit in datum order, not the first. Keep that
|
||||
same order here, including gaps left by dead or disconnected players. */
|
||||
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++;
|
||||
if (player->unit_index != NONE && player_count < NUMBEROF(players))
|
||||
players[player_count++] = player->unit_index;
|
||||
}
|
||||
if (player_count == 0 || players[player_count - 1] != unit_index)
|
||||
return 0;
|
||||
for (position = player_count - NAMED_PLAYERS - 1;
|
||||
position >= 0 && count < maximum_count;
|
||||
position--)
|
||||
{
|
||||
unit_indices[count++] = players[position];
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -221,6 +206,70 @@ static boolean teleport_to(
|
||||
return arrived;
|
||||
}
|
||||
|
||||
/* A placement may fail after moving the unit, or find free ground outside
|
||||
this volume. Restore that unit before retrying; never move the loaded BSP.
|
||||
The second attempt uses the same crowd fallback as scripted teleports. */
|
||||
static void gather_player_in_volume(
|
||||
short trigger_volume_index,
|
||||
long unit_index,
|
||||
long anchor_index,
|
||||
real_point3d const *destination)
|
||||
{
|
||||
long player_index = player_index_from_unit_index(unit_index);
|
||||
real_point3d original;
|
||||
real_point3d placed;
|
||||
short attempt;
|
||||
|
||||
if (player_index == NONE)
|
||||
return;
|
||||
object_get_origin(unit_index, &original);
|
||||
for (attempt = 0; attempt < 2; attempt++)
|
||||
{
|
||||
boolean arrived = teleport_to(player_index, anchor_index, destination, attempt != 0);
|
||||
|
||||
if (arrived && scenario_trigger_volume_test_object(trigger_volume_index, unit_index) &&
|
||||
!player_position_switches_bsp(object_get_origin(unit_index, &placed)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
/* This was the unit's valid position at entry, on the same tick. */
|
||||
object_set_position(unit_index, &original, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void coop_scripts_gather_in_volume(
|
||||
short trigger_volume_index,
|
||||
long object_list_index)
|
||||
{
|
||||
long reference_index;
|
||||
long object_index;
|
||||
long inside_index = NONE;
|
||||
real_point3d destination;
|
||||
|
||||
if (!coop_scripts_host())
|
||||
return;
|
||||
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 (scenario_trigger_volume_test_object(trigger_volume_index, object_index) &&
|
||||
!player_position_switches_bsp(object_get_origin(object_index, &destination)))
|
||||
{
|
||||
inside_index = object_index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inside_index == NONE)
|
||||
return;
|
||||
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 (!scenario_trigger_volume_test_object(trigger_volume_index, object_index))
|
||||
gather_player_in_volume(trigger_volume_index, object_index, inside_index, &destination);
|
||||
}
|
||||
}
|
||||
|
||||
boolean coop_scripts_unit_near(
|
||||
long unit_index,
|
||||
real_point3d const *destination)
|
||||
|
||||
@@ -16,9 +16,14 @@ Campaign scripts made to work for every player in network co-op
|
||||
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);
|
||||
/* An empty (players) result or failed list allocation cannot advance a
|
||||
co-op gate. Does not change valid empty AI lists or non-host behavior. */
|
||||
boolean coop_scripts_empty_player_list(
|
||||
long object_list_index);
|
||||
/* and when a script waits on it and one player is inside, the players of
|
||||
the list outside come beside that one, so a gate (the Maw's lift, the run
|
||||
to its bridge, a30's Pelicans) leaves no one behind */
|
||||
to its bridge, a30's Pelicans) leaves no one behind. The caller must test
|
||||
the volume again: a failed placement leaves the script waiting. */
|
||||
void coop_scripts_gather_in_volume(
|
||||
short trigger_volume_index,
|
||||
long object_list_index);
|
||||
|
||||
@@ -634,6 +634,9 @@ vertices. */
|
||||
|
||||
#ifndef HALO_GLES
|
||||
#define VISIBILITY_BATCH_COUNT 3
|
||||
/* what a test's place in the buffer holds until the GPU copies its count
|
||||
there: more than any test covers (8K with 4x MSAA has 133 million samples) */
|
||||
#define VISIBILITY_UNCOPIED 0xFFFFFFFFUL
|
||||
/* a frame's tests: their queries (kept until every count is read), the slot
|
||||
and area of each, and (once the frame is presented) the fence its copies
|
||||
wait on */
|
||||
@@ -2290,7 +2293,7 @@ static void hdr_read(void)
|
||||
exposure = config_real("display.hdr_exposure");
|
||||
wanted = config_boolean("display.hdr");
|
||||
#ifdef HALO_ANDROID
|
||||
/* (port, Ringfall: never in VR, whose eyes' pictures go to the headset
|
||||
/* (port, Mjolnir VR: never in VR, whose eyes' pictures go to the headset
|
||||
as they are, and whose GPU time is short already) */
|
||||
if (halo_vr_active())
|
||||
wanted = FALSE;
|
||||
@@ -3082,7 +3085,7 @@ static void visibility_results_initialize(void)
|
||||
{
|
||||
const char *vendor = (const char *)glGetString(GL_VENDOR);
|
||||
GLsizeiptr size = VISIBILITY_BATCH_COUNT * VISIBILITY_TEST_SLOTS * sizeof(GLuint);
|
||||
GLbitfield flags = GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
|
||||
GLbitfield flags = GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
|
||||
BOOL success;
|
||||
|
||||
device.visibility_logging = config_boolean("debug.gl_debug");
|
||||
@@ -7389,20 +7392,90 @@ void d3d8_clear_back_buffer_region(float left, float top, float right, float bot
|
||||
}
|
||||
|
||||
#ifndef HALO_GLES
|
||||
/* the counts read from each query, as with AMD's driver, once a batch's
|
||||
copies into the buffer have failed: a batch that fails is not used again,
|
||||
and with the buffer the readback would stop once all of them had. The
|
||||
batches still waiting keep their queries and are read from them. */
|
||||
static void visibility_fall_back_to_queries(void)
|
||||
{
|
||||
unsigned long i;
|
||||
|
||||
for (i = 0; i < VISIBILITY_BATCH_COUNT; i++)
|
||||
{
|
||||
if (device.visibility_batches[i].fence)
|
||||
glDeleteSync(device.visibility_batches[i].fence);
|
||||
device.visibility_batches[i].fence = NULL;
|
||||
}
|
||||
device.visibility_mapped = NULL;
|
||||
platform_log("GPU: visibility test counts read from each query from now on");
|
||||
}
|
||||
|
||||
/* this frame's batch sent, before the swap. A copy into the buffer right
|
||||
after each test stopped the GPU until that test's draws were through,
|
||||
hundreds of times a frame in The Library; the GPU copies a batch's counts
|
||||
when its frame is presented, over the CPU's VISIBILITY_UNCOPIED, with a
|
||||
fence after the copies. Nothing here waits for an answer from the driver:
|
||||
with a threaded driver (NVIDIA's) any such call waits until its thread has
|
||||
carried out the whole frame queued before it, and here, before the swap,
|
||||
that held the frame back every time. GL errors are asked for with
|
||||
debug.gl_debug; the swap, next, sends the commands to the GPU. */
|
||||
static void visibility_send_batch(void)
|
||||
{
|
||||
struct visibility_batch *batch;
|
||||
unsigned long i;
|
||||
|
||||
if (device.visibility_unavailable || device.visibility_batch >= VISIBILITY_BATCH_COUNT)
|
||||
return;
|
||||
batch = &device.visibility_batches[device.visibility_batch];
|
||||
if (!batch->count)
|
||||
return;
|
||||
if (device.visibility_mapped)
|
||||
{
|
||||
volatile GLuint *counts = device.visibility_mapped + device.visibility_batch * VISIBILITY_TEST_SLOTS;
|
||||
|
||||
if (device.visibility_logging)
|
||||
visibility_readback_gl_ok("before query copies");
|
||||
/* (the coherent mapping makes these writes visible to the copies
|
||||
sent after them; a place that still holds VISIBILITY_UNCOPIED
|
||||
once the fence has passed is a copy that failed) */
|
||||
for (i = 0; i < batch->count; i++)
|
||||
counts[i] = VISIBILITY_UNCOPIED;
|
||||
for (i = 0; i < batch->count; i++)
|
||||
glGetQueryBufferObjectuiv(batch->queries[i], device.visibility_buffer, GL_QUERY_RESULT,
|
||||
(GLintptr)((device.visibility_batch * VISIBILITY_TEST_SLOTS + i) * sizeof(GLuint)));
|
||||
batch->fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (!batch->fence || (device.visibility_logging && !visibility_readback_gl_ok("query copies")))
|
||||
{
|
||||
batch->failed = TRUE;
|
||||
device.visibility_stats.failures++;
|
||||
if (batch->fence)
|
||||
glDeleteSync(batch->fence);
|
||||
batch->fence = NULL;
|
||||
platform_log("GPU: visibility test batch %lu dropped: its counts could not be copied",
|
||||
device.visibility_batch);
|
||||
visibility_fall_back_to_queries();
|
||||
}
|
||||
}
|
||||
batch->sealed = TRUE;
|
||||
batch->serial = ++device.visibility_serial;
|
||||
batch->frame = device.frame;
|
||||
device.visibility_batch = VISIBILITY_BATCH_COUNT;
|
||||
device.visibility_stats.submitted++;
|
||||
}
|
||||
|
||||
/* the counts of earlier frames' tests, read into their slots oldest batch
|
||||
first, and this frame's batch sent. A copy into the buffer right after each
|
||||
test stopped the GPU until that test's draws were through, hundreds of times
|
||||
a frame in The Library; the GPU copies a batch's counts when its frame is
|
||||
presented, and the CPU reads them once the fence after the copies has
|
||||
passed, a frame or more later. With AMD's driver the CPU reads each query
|
||||
once it says it is available. A batch is not reused until all its counts
|
||||
have been read. */
|
||||
first once the fence after their copies has passed, after the swap, when
|
||||
the driver has the least queued before the question; with AMD's driver the
|
||||
CPU reads each query once it says it is available. A batch is not reused
|
||||
until all its counts have been read; one the GPU is late with is waited for
|
||||
at the next presents, however long (its slots keep their earlier counts
|
||||
meanwhile, and with every batch waiting, tests are dropped). */
|
||||
static void visibility_read_batch(void)
|
||||
{
|
||||
struct visibility_batch *batch;
|
||||
unsigned long i, oldest, pending = 0, oldest_age = 0;
|
||||
GLint binding = 0;
|
||||
BOOL bound = TRUE;
|
||||
BOOL bound = TRUE, fall_back = FALSE;
|
||||
|
||||
if (device.visibility_unavailable)
|
||||
return;
|
||||
@@ -7430,17 +7503,21 @@ static void visibility_read_batch(void)
|
||||
if (oldest == VISIBILITY_BATCH_COUNT)
|
||||
break;
|
||||
batch = &device.visibility_batches[oldest];
|
||||
if (device.visibility_mapped)
|
||||
/* (the batch this present sent: looked for from the next one) */
|
||||
if (batch->frame == device.frame)
|
||||
break;
|
||||
if (device.visibility_mapped && batch->fence)
|
||||
{
|
||||
GLenum status = glClientWaitSync(batch->fence, 0, 0);
|
||||
BOOL success = visibility_readback_gl_ok("copy completion");
|
||||
/* (the flush makes sure the fence reaches the GPU without
|
||||
counting on the swap's; after the swap it has nothing to send) */
|
||||
GLenum status = glClientWaitSync(batch->fence, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
|
||||
|
||||
if (success && status == GL_TIMEOUT_EXPIRED)
|
||||
if (status == GL_TIMEOUT_EXPIRED)
|
||||
{
|
||||
device.visibility_stats.unavailable++;
|
||||
break;
|
||||
}
|
||||
if (!success || (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED))
|
||||
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)
|
||||
{
|
||||
batch->failed = TRUE;
|
||||
device.visibility_stats.failures++;
|
||||
@@ -7449,7 +7526,10 @@ static void visibility_read_batch(void)
|
||||
glDeleteSync(batch->fence);
|
||||
batch->fence = NULL;
|
||||
if (batch->failed)
|
||||
continue;
|
||||
{
|
||||
fall_back = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (batch->read < batch->count)
|
||||
{
|
||||
@@ -7458,7 +7538,15 @@ static void visibility_read_batch(void)
|
||||
float area = batch->areas[test];
|
||||
|
||||
if (device.visibility_mapped)
|
||||
{
|
||||
samples = device.visibility_mapped[oldest * VISIBILITY_TEST_SLOTS + test];
|
||||
if (samples == VISIBILITY_UNCOPIED)
|
||||
{
|
||||
batch->failed = TRUE;
|
||||
device.visibility_stats.failures++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
glGetQueryObjectuiv(batch->queries[test], GL_QUERY_RESULT_AVAILABLE, &available);
|
||||
@@ -7491,7 +7579,12 @@ static void visibility_read_batch(void)
|
||||
{
|
||||
/* (its slots keep their earlier counts, and its queries, whose
|
||||
state is unknown, are not used again) */
|
||||
platform_log("GPU: visibility test batch %lu dropped: its queries could not be read", oldest);
|
||||
platform_log("GPU: visibility test batch %lu dropped: its counts could not be read", oldest);
|
||||
if (device.visibility_mapped)
|
||||
{
|
||||
fall_back = TRUE;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (batch->read < batch->count)
|
||||
@@ -7505,42 +7598,10 @@ static void visibility_read_batch(void)
|
||||
glBindBuffer(GL_QUERY_BUFFER, (GLuint)binding);
|
||||
visibility_readback_gl_ok("restore query buffer");
|
||||
}
|
||||
if (device.visibility_batch < VISIBILITY_BATCH_COUNT)
|
||||
{
|
||||
batch = &device.visibility_batches[device.visibility_batch];
|
||||
if (batch->count)
|
||||
{
|
||||
if (device.visibility_mapped)
|
||||
{
|
||||
BOOL success;
|
||||
|
||||
visibility_readback_gl_ok("before query copies");
|
||||
for (i = 0; i < batch->count; i++)
|
||||
glGetQueryBufferObjectuiv(batch->queries[i], device.visibility_buffer, GL_QUERY_RESULT,
|
||||
(GLintptr)((device.visibility_batch * VISIBILITY_TEST_SLOTS + i) * sizeof(GLuint)));
|
||||
batch->fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
success = visibility_readback_gl_ok("query copies");
|
||||
if (!success || !batch->fence)
|
||||
{
|
||||
batch->failed = TRUE;
|
||||
device.visibility_stats.failures++;
|
||||
if (batch->fence)
|
||||
glDeleteSync(batch->fence);
|
||||
batch->fence = NULL;
|
||||
platform_log("GPU: visibility test batch %lu dropped: its counts could not be copied",
|
||||
device.visibility_batch);
|
||||
}
|
||||
}
|
||||
batch->sealed = TRUE;
|
||||
batch->serial = ++device.visibility_serial;
|
||||
batch->frame = device.frame;
|
||||
device.visibility_batch = VISIBILITY_BATCH_COUNT;
|
||||
device.visibility_stats.submitted++;
|
||||
/* (sent to the GPU now, whether or not presenting flushes: the
|
||||
counts are first looked for at the next present) */
|
||||
glFlush();
|
||||
}
|
||||
}
|
||||
/* (after the reads from the buffer: those from each query unbind it
|
||||
first, from the next present) */
|
||||
if (fall_back)
|
||||
visibility_fall_back_to_queries();
|
||||
for (i = 0; i < VISIBILITY_BATCH_COUNT; i++)
|
||||
{
|
||||
batch = &device.visibility_batches[i];
|
||||
@@ -7683,7 +7744,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
|
||||
back_buffer->target.texture);
|
||||
render_target_resolve(&back_buffer->target);
|
||||
#ifndef HALO_GLES
|
||||
visibility_read_batch();
|
||||
visibility_send_batch();
|
||||
#endif
|
||||
if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0)
|
||||
write_screenshot(back_buffer);
|
||||
@@ -7732,6 +7793,9 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
|
||||
glBindVertexArray(device.vertex_array);
|
||||
platform_video_swap();
|
||||
}
|
||||
#ifndef HALO_GLES
|
||||
visibility_read_batch();
|
||||
#endif
|
||||
xgpu_gl_state_invalidate();
|
||||
memory_watch_begin_frame();
|
||||
xgpu_texture_cache_begin_frame();
|
||||
|
||||
@@ -564,7 +564,7 @@ unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned
|
||||
return png_pixels_texture(pixels, width, height, levels);
|
||||
}
|
||||
|
||||
/* (port, Ringfall: the same in two steps, the decode on any thread:
|
||||
/* (port, Mjolnir VR: the same in two steps, the decode on any thread:
|
||||
menu_files.c's menu pictures) */
|
||||
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height)
|
||||
{
|
||||
|
||||
@@ -648,7 +648,7 @@ static const struct config_setting config_settings[] =
|
||||
"as CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER. Their tags are checked\n"
|
||||
"as the game's own maps' are before they run; false refuses them\n"
|
||||
"(docs/custom_edition_caches.md)." },
|
||||
/* (the desktop's local bots, bots.c; Ringfall's host bots have their own
|
||||
/* (the desktop's local bots, bots.c; Mjolnir VR's host bots have their own
|
||||
bots.count and bots.skill, Android's, below: host_bots.c) */
|
||||
{ "bots.count", _config_integer, "0", "HALO_BOTS", _environment_value, _platform_desktop,
|
||||
"Computer players (bots) in a local (split screen) Slayer game: 0 to 15\n"
|
||||
|
||||
@@ -117,6 +117,9 @@ static void screen_keyboard_update(void);
|
||||
/* the windows' icon, a PNG (tools/embed_assets.py, from port/assets/icon) */
|
||||
extern const unsigned int platform_window_icon[];
|
||||
extern const unsigned long platform_window_icon_size;
|
||||
/* the menus' pointer, a PNG (tools/embed_assets.py, from port/assets/cursor) */
|
||||
extern const unsigned int platform_menu_cursor[];
|
||||
extern const unsigned long platform_menu_cursor_size;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1005,6 +1008,116 @@ BOOL platform_video_hdr(float *sdr_white, float *headroom)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef HALO_ANDROID
|
||||
#ifndef HALO_GLES
|
||||
/* ---------- the menus' cursor
|
||||
|
||||
The blue arrow the menus show for the mouse, in place of the system's. Halo
|
||||
PC drew its cursor in the menus' 640 x 480, so it took the same part of the
|
||||
screen at any resolution; this one is made again whenever the window's size
|
||||
in pixels changes (menu_cursor_fit, from platform_pump_events). */
|
||||
|
||||
/* the arrow's height in the menus' 480 lines (72 pixels at 1080p) */
|
||||
#define MENU_CURSOR_HEIGHT 32
|
||||
/* the arrow's tip in the picture, past its soft edge: the click point */
|
||||
#define MENU_CURSOR_TIP 2
|
||||
|
||||
static SDL_Surface *menu_cursor_picture;
|
||||
static SDL_Cursor *menu_cursor;
|
||||
static int menu_cursor_height;
|
||||
|
||||
/* the picture at a height, halved first while it is twice that or more (one
|
||||
linear scaling to less than half the size skips pixels) */
|
||||
static SDL_Surface *menu_cursor_scale(int height)
|
||||
{
|
||||
int width = (menu_cursor_picture->w * height + menu_cursor_picture->h / 2) / menu_cursor_picture->h;
|
||||
SDL_Surface *surface = SDL_DuplicateSurface(menu_cursor_picture);
|
||||
|
||||
while (surface && surface->h / 2 >= height)
|
||||
{
|
||||
SDL_Surface *half = SDL_ScaleSurface(surface, surface->w / 2, surface->h / 2, SDL_SCALEMODE_LINEAR);
|
||||
|
||||
SDL_DestroySurface(surface);
|
||||
surface = half;
|
||||
}
|
||||
if (surface && surface->h != height)
|
||||
{
|
||||
SDL_Surface *sized = SDL_ScaleSurface(surface, width > 0 ? width : 1, height, SDL_SCALEMODE_LINEAR);
|
||||
|
||||
SDL_DestroySurface(surface);
|
||||
surface = sized;
|
||||
}
|
||||
return surface;
|
||||
}
|
||||
|
||||
/* the cursor made for the window's size, when that has changed: its height
|
||||
the part of the window the menus' 480 lines take (their 4:3 fits a window of
|
||||
another shape), in pixels; where the desktop counts in points (macOS, Wayland
|
||||
at a scale) the cursor is that size in points, with the pixels' image as its
|
||||
alternate */
|
||||
static void menu_cursor_fit(void)
|
||||
{
|
||||
int width = 0, height = 0, lines, cursor_height;
|
||||
float density;
|
||||
SDL_Surface *pixels, *points;
|
||||
SDL_Cursor *cursor = NULL;
|
||||
|
||||
if (!menu_cursor_picture || !SDL_GetWindowSizeInPixels(platform_window, &width, &height) ||
|
||||
width <= 0 || height <= 0)
|
||||
return;
|
||||
lines = height < width * 3 / 4 ? height : width * 3 / 4;
|
||||
cursor_height = lines * MENU_CURSOR_HEIGHT / 480;
|
||||
if (cursor_height < 16)
|
||||
cursor_height = 16;
|
||||
if (cursor_height == menu_cursor_height)
|
||||
return;
|
||||
menu_cursor_height = cursor_height;
|
||||
density = SDL_GetWindowPixelDensity(platform_window);
|
||||
pixels = menu_cursor_scale(cursor_height);
|
||||
points = density > 1.0f ? menu_cursor_scale((int)(cursor_height / density + 0.5f)) : pixels;
|
||||
if (pixels && points && (points == pixels || SDL_AddSurfaceAlternateImage(points, pixels)))
|
||||
cursor = SDL_CreateColorCursor(points,
|
||||
(MENU_CURSOR_TIP * points->w + menu_cursor_picture->w / 2) / menu_cursor_picture->w,
|
||||
(MENU_CURSOR_TIP * points->h + menu_cursor_picture->h / 2) / menu_cursor_picture->h);
|
||||
if (cursor && SDL_SetCursor(cursor))
|
||||
{
|
||||
SDL_DestroyCursor(menu_cursor);
|
||||
menu_cursor = cursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
platform_log("cannot set the menus' cursor: %s", SDL_GetError());
|
||||
if (cursor)
|
||||
SDL_DestroyCursor(cursor);
|
||||
}
|
||||
if (points != pixels)
|
||||
SDL_DestroySurface(points);
|
||||
SDL_DestroySurface(pixels);
|
||||
}
|
||||
|
||||
/* A controller's buttons or sticks, used while a menu is up, hide the
|
||||
cursor, and the mouse moved or clicked shows it again, so a player on a
|
||||
controller has no arrow left over the menus; a menu opened while the
|
||||
controller was used last opens without it
|
||||
(platform_ui_pointer_set_active). */
|
||||
static BOOL menu_cursor_controller;
|
||||
|
||||
static void menu_cursor_controller_used(BOOL in_menus)
|
||||
{
|
||||
menu_cursor_controller = TRUE;
|
||||
if (in_menus && SDL_CursorVisible())
|
||||
SDL_HideCursor();
|
||||
}
|
||||
|
||||
static void menu_cursor_mouse_used(BOOL in_menus)
|
||||
{
|
||||
menu_cursor_controller = FALSE;
|
||||
if (in_menus && !SDL_CursorVisible())
|
||||
SDL_ShowCursor();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
BOOL platform_video_initialize(unsigned long width, unsigned long height)
|
||||
{
|
||||
int version;
|
||||
@@ -1097,6 +1210,16 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
|
||||
platform_log("cannot set the window's icon: %s", SDL_GetError());
|
||||
SDL_DestroySurface(icon);
|
||||
}
|
||||
/* the pointer the menus show (platform_ui_pointer_set_active), in place
|
||||
of the system's, at the window's size */
|
||||
if (platform_menu_cursor_size && !menu_cursor_picture)
|
||||
{
|
||||
menu_cursor_picture = SDL_LoadPNG_IO(SDL_IOFromConstMem(platform_menu_cursor, platform_menu_cursor_size), true);
|
||||
if (!menu_cursor_picture)
|
||||
platform_log("cannot read the menus' cursor: %s", SDL_GetError());
|
||||
}
|
||||
menu_cursor_height = 0;
|
||||
menu_cursor_fit();
|
||||
#endif
|
||||
platform_fullscreen_requested = platform_fullscreen_setting();
|
||||
platform_window_mode = platform_display_mode();
|
||||
@@ -1934,6 +2057,9 @@ void platform_pump_events(void)
|
||||
halo_vr_frame_begin();
|
||||
exit_after_check();
|
||||
platform_show_pending_message();
|
||||
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
|
||||
menu_cursor_fit();
|
||||
#endif
|
||||
#ifndef HALO_ANDROID
|
||||
updater_poll(platform_window);
|
||||
/* (Settings > Audio's output device, as it changes: dsound_sdl.c) */
|
||||
@@ -1999,6 +2125,13 @@ void platform_pump_events(void)
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
#ifndef HALO_ANDROID
|
||||
#ifndef HALO_GLES
|
||||
/* (not the warps: to the middle as a menu opens, and back to
|
||||
where the mouse is held without relative mode) */
|
||||
if (input_state.ui_pointer ? event.motion.x != ui_pointer.x || event.motion.y != ui_pointer.y :
|
||||
!mouse_warp_capture || (int)event.motion.x != mouse_warp_x || (int)event.motion.y != mouse_warp_y)
|
||||
menu_cursor_mouse_used(input_state.ui_pointer);
|
||||
#endif
|
||||
if (scoreboard_pointer_active)
|
||||
{
|
||||
scoreboard_pointer.x = event.motion.x;
|
||||
@@ -2028,6 +2161,10 @@ void platform_pump_events(void)
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
|
||||
if (event.button.down)
|
||||
menu_cursor_mouse_used(input_state.ui_pointer);
|
||||
#endif
|
||||
if (event.button.button < 32)
|
||||
{
|
||||
if (event.button.down)
|
||||
@@ -2197,6 +2334,17 @@ void platform_pump_events(void)
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
menu_cursor_controller_used(input_state.ui_pointer);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
/* (past a stick's or trigger's rest, as gamepad_idle in
|
||||
xinput_sdl.c has it) */
|
||||
if (event.gaxis.value > 8000 || event.gaxis.value < -8000)
|
||||
menu_cursor_controller_used(input_state.ui_pointer);
|
||||
break;
|
||||
#endif
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
/* (SDL keeps a gamepad open until it is closed, even once the
|
||||
controller has gone) */
|
||||
@@ -2284,6 +2432,10 @@ void platform_ui_pointer_set_active(BOOL active)
|
||||
memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed));
|
||||
pthread_mutex_unlock(&input_lock);
|
||||
platform_mouse_capture(!active && !input_state.mouse_released);
|
||||
#ifndef HALO_GLES
|
||||
if (active && menu_cursor_controller)
|
||||
SDL_HideCursor();
|
||||
#endif
|
||||
if (active)
|
||||
{
|
||||
int width, height;
|
||||
|
||||
@@ -47,7 +47,7 @@ update.h's: posix_update.c on Linux, win32_update.c on Windows.
|
||||
#endif
|
||||
|
||||
/* the VR port's own releases (the original's must not replace it) */
|
||||
#define UPDATE_REPOSITORY "Cesarus85/ringfall-mjolnir-vr"
|
||||
#define UPDATE_REPOSITORY "Cesarus85/mjolnir-vr"
|
||||
#ifdef _WIN32
|
||||
#define UPDATE_PLATFORM "windows"
|
||||
#define PATH_SEPARATOR "\\"
|
||||
|
||||
@@ -964,6 +964,22 @@ static unsigned long palette_hash(const D3DCOLOR *palette)
|
||||
return hash ? hash : 1;
|
||||
}
|
||||
|
||||
/* display.high_res_hud, read again only when a setting changes: it is asked
|
||||
at every bind, and config_boolean takes the settings' lock and looks the name
|
||||
up among them all */
|
||||
static BOOL high_res_hud(void)
|
||||
{
|
||||
static unsigned long read_at = (unsigned long)-1;
|
||||
static BOOL enabled;
|
||||
|
||||
if (read_at != config_changes())
|
||||
{
|
||||
read_at = config_changes();
|
||||
enabled = config_boolean("display.high_res_hud") != 0;
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/* an entry's GL texture and description: its replacement's, if it has one,
|
||||
with the bitmap's own size (which its coordinates are in) */
|
||||
static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target,
|
||||
@@ -1018,7 +1034,7 @@ static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target,
|
||||
}
|
||||
/* These sheets keep the map's art and coordinates. Decode through the
|
||||
* normal format reader; the HUD module owns only the interpolation. */
|
||||
if (config_boolean("display.high_res_hud"))
|
||||
if (high_res_hud())
|
||||
{
|
||||
if (!entry->hud_upscale_checked)
|
||||
{
|
||||
|
||||
@@ -201,6 +201,8 @@ long hs_players(
|
||||
if (player->unit_index != NONE)
|
||||
object_list_add(object_list_index, player->unit_index);
|
||||
}
|
||||
/* port: even an empty (players) list must not open a co-op gate. */
|
||||
object_list_mark_players(object_list_index);
|
||||
|
||||
return object_list_index;
|
||||
}
|
||||
@@ -894,14 +896,24 @@ boolean hs_trigger_volume_test_objects_all(
|
||||
long object_list_index)
|
||||
{
|
||||
boolean inside;
|
||||
boolean waiting = hs_runtime_waiting_on_call();
|
||||
|
||||
/* port: an empty player list during respawn is not an arrival. Empty
|
||||
AI lists retain the original ALL semantics. */
|
||||
if (waiting && coop_scripts_empty_player_list(object_list_index))
|
||||
return FALSE;
|
||||
if (!coop_scripts_any_player_will_do(object_list_index))
|
||||
return hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);
|
||||
/* port: in network co-op, waiting for every player means waiting for
|
||||
any one of them, and the rest are brought to them (coop_scripts.c) */
|
||||
inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, FALSE);
|
||||
if (inside && hs_runtime_waiting_on_call())
|
||||
if (inside && waiting)
|
||||
{
|
||||
coop_scripts_gather_in_volume(trigger_volume_index, object_list_index);
|
||||
/* port: gathering may fail or choose a point outside this volume.
|
||||
Do not close an irreversible gate until everyone actually arrived. */
|
||||
inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ enum
|
||||
player */
|
||||
MAXIMUM_OBJECT_LISTS_PER_MAP = HALO_PORT_MAXIMUM_OBJECT_LISTS_PER_MAP,
|
||||
MAXIMUM_LISTED_OBJECTS_PER_MAP = HALO_PORT_MAXIMUM_LISTED_OBJECTS_PER_MAP,
|
||||
/* port: list provenance; does not change the header's size. */
|
||||
_object_list_players_bit = 0,
|
||||
};
|
||||
|
||||
/* ---------- macros */
|
||||
@@ -131,12 +133,26 @@ long object_list_new(
|
||||
{
|
||||
struct object_list_header_datum* list = object_list_header_get(list_index);
|
||||
list->count = 0;
|
||||
list->flags = 0; /* port: a recycled slot has no previous provenance */
|
||||
list->first_reference_index = NONE;
|
||||
}
|
||||
|
||||
return list_index;
|
||||
}
|
||||
|
||||
/* port: only hs_players marks its completed result. */
|
||||
void object_list_mark_players(long list_index)
|
||||
{
|
||||
if (list_index != NONE)
|
||||
SET_FLAG(object_list_header_get(list_index)->flags, _object_list_players_bit, TRUE);
|
||||
}
|
||||
|
||||
boolean object_list_is_players(long list_index)
|
||||
{
|
||||
return list_index != NONE &&
|
||||
TEST_FLAG(object_list_header_get(list_index)->flags, _object_list_players_bit);
|
||||
}
|
||||
|
||||
void object_list_delete(
|
||||
long list_index)
|
||||
{
|
||||
@@ -164,6 +180,8 @@ void object_list_add(
|
||||
return;
|
||||
|
||||
list = object_list_header_get(object_list_index);
|
||||
/* port: a changed list no longer has hs_players' provenance. */
|
||||
list->flags = 0;
|
||||
reference_list_add(object_list_data, &list->first_reference_index, object_index);
|
||||
list->count++;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ OBJECT_LISTS.H
|
||||
struct object_list_header_datum
|
||||
{
|
||||
short identifier;
|
||||
word pad;
|
||||
word flags; /* port: provenance, using the original unused word */
|
||||
short reference_count;
|
||||
short count;
|
||||
long first_reference_index;
|
||||
@@ -32,6 +32,9 @@ void object_lists_dispose(void);
|
||||
void object_lists_initialize_for_new_map(void);
|
||||
void object_lists_dispose_from_old_map(void);
|
||||
long object_list_new(void);
|
||||
/* port: distinguish an empty (players) result from an empty AI list. */
|
||||
void object_list_mark_players(long list_index);
|
||||
boolean object_list_is_players(long list_index);
|
||||
void object_list_delete(long list_index);
|
||||
void object_list_add(long object_list_index, long object_index);
|
||||
long object_list_get_first(
|
||||
|
||||
@@ -50,6 +50,8 @@ SMAA_FILES = (("SMAA.hlsl", "xgpu_smaa_shader"), ("area_tex.zlib", "xgpu_smaa_ar
|
||||
("search_tex.zlib", "xgpu_smaa_search_texture"))
|
||||
# the desktop windows' icon (tools/app_icon.py; port/linux/src/sdl_platform.c)
|
||||
WINDOW_ICON = Path("port/assets/icon/opence-icon-256.png")
|
||||
# the pointer the menus show in place of the system's (port/linux/src/sdl_platform.c)
|
||||
MENU_CURSOR = Path("port/assets/cursor/menu_cursor.png")
|
||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
@@ -92,7 +94,8 @@ def hud_asset_inputs() -> List[Path]:
|
||||
return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()),
|
||||
*(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files()),
|
||||
*(SMAA_ASSETS / name for name, _ in smaa_files()),
|
||||
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else [])]
|
||||
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else []),
|
||||
*([MENU_CURSOR] if (ROOT / MENU_CURSOR).is_file() else [])]
|
||||
|
||||
|
||||
def hud_configure_inputs() -> List[Path]:
|
||||
@@ -251,6 +254,13 @@ def main() -> None:
|
||||
lines.extend(words(data) if data else ["\t0,"])
|
||||
lines.append("};")
|
||||
lines.append(f"const unsigned long platform_window_icon_size = {len(data)};")
|
||||
# the menus' pointer, a PNG (of size 0 where the checkout has none)
|
||||
data = (ROOT / MENU_CURSOR).read_bytes() if (ROOT / MENU_CURSOR).is_file() else b""
|
||||
lines.append("")
|
||||
lines.append("const unsigned int platform_menu_cursor[] = {")
|
||||
lines.extend(words(data) if data else ["\t0,"])
|
||||
lines.append("};")
|
||||
lines.append(f"const unsigned long platform_menu_cursor_size = {len(data)};")
|
||||
lines.append("")
|
||||
lines.append("#endif")
|
||||
output = Path(sys.argv[1])
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "harness.h"
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned short word;
|
||||
#define SET_FLAG(f,b,v) ((f)=(v)?(f)|(1U<<(b)):(f)&~(1U<<(b)))
|
||||
#define MAX_PLAYERS 128
|
||||
#include "list_header.inc"
|
||||
static struct object_list_header_datum headers[8];
|
||||
static int used[8], fail_allocate;
|
||||
static struct {long value,next;} refs[2048];
|
||||
static int references;
|
||||
static void *object_list_header_data,*object_list_data;
|
||||
#define object_list_header_get(i) (&headers[(i)])
|
||||
#define datum_new gate_datum_new
|
||||
static long gate_datum_new(void *data){int i;if(fail_allocate)return NONE;for(i=0;i<8;i++)if(!used[i]){used[i]=1;return i;}return NONE;}
|
||||
static void reference_list_add(void *data,long *head,long object){long p=references++;CHECK(p<2048,"fixture references");refs[p].value=object;refs[p].next=*head;*head=p;}
|
||||
static long object_list_get_first(long list,long *ref){if(list==NONE)return NONE;*ref=headers[list].first_reference_index;if(*ref==NONE)return NONE;long v=refs[*ref].value;*ref=refs[*ref].next;return v;}
|
||||
static long object_list_get_next(long list,long *ref){if(*ref==NONE)return NONE;long v=refs[*ref].value;*ref=refs[*ref].next;return v;}
|
||||
static short object_list_count(long list){return list==NONE?0:headers[list].count;}
|
||||
#include "lists.inc"
|
||||
|
||||
static int host=TRUE,waiting=TRUE,ignore_players,teleports,rollbacks,failures,bad_place,crowded,side_effects;
|
||||
static int player_count;
|
||||
struct data_iterator {int index;};
|
||||
struct player_datum {long unit_index;};
|
||||
static struct player_datum people[MAX_PLAYERS];
|
||||
static struct {real_point3d position;} objects[MAX_PLAYERS+1];
|
||||
static void *player_data;
|
||||
static long data_next_index(void *data,long previous){return previous+1<player_count?previous+1:NONE;}
|
||||
static struct player_datum *player_get(long i){CHECK(i>=0&&i<player_count,"invalid player");return &people[i];}
|
||||
static boolean coop_scripts_host(void){return host;}
|
||||
static long player_index_from_unit_index(long index){int i;for(i=0;i<player_count;i++)if(people[i].unit_index==index)return i;return NONE;}
|
||||
static real_point3d *object_get_origin(long i,real_point3d *p){*p=objects[i].position;return p;}
|
||||
static void object_set_position(long i,real_point3d const *p,void *forward,void *up){objects[i].position=*p;rollbacks++;}
|
||||
static boolean scenario_trigger_volume_test_object(short volume,long i){return objects[i].position.x>10&&objects[i].position.x<20;}
|
||||
static boolean player_position_switches_bsp(real_point3d const *p){return p->y>50;}
|
||||
static void biped_fix_ignore_players(boolean value){ignore_players=value;}
|
||||
static boolean player_teleport(long p,long anchor,real_point3d const *point){
|
||||
long u=people[p].unit_index;teleports++;CHECK(u!=NONE,"teleport dead player");
|
||||
if(failures){if(side_effects)objects[u].position.x=25;return FALSE;}
|
||||
if(crowded&&!ignore_players)return FALSE;
|
||||
objects[u].position=*point;
|
||||
if(bad_place==1)objects[u].position.x=25;
|
||||
if(bad_place==2)objects[u].position.y=100;
|
||||
return TRUE;
|
||||
}
|
||||
static boolean hs_runtime_waiting_on_call(void){return waiting;}
|
||||
#include "players.inc"
|
||||
#include "gather.inc"
|
||||
/* The original volume implementation is compiled too, including ALL of an
|
||||
empty list; provenance must preserve that behavior for AI lists. */
|
||||
#define MAXIMUM_TRIGGER_VOLUMES_PER_SCENARIO 256
|
||||
#define VALID_INDEX(i,n) ((unsigned)(i)<(n))
|
||||
#define BIT_VECTOR_SET_FLAG(bits,i,value) ((void)0)
|
||||
#define _error_silent 0
|
||||
#define error(...) ((void)0)
|
||||
#include "volume.inc"
|
||||
#include "gate.inc"
|
||||
static void reset(int count){int i;memset(used,0,sizeof(used));memset(headers,0,sizeof(headers));memset(objects,0,sizeof(objects));references=0;fail_allocate=0;host=waiting=TRUE;ignore_players=teleports=rollbacks=failures=bad_place=crowded=side_effects=0;player_count=count;for(i=0;i<count;i++){people[i].unit_index=i;objects[i].position.x=i?1:15;}}
|
||||
static boolean gate(long list){boolean r=hs_trigger_volume_test_objects_all(107,list);CHECK(!ignore_players,"collision override leaked");return r;}
|
||||
static int all_inside(void){int i;for(i=0;i<player_count;i++)if(people[i].unit_index!=NONE&&!scenario_trigger_volume_test_object(107,people[i].unit_index))return FALSE;return TRUE;}
|
||||
int main(int argc,char **argv){char const *name=argc>1?argv[1]:"";long list;int n;
|
||||
CHECK(sizeof(struct object_list_header_datum)==12,"checkpoint ABI changed");
|
||||
if(!strcmp(name,"crowds")||!strcmp(name,"refused")||!strcmp(name,"outside")||!strcmp(name,"bsp-result")){
|
||||
for(n=2;n<=128;n*=2){reset(n);list=hs_players();failures=!strcmp(name,"refused");bad_place=!strcmp(name,"outside")?1:!strcmp(name,"bsp-result")?2:0;crowded=!strcmp(name,"crowds");side_effects=TRUE;
|
||||
CHECK(gate(list)==(!failures&&!bad_place),"unsafe result for %d players",n);
|
||||
CHECK(all_inside()==(!failures&&!bad_place),"incorrect final positions");
|
||||
CHECK(teleports<=2*(n-1),"unbounded placement search");
|
||||
if(failures||bad_place){for(int i=1;i<n;i++)CHECK(objects[i].position.x==1&&objects[i].position.y==0,"bad placement not rolled back");CHECK(rollbacks==2*(n-1),"missing rollback");failures=bad_place=0;CHECK(gate(list)&&all_inside(),"failed gate did not retry");}
|
||||
}
|
||||
}else if(!strcmp(name,"one-player")){
|
||||
reset(1);list=hs_players();CHECK(gate(list)&&!teleports,"one-player host changed");objects[0].position.x=1;CHECK(!gate(list)&&!teleports,"one player outside passed gate");
|
||||
}else if(!strcmp(name,"empty")){
|
||||
reset(4);for(n=0;n<4;n++)people[n].unit_index=NONE;
|
||||
list=hs_players();CHECK(object_list_is_players(list),"empty players lost provenance");CHECK(!gate(list)&&!teleports,"empty players advanced gate");
|
||||
long ai=object_list_new();CHECK(gate(ai)&&!teleports,"empty AI semantics changed");
|
||||
waiting=FALSE;CHECK(gate(list),"non-waiting empty ALL semantics changed");
|
||||
waiting=TRUE;host=FALSE;CHECK(gate(list),"local/client empty ALL semantics changed");
|
||||
}else if(!strcmp(name,"allocation")){
|
||||
reset(2);fail_allocate=1;list=hs_players();CHECK(list==NONE&&!gate(list)&&!teleports,"allocation failure opened gate");
|
||||
}else if(!strcmp(name,"lifetime")){
|
||||
reset(0);list=hs_players();CHECK(object_list_is_players(list),"mark failed");used[list]=0;
|
||||
long recycled=object_list_new();CHECK(recycled==list&&!object_list_is_players(recycled),"provenance survived slot reuse");
|
||||
object_list_mark_players(recycled);object_list_add(recycled,MAX_PLAYERS);CHECK(!object_list_is_players(recycled),"changed list retained provenance");
|
||||
}else if(!strcmp(name,"contexts")){
|
||||
reset(2);list=hs_players();waiting=FALSE;CHECK(gate(list)&&!teleports,"if/not test moved players or lost ANY semantics");
|
||||
waiting=TRUE;host=FALSE;CHECK(!gate(list)&&!teleports,"non-host gathered players");
|
||||
objects[1].position.x=15;CHECK(gate(list),"non-host ALL changed");
|
||||
}else if(!strcmp(name,"mixed")){
|
||||
reset(2);list=hs_players();object_list_add(list,MAX_PLAYERS);CHECK(!gate(list)&&!teleports,"mixed AI/player list gathered");
|
||||
}else if(!strcmp(name,"no-arrival")){
|
||||
reset(3);objects[0].position.x=1;list=hs_players();CHECK(!gate(list)&&!teleports,"gathered with nobody inside");
|
||||
}else if(!strcmp(name,"bsp-anchor")){
|
||||
reset(3);objects[0].position.y=100;list=hs_players();CHECK(!gate(list)&&!teleports,"used a loading-zone anchor");
|
||||
objects[1].position.x=15;CHECK(gate(list)&&all_inside(),"did not choose a safe second anchor");
|
||||
}else return 2;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "harness.h"
|
||||
#include <ctype.h>
|
||||
typedef unsigned short word;
|
||||
#define NUMBEROF(a) (sizeof(a) / sizeof((a)[0]))
|
||||
struct player_datum
|
||||
{
|
||||
long unit_index;
|
||||
};
|
||||
struct data_iterator
|
||||
{
|
||||
int slot;
|
||||
};
|
||||
static struct player_datum people[128];
|
||||
static int occupied[128], host = TRUE, calls[128];
|
||||
static long ref_head;
|
||||
static struct data_array *player_data, *object_list_data;
|
||||
static long data_next_index(struct data_array *data, long previous)
|
||||
{
|
||||
int i;
|
||||
for (i = previous + 1; i < 128; i++)
|
||||
if (occupied[i])
|
||||
return i;
|
||||
return NONE;
|
||||
}
|
||||
static struct player_datum *player_get(long i)
|
||||
{
|
||||
return &people[i];
|
||||
}
|
||||
static void data_iterator_new(struct data_iterator *it, struct data_array *data)
|
||||
{
|
||||
it->slot = NONE;
|
||||
}
|
||||
static struct player_datum *data_iterator_next(struct data_iterator *it)
|
||||
{
|
||||
it->slot = data_next_index(player_data, it->slot);
|
||||
return it->slot == NONE ? NULL : &people[it->slot];
|
||||
}
|
||||
static boolean coop_scripts_host(void)
|
||||
{
|
||||
return host;
|
||||
}
|
||||
static void reference_list_port_full(struct data_array *data)
|
||||
{
|
||||
CHECK(FALSE, "fixture full");
|
||||
}
|
||||
static void data_delete_all(struct data_array *data)
|
||||
{
|
||||
memset(data->data, 0, data->maximum_count * data->size);
|
||||
data->count = 0;
|
||||
}
|
||||
#include "reference.inc"
|
||||
static long object_list_new(void)
|
||||
{
|
||||
ref_head = NONE;
|
||||
return 0;
|
||||
}
|
||||
static void object_list_add(long list, long unit)
|
||||
{
|
||||
reference_list_add(object_list_data, &ref_head, unit);
|
||||
}
|
||||
static void object_list_mark_players(long list) {}
|
||||
static void object_list_delete(long list)
|
||||
{
|
||||
data_delete_all(object_list_data);
|
||||
}
|
||||
#include "followers.inc"
|
||||
#include "players.inc"
|
||||
static long player_index_from_unit_index(long unit)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 128; i++)
|
||||
if (occupied[i] && people[i].unit_index == unit)
|
||||
return i;
|
||||
return NONE;
|
||||
}
|
||||
static boolean coop_scripts_unit_near(long unit, real_point3d const *p)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
static boolean player_position_switches_bsp(real_point3d const *p)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
static real_point3d *object_get_origin(long unit, real_point3d *p)
|
||||
{
|
||||
memset(p, 0, sizeof(*p));
|
||||
return p;
|
||||
}
|
||||
static boolean coop_scripts_arrive_at(long player, real_point3d const *p, long const *arrived,
|
||||
short count)
|
||||
{
|
||||
calls[player]++;
|
||||
return TRUE;
|
||||
}
|
||||
static void unit_scripting_suspended(long unit, boolean value)
|
||||
{
|
||||
calls[player_index_from_unit_index(unit)]++;
|
||||
}
|
||||
static struct
|
||||
{
|
||||
struct
|
||||
{
|
||||
long parent_object_index;
|
||||
} object;
|
||||
} objects[128];
|
||||
#define object_get(unit) (&objects[player_index_from_unit_index(unit)])
|
||||
static void unit_scripting_exit_vehicle(long unit)
|
||||
{
|
||||
calls[player_index_from_unit_index(unit)]++;
|
||||
}
|
||||
static char *strlwr(char *p)
|
||||
{
|
||||
char *s = p;
|
||||
while (*s)
|
||||
{
|
||||
*s = tolower((unsigned char)*s);
|
||||
s++;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
static void vehicle_scripting_load_magic(long vehicle, char const *seat, long list)
|
||||
{
|
||||
struct data_reference *r = datum_get(object_list_data, ref_head);
|
||||
CHECK(!strcmp(seat, "p-rider"), "seat prefix changed");
|
||||
calls[player_index_from_unit_index(r->datum_index)]++;
|
||||
}
|
||||
static void player_add_equipment(long unit, short profile, boolean reset)
|
||||
{
|
||||
calls[player_index_from_unit_index(unit)]++;
|
||||
}
|
||||
#include "actions.inc"
|
||||
static int script_units(long *units)
|
||||
{
|
||||
int n = 0;
|
||||
long ref;
|
||||
hs_players();
|
||||
for (ref = ref_head; ref != NONE;)
|
||||
{
|
||||
struct data_reference *r = datum_get(object_list_data, ref);
|
||||
units[n++] = r->datum_index;
|
||||
ref = r->next_reference_index;
|
||||
}
|
||||
object_list_delete(0);
|
||||
return n;
|
||||
}
|
||||
static void verify(void)
|
||||
{
|
||||
long named[128], followers[130];
|
||||
int n = script_units(named), i, j, count;
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
memset(followers, 0x55, sizeof(followers));
|
||||
count = players_following(named[i], followers, 128);
|
||||
CHECK(count == (i == 0 && n > 2 ? n - 2 : 0),
|
||||
"script player %d, %d live units: %d followers", i, n, count);
|
||||
for (j = 0; j < count; j++)
|
||||
CHECK(followers[j] == named[j + 2], "follower %d disagrees with hs_players", j);
|
||||
}
|
||||
CHECK(!players_following(NONE, followers, 128), "NONE has followers");
|
||||
CHECK(!players_following(123456, followers, 128), "nonplayer has followers");
|
||||
}
|
||||
static void reset(int count)
|
||||
{
|
||||
int i;
|
||||
memset(occupied, 0, sizeof(occupied));
|
||||
memset(calls, 0, sizeof(calls));
|
||||
host = TRUE;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
occupied[i] = 1;
|
||||
people[i].unit_index = 1000 + 13 * i;
|
||||
objects[i].object.parent_object_index = 555;
|
||||
}
|
||||
data_delete_all(object_list_data);
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char const *name = argc > 1 ? argv[1] : "";
|
||||
int n, i;
|
||||
long units[128], out[128];
|
||||
real_point3d destination = {1, 2, 3};
|
||||
object_list_data = game_state_data_new("test refs", 128, sizeof(struct data_reference));
|
||||
if (!strcmp(name, "counts"))
|
||||
{
|
||||
for (n = 0; n <= 128; n++)
|
||||
{
|
||||
reset(n);
|
||||
verify();
|
||||
}
|
||||
}
|
||||
else if (!strcmp(name, "gaps"))
|
||||
{
|
||||
reset(128);
|
||||
for (i = 0; i < 128; i++)
|
||||
{
|
||||
if (i % 3 == 0)
|
||||
occupied[i] = 0;
|
||||
if (i % 5 == 0)
|
||||
people[i].unit_index = NONE;
|
||||
}
|
||||
verify();
|
||||
}
|
||||
else if (!strcmp(name, "respawn"))
|
||||
{
|
||||
reset(12);
|
||||
for (i = 11; i >= 0; i--)
|
||||
{
|
||||
people[i].unit_index = NONE;
|
||||
verify();
|
||||
people[i].unit_index = 9000 + i;
|
||||
verify();
|
||||
occupied[i] = 0;
|
||||
verify();
|
||||
}
|
||||
}
|
||||
else if (!strcmp(name, "bounded"))
|
||||
{
|
||||
reset(12);
|
||||
n = script_units(units);
|
||||
for (i = 0; i <= n; i++)
|
||||
{
|
||||
out[i] = 123456;
|
||||
CHECK(players_following(units[0], out, i) == (i < n - 2 ? i : n - 2),
|
||||
"capacity ignored");
|
||||
CHECK(out[i] == 123456, "output overrun");
|
||||
}
|
||||
}
|
||||
else if (!strcmp(name, "nonhost"))
|
||||
{
|
||||
reset(12);
|
||||
n = script_units(units);
|
||||
host = FALSE;
|
||||
CHECK(!players_following(units[0], out, 128), "nonhost followers");
|
||||
}
|
||||
else
|
||||
{
|
||||
reset(12);
|
||||
n = script_units(units);
|
||||
if (!strcmp(name, "teleport"))
|
||||
coop_scripts_teleport_followers(units[0], &destination);
|
||||
else if (!strcmp(name, "suspend"))
|
||||
coop_scripts_suspend_followers(units[0], TRUE);
|
||||
else if (!strcmp(name, "exit"))
|
||||
{
|
||||
objects[player_index_from_unit_index(units[4])].object.parent_object_index = 556;
|
||||
coop_scripts_exit_followers(units[0], 555);
|
||||
}
|
||||
else if (!strcmp(name, "board"))
|
||||
coop_scripts_board_followers(units[0], 555, "P-RiderLF");
|
||||
else if (!strcmp(name, "equipment"))
|
||||
coop_scripts_player_add_equipment(units[0], 2, TRUE);
|
||||
else
|
||||
CHECK(FALSE, "unknown case");
|
||||
for (i = 0; i < n; i++)
|
||||
CHECK(calls[player_index_from_unit_index(units[i])] ==
|
||||
((i >= 2 && (!strcmp(name, "exit") ? i != 4 : TRUE)) ||
|
||||
(i == 0 && !strcmp(name, "equipment"))),
|
||||
"wrong action recipient %d", i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Co-op one-way gates: real player lists, volume tests and placement code."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
|
||||
from harness import build,read,run,function,structure,constant,mutated,CHECK_FAILED
|
||||
|
||||
def generated(fault=None):
|
||||
lists=read('source/hs/object_lists.c')
|
||||
code='\n'.join(function(lists,n) for n in ['object_list_new','object_list_mark_players','object_list_is_players','object_list_add'])
|
||||
players=function(read('source/hs/hs_library_external.c'),'hs_players')
|
||||
port=read('port/linux/game/coop_scripts.c')
|
||||
gather='\n'.join(function(port,n) for n in ['coop_scripts_any_player_will_do','coop_scripts_empty_player_list','teleport_to','gather_player_in_volume','coop_scripts_gather_in_volume'])
|
||||
gate=function(read('source/hs/hs_library_external.c'),'hs_trigger_volume_test_objects_all')
|
||||
if fault=='unverified':gate=mutated(gate,'inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);','inside = TRUE;')
|
||||
if fault=='empty':players=mutated(players,'object_list_mark_players(object_list_index);',';')
|
||||
if fault=='recycled':code=mutated(code,'list->flags = 0; /* port: a recycled slot has no previous provenance */',';')
|
||||
if fault=='rollback':gather=mutated(gather,'object_set_position(unit_index, &original, NULL, NULL);',';')
|
||||
if fault=='stack':gather=mutated(gather,'attempt != 0','FALSE')
|
||||
return (('list_header.inc','#define _object_list_players_bit '+str(constant(lists,'_object_list_players_bit'))+'\n'+structure(read('source/hs/object_lists.h'),'object_list_header_datum')),('lists.inc',code),('players.inc',players),('gather.inc',gather),('volume.inc',function(read('source/hs/hs_library_external.c'),'hs_trigger_volume_test_objects')),('gate.inc',gate))
|
||||
|
||||
@pytest.mark.parametrize('case',['one-player','crowds','refused','outside','bsp-result','empty','allocation','lifetime','contexts','mixed','no-arrival','bsp-anchor'])
|
||||
def test_gate(case):
|
||||
status,output=run(build('coop_gate',generated()),case)
|
||||
assert status==0,output
|
||||
|
||||
@pytest.mark.parametrize('fault,case',[('unverified','refused'),('empty','empty'),('recycled','lifetime'),('rollback','outside'),('stack','crowds')])
|
||||
def test_negative(fault,case):
|
||||
status,output=run(build('coop_gate',generated(fault)),case)
|
||||
assert status==CHECK_FAILED,output
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Script player0/followers must agree with the engine's actual players list."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from harness import build, read, run, function, inline, constant, structure, mutated, CHECK_FAILED
|
||||
|
||||
|
||||
def generated(fault=None):
|
||||
port = read('port/linux/game/coop_scripts.c')
|
||||
followers = function(port, 'players_following')
|
||||
if fault == 'forward':
|
||||
followers = mutated(followers, 'players[player_count - 1] != unit_index', 'players[0] != unit_index')
|
||||
if fault == 'named':
|
||||
followers = mutated(followers, 'player_count - NAMED_PLAYERS - 1', 'player_count - 2')
|
||||
return (
|
||||
('reference.inc', structure(read('source/objects/reference_lists.h'), 'data_reference') + '\n' +
|
||||
inline(read('source/objects/reference_lists.h'), 'reference_list_add')),
|
||||
('players.inc', function(read('source/hs/hs_library_external.c'), 'hs_players')),
|
||||
('followers.inc', '#define HALO_PORT_MAXIMUM_NETWORK_PLAYERS ' +
|
||||
str(constant(read('port/linux/include/halo_port_limits.h'), 'HALO_PORT_MAXIMUM_NETWORK_PLAYERS')) +
|
||||
'\n#define NAMED_PLAYERS ' + str(constant(port, 'NAMED_PLAYERS')) + '\n' + followers),
|
||||
('actions.inc', '\n'.join(function(port, name) for name in [
|
||||
'coop_scripts_teleport_followers', 'coop_scripts_suspend_followers',
|
||||
'coop_scripts_exit_followers', 'follower_seat_name',
|
||||
'coop_scripts_board_followers', 'coop_scripts_player_add_equipment'])),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('case', ['counts', 'gaps', 'respawn', 'bounded', 'nonhost', 'teleport', 'suspend', 'exit', 'board', 'equipment'])
|
||||
def test_script_players(case):
|
||||
status, output = run(build('coop_script_players', generated()), case)
|
||||
assert status == 0, output
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault', ['forward', 'named'])
|
||||
def test_negative(fault):
|
||||
status, output = run(build('coop_script_players', generated(fault)), 'counts')
|
||||
assert status == CHECK_FAILED, output
|
||||
@@ -0,0 +1,58 @@
|
||||
"""The desktop visibility readback asks the driver for nothing before the swap (a GL error, a fence's state), and at
|
||||
most one question after it, so that a threaded driver need not catch up with the frame just drawn before it is
|
||||
shown. Each batch's counts are taken once the fence after their copies has passed; a GPU however late is waited for,
|
||||
never given up on; a copy that failed is seen by the place it left uncopied, without asking for GL errors."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from harness import CHECK_FAILED, build, constant, function, mutated, read, run, structure
|
||||
|
||||
CASES = ['quiet', 'late', 'lost_copy', 'fence_failed', 'logging']
|
||||
SOURCE = 'port/linux/src/d3d8_gl.c'
|
||||
|
||||
|
||||
def generated(fault=None):
|
||||
source = read(SOURCE)
|
||||
config = '#define VISIBILITY_TEST_SLOTS %d\n#define VISIBILITY_BATCH_COUNT %d\n%s;\n' % (
|
||||
constant(source, 'VISIBILITY_TEST_SLOTS'), constant(source, 'VISIBILITY_BATCH_COUNT'),
|
||||
structure(source, 'visibility_batch'))
|
||||
code = '\n'.join(function(source, name) for name in (
|
||||
'visibility_readback_gl_ok', 'visibility_fall_back_to_queries', 'visibility_send_batch',
|
||||
'visibility_read_batch'))
|
||||
if fault:
|
||||
code = mutated(code, *fault)
|
||||
return (('config.inc', config), ('under_test.inc', code))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('case', CASES)
|
||||
def test_case(case):
|
||||
status, output = run(build('visibility_readback', generated()), case)
|
||||
assert status == 0, output
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fault,case', [
|
||||
# GL errors asked for before every swap
|
||||
(('if (device.visibility_logging)\n\t\t\tvisibility_readback_gl_ok("before query copies");',
|
||||
'visibility_readback_gl_ok("before query copies");'), 'quiet'),
|
||||
# the batch just sent asked about too: two questions a present
|
||||
(('if (batch->frame == device.frame)\n\t\t\tbreak;', 'if (0)\n\t\t\tbreak;'), 'quiet'),
|
||||
# a late GPU's batches given up after a while
|
||||
(('if (status == GL_TIMEOUT_EXPIRED)\n',
|
||||
'if (status == GL_TIMEOUT_EXPIRED && device.frame - batch->frame < 240)\n'), 'late'),
|
||||
# a copy that wrote nothing taken for a count
|
||||
(('if (samples == VISIBILITY_UNCOPIED)', 'if (0)'), 'lost_copy'),
|
||||
# a fence waited on, counting on the swap to have sent it
|
||||
(('glClientWaitSync(batch->fence, GL_SYNC_FLUSH_COMMANDS_BIT, 0)', 'glClientWaitSync(batch->fence, 0, 0)'),
|
||||
'quiet'),
|
||||
# the buffer read on after a lost copy, whose batch is not used again (with all three lost, no more counts)
|
||||
(('if (fall_back)\n\t\tvisibility_fall_back_to_queries();', 'if (fall_back)\n\t\tfall_back = FALSE;'),
|
||||
'lost_copy'),
|
||||
# a failed fence taken for a passed one
|
||||
(('if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)', 'if (0)'), 'fence_failed'),
|
||||
])
|
||||
def test_negative_control(fault, case):
|
||||
status, output = run(build('visibility_readback', generated(fault)), case)
|
||||
assert status == CHECK_FAILED, output
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "harness.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef unsigned int GLuint;
|
||||
typedef int GLint;
|
||||
typedef unsigned int GLenum;
|
||||
typedef unsigned int GLbitfield;
|
||||
typedef unsigned long long GLuint64;
|
||||
typedef intptr_t GLintptr;
|
||||
typedef unsigned long long ULONGLONG;
|
||||
typedef int BOOL;
|
||||
typedef struct fence *GLsync;
|
||||
#define GL_NO_ERROR 0
|
||||
#define GL_INVALID_OPERATION 0x0502
|
||||
#define GL_QUERY_RESULT 0x8866
|
||||
#define GL_QUERY_RESULT_AVAILABLE 0x8867
|
||||
#define GL_QUERY_BUFFER 0x9192
|
||||
#define GL_QUERY_BUFFER_BINDING 0x9193
|
||||
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
||||
#define GL_ALREADY_SIGNALED 0x911A
|
||||
#define GL_TIMEOUT_EXPIRED 0x911B
|
||||
#define GL_CONDITION_SATISFIED 0x911C
|
||||
#define GL_WAIT_FAILED 0x911D
|
||||
#include "config.inc"
|
||||
/* (the code before the change has none) */
|
||||
#ifndef VISIBILITY_UNCOPIED
|
||||
#define VISIBILITY_UNCOPIED 0xFFFFFFFFUL
|
||||
#endif
|
||||
|
||||
static struct
|
||||
{
|
||||
unsigned long frame;
|
||||
GLuint visibility_known[VISIBILITY_TEST_SLOTS];
|
||||
struct visibility_batch visibility_batches[VISIBILITY_BATCH_COUNT];
|
||||
unsigned long visibility_batch;
|
||||
ULONGLONG visibility_serial;
|
||||
BOOL visibility_unavailable;
|
||||
BOOL visibility_logging;
|
||||
GLuint visibility_buffer;
|
||||
volatile GLuint *visibility_mapped;
|
||||
struct
|
||||
{
|
||||
unsigned long submitted, completed, samples, nonzero, maximum;
|
||||
unsigned long unavailable, dropped, failures;
|
||||
} visibility_stats;
|
||||
} device;
|
||||
|
||||
/* the GPU: each query's count, and the commands it has yet to carry out, in
|
||||
the order they were sent (gpu_run carries them out) */
|
||||
struct fence
|
||||
{
|
||||
BOOL signaled, broken;
|
||||
};
|
||||
enum { COMMAND_COPY, COMMAND_FENCE };
|
||||
static struct
|
||||
{
|
||||
int type;
|
||||
GLuint query;
|
||||
GLintptr offset;
|
||||
struct fence *fence;
|
||||
} commands[512];
|
||||
static unsigned long command_count;
|
||||
static GLuint query_samples[64];
|
||||
static GLuint results[VISIBILITY_BATCH_COUNT * VISIBILITY_TEST_SLOTS];
|
||||
static struct fence fences[512];
|
||||
static unsigned long fence_count;
|
||||
static BOOL copy_lost, copy_error, fence_broken;
|
||||
static GLenum gl_error;
|
||||
/* calls that make a threaded driver catch up with every command before them
|
||||
(they answer the CPU), all of them and those before the swap; the flushes */
|
||||
static unsigned long driver_answers, answers_before_swap, flushes, logs;
|
||||
static BOOL before_swap;
|
||||
|
||||
static void answered(void)
|
||||
{
|
||||
driver_answers++;
|
||||
answers_before_swap += before_swap;
|
||||
}
|
||||
|
||||
static GLenum glGetError(void)
|
||||
{
|
||||
GLenum error = gl_error;
|
||||
|
||||
answered();
|
||||
gl_error = GL_NO_ERROR;
|
||||
return error;
|
||||
}
|
||||
|
||||
static void glGetQueryBufferObjectuiv(GLuint query, GLuint buffer, GLenum name, GLintptr offset)
|
||||
{
|
||||
CHECK(buffer == device.visibility_buffer && name == GL_QUERY_RESULT, "a copy into another buffer");
|
||||
commands[command_count].type = COMMAND_COPY;
|
||||
commands[command_count].query = query;
|
||||
commands[command_count++].offset = offset;
|
||||
if (copy_error)
|
||||
gl_error = GL_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
static GLsync glFenceSync(GLenum condition, GLbitfield flags)
|
||||
{
|
||||
struct fence *fence = &fences[fence_count++ % 512];
|
||||
|
||||
fence->signaled = FALSE;
|
||||
fence->broken = fence_broken;
|
||||
commands[command_count].type = COMMAND_FENCE;
|
||||
commands[command_count++].fence = fence;
|
||||
return fence;
|
||||
}
|
||||
|
||||
static GLenum glClientWaitSync(GLsync fence, GLbitfield flags, GLuint64 timeout)
|
||||
{
|
||||
/* (a fence not yet sent to the GPU may never pass without a flush) */
|
||||
CHECK(flags & GL_SYNC_FLUSH_COMMANDS_BIT, "a fence waited on without asking for a flush");
|
||||
answered();
|
||||
if (fence->broken)
|
||||
return GL_WAIT_FAILED;
|
||||
return fence->signaled ? GL_ALREADY_SIGNALED : GL_TIMEOUT_EXPIRED;
|
||||
}
|
||||
|
||||
static void glDeleteSync(GLsync fence) { }
|
||||
static void glFlush(void) { flushes++; }
|
||||
static void glBindBuffer(GLenum target, GLuint buffer) { }
|
||||
static void glGetIntegerv(GLenum name, GLint *value) { answered(); *value = 0; }
|
||||
static void glGetQueryObjectuiv(GLuint query, GLenum name, GLuint *value)
|
||||
{
|
||||
answered();
|
||||
*value = name == GL_QUERY_RESULT_AVAILABLE ? 1 : query_samples[query];
|
||||
}
|
||||
|
||||
static void platform_log(const char *format, ...)
|
||||
{
|
||||
va_list arguments;
|
||||
|
||||
logs++;
|
||||
va_start(arguments, format);
|
||||
vfprintf(stderr, format, arguments);
|
||||
va_end(arguments);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
#include "under_test.inc"
|
||||
|
||||
/* the GPU carries out the first count of its commands (all of them with
|
||||
ULONG_MAX): a copy writes its query's count (none when copy_lost), a fence
|
||||
passes */
|
||||
static void gpu_run(unsigned long count)
|
||||
{
|
||||
unsigned long i;
|
||||
|
||||
if (count > command_count)
|
||||
count = command_count;
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (commands[i].type == COMMAND_COPY && !copy_lost)
|
||||
results[commands[i].offset / sizeof(GLuint)] = query_samples[commands[i].query];
|
||||
else if (commands[i].type == COMMAND_FENCE)
|
||||
commands[i].fence->signaled = TRUE;
|
||||
}
|
||||
for (i = count; i < command_count; i++)
|
||||
commands[i - count] = commands[i];
|
||||
command_count -= count;
|
||||
}
|
||||
|
||||
/* a present: the batch sent before the swap, the counts read after it */
|
||||
static void present(unsigned long index)
|
||||
{
|
||||
device.frame = index;
|
||||
before_swap = TRUE;
|
||||
visibility_send_batch();
|
||||
before_swap = FALSE;
|
||||
visibility_read_batch();
|
||||
}
|
||||
|
||||
/* a frame's two tests in the batch being filled (slots 10 and 11, counts by
|
||||
frame), then its present */
|
||||
static struct visibility_batch *frame(unsigned long index)
|
||||
{
|
||||
struct visibility_batch *batch = &device.visibility_batches[device.visibility_batch];
|
||||
|
||||
CHECK(device.visibility_batch < VISIBILITY_BATCH_COUNT, "no batch free at frame %lu", index);
|
||||
batch->queries[0] = (GLuint)(1 + (index * 2) % 62);
|
||||
batch->queries[1] = (GLuint)(2 + (index * 2) % 62);
|
||||
query_samples[batch->queries[0]] = (GLuint)(100 + index);
|
||||
query_samples[batch->queries[1]] = (GLuint)(200 + index);
|
||||
batch->slots[0] = 10;
|
||||
batch->slots[1] = 11;
|
||||
batch->areas[0] = batch->areas[1] = 1.0f;
|
||||
batch->count = 2;
|
||||
present(index);
|
||||
return batch;
|
||||
}
|
||||
|
||||
static void start(BOOL logging)
|
||||
{
|
||||
device.visibility_buffer = 7;
|
||||
device.visibility_mapped = results;
|
||||
device.visibility_logging = logging;
|
||||
device.visibility_known[10] = 77;
|
||||
device.visibility_known[11] = 88;
|
||||
}
|
||||
|
||||
static void known(GLuint first, GLuint second, const char *what)
|
||||
{
|
||||
CHECK(device.visibility_known[10] == first && device.visibility_known[11] == second,
|
||||
"%s: the slots hold %u and %u, not %u and %u", what, device.visibility_known[10], device.visibility_known[11],
|
||||
first, second);
|
||||
}
|
||||
|
||||
/* frames whose commands the GPU carries out before the next present, which
|
||||
reads their counts */
|
||||
static void play(unsigned long frames)
|
||||
{
|
||||
unsigned long index;
|
||||
|
||||
for (index = 0; index < frames; index++)
|
||||
{
|
||||
frame(index);
|
||||
if (index)
|
||||
known((GLuint)(100 + index - 1), (GLuint)(200 + index - 1), "a frame on");
|
||||
gpu_run(ULONG_MAX);
|
||||
}
|
||||
CHECK(device.visibility_stats.completed == frames - 1, "%lu batches read, not %lu",
|
||||
device.visibility_stats.completed, frames - 1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *case_name = argv[1];
|
||||
|
||||
CASE("quiet")
|
||||
{
|
||||
/* without debug.gl_debug, nothing before the swap asks the driver
|
||||
for an answer, no present flushes (the swap does), and after the
|
||||
swap at most one question a present */
|
||||
start(FALSE);
|
||||
play(12);
|
||||
CHECK(answers_before_swap == 0, "the driver was asked %lu times before the swap in 12 presents",
|
||||
answers_before_swap);
|
||||
CHECK(flushes == 0, "%lu flushes in 12 presents", flushes);
|
||||
CHECK(driver_answers <= 12, "the driver was asked %lu times in 12 presents", driver_answers);
|
||||
}
|
||||
CASE("late")
|
||||
{
|
||||
/* a GPU 1000 presents behind: its counts are not taken before their
|
||||
fence passes, no batch is given up, and the counts are read once
|
||||
they come (visibility goes on updating) */
|
||||
unsigned long index;
|
||||
|
||||
start(FALSE);
|
||||
frame(0);
|
||||
frame(1);
|
||||
frame(2);
|
||||
for (index = 3; index < 1003; index++)
|
||||
present(index);
|
||||
known(77, 88, "nothing carried out");
|
||||
for (index = 0; index < VISIBILITY_BATCH_COUNT; index++)
|
||||
CHECK(!device.visibility_batches[index].failed, "batch %lu given up while the GPU was late", index);
|
||||
gpu_run(2);
|
||||
present(1003);
|
||||
known(77, 88, "copies done, fence not passed");
|
||||
gpu_run(ULONG_MAX);
|
||||
present(1004);
|
||||
known(102, 202, "all carried out");
|
||||
frame(1005);
|
||||
gpu_run(ULONG_MAX);
|
||||
present(1006);
|
||||
known(1105, 1205, "the next batch after a late GPU");
|
||||
}
|
||||
CASE("lost_copy")
|
||||
{
|
||||
/* a copy that wrote nothing (a failed one) is seen without asking
|
||||
for GL errors: its place still holds VISIBILITY_UNCOPIED once the
|
||||
fence has passed. Its batch is dropped, the slots keep their earlier
|
||||
counts, and the counts are read from each query from then on: the
|
||||
batch sent meanwhile, and those after it. */
|
||||
start(FALSE);
|
||||
frame(0);
|
||||
copy_lost = TRUE;
|
||||
gpu_run(ULONG_MAX);
|
||||
copy_lost = FALSE;
|
||||
frame(1);
|
||||
CHECK(device.visibility_batches[0].failed, "a batch whose counts were never copied was kept");
|
||||
known(77, 88, "after a lost copy");
|
||||
CHECK(logs > 0 && device.visibility_stats.failures, "a lost copy not reported");
|
||||
CHECK(driver_answers <= 1, "GL errors asked for without debug.gl_debug (%lu answers)", driver_answers);
|
||||
CHECK(!device.visibility_mapped, "the buffer still read after its copies failed");
|
||||
gpu_run(ULONG_MAX);
|
||||
present(2);
|
||||
known(101, 201, "the batch sent before the lost copy was seen");
|
||||
frame(3);
|
||||
present(4);
|
||||
known(103, 203, "a batch sent after the lost copy");
|
||||
}
|
||||
CASE("fence_failed")
|
||||
{
|
||||
/* a fence that cannot be waited on drops its batch, keeping the
|
||||
slots' earlier counts; the counts are read from each query from
|
||||
then on */
|
||||
start(FALSE);
|
||||
fence_broken = TRUE;
|
||||
frame(0);
|
||||
fence_broken = FALSE;
|
||||
gpu_run(ULONG_MAX);
|
||||
frame(1);
|
||||
CHECK(device.visibility_batches[0].failed, "a batch whose fence failed was read");
|
||||
known(77, 88, "after a failed fence");
|
||||
CHECK(!device.visibility_mapped, "the buffer still read after a fence failed");
|
||||
present(2);
|
||||
known(101, 201, "the batch after a failed fence");
|
||||
}
|
||||
CASE("logging")
|
||||
{
|
||||
/* with debug.gl_debug, GL errors are asked for, and a copy that
|
||||
failed drops its batch as it is sent */
|
||||
start(TRUE);
|
||||
play(6);
|
||||
CHECK(driver_answers > 6, "GL errors never asked for with debug.gl_debug");
|
||||
copy_error = TRUE;
|
||||
CHECK(frame(6)->failed, "a batch whose copies failed was kept");
|
||||
CHECK(logs > 0 && device.visibility_stats.failures, "a failed copy not reported");
|
||||
CHECK(!device.visibility_mapped, "the buffer still read after its copies failed");
|
||||
copy_error = FALSE;
|
||||
frame(7);
|
||||
present(8);
|
||||
known(107, 207, "a batch after the failed copies");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ringfall: Mjolnir VR's pictures, drawn from nothing but shapes and the
|
||||
Mjolnir VR's pictures, drawn from nothing but shapes and the
|
||||
port's Overpass font (port/assets/fonts, SIL Open Font License):
|
||||
|
||||
python3 tools/ringfall_art.py
|
||||
python3 tools/mjolnir_art.py
|
||||
|
||||
writes
|
||||
- port/android/art/android-icon.png, the launcher icon's artwork (then run
|
||||
@@ -99,11 +99,11 @@ def italic_text(text, height, fill):
|
||||
|
||||
|
||||
def logo(width):
|
||||
""""RINGFALL" over "MJOLNIR VR", as wide as asked, transparent"""
|
||||
title = italic_text("RINGFALL", 200, LIGHT)
|
||||
""""MJOLNIR" over "VR", as wide as asked, transparent"""
|
||||
title = italic_text("MJOLNIR", 200, LIGHT)
|
||||
title = title.resize((width, int(title.height * width / title.width)), Image.LANCZOS)
|
||||
subtitle = italic_text("MJOLNIR VR", 200, ACCENT)
|
||||
sub_width = int(width * 0.62)
|
||||
subtitle = italic_text("VR", 200, ACCENT)
|
||||
sub_width = int(width * 0.30)
|
||||
subtitle = subtitle.resize((sub_width, int(subtitle.height * sub_width / subtitle.width)), Image.LANCZOS)
|
||||
gap = int(title.height * 0.16)
|
||||
image = Image.new("RGBA", (width, title.height + gap + subtitle.height), (0, 0, 0, 0))
|
||||
@@ -5,14 +5,14 @@ The Steam Frame's installer as release assets (port/android/STEAM_FRAME.md):
|
||||
python3 tools/steam_frame_bundle.py <apk> <output folder> <owner/repository>
|
||||
|
||||
writes into the output folder
|
||||
- ringfall-mjolnir-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
|
||||
- mjolnir-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
|
||||
as install.sh), the APK (RingfallMjolnirVR.apk), the launcher
|
||||
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh, which updates the game
|
||||
from the repository named), the build's number (build.txt: the workflow's
|
||||
run number, HALO_BUILD_NUMBER or GITHUB_RUN_NUMBER; 0, which never
|
||||
updates, without one), the disc image reader (steam_frame_xiso.py) and
|
||||
the library pictures (grid/);
|
||||
- Install-Ringfall-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what
|
||||
- Install-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what
|
||||
the player downloads and opens in Desktop Mode; it fetches the latest
|
||||
release's zip from the repository named.
|
||||
"""
|
||||
@@ -44,7 +44,7 @@ def main():
|
||||
build = os.environ.get("HALO_BUILD_NUMBER") or os.environ.get("GITHUB_RUN_NUMBER") or "0"
|
||||
if not build.isdigit():
|
||||
build = "0"
|
||||
bundle = output / "ringfall-mjolnir-vr-steam-frame.zip"
|
||||
bundle = output / "mjolnir-vr-steam-frame.zip"
|
||||
with zipfile.ZipFile(bundle, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for path, name, mode in files:
|
||||
info = zipfile.ZipInfo.from_file(path, name)
|
||||
@@ -60,7 +60,7 @@ def main():
|
||||
print(f"{bundle.relative_to(Path.cwd()) if bundle.is_relative_to(Path.cwd()) else bundle}")
|
||||
|
||||
desktop = (ANDROID / "steam_frame_installer.desktop").read_text().replace("@REPOSITORY@", repository)
|
||||
installer = output / "Install-Ringfall-Mjolnir-VR.desktop"
|
||||
installer = output / "Install-Mjolnir-VR.desktop"
|
||||
installer.write_text(desktop)
|
||||
installer.chmod(0o755)
|
||||
print(installer)
|
||||
|
||||