Merge merge/upstream-2026-10-10: OpenCE network version 25, in-game settings, touch controls (off in VR), FOV options (off in VR)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan MaierandClaude Opus 5.5 committed 2026-10-10 11:11:50 +02:00
commit 697a85f1cb
510 files changed
+78151 -2032

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Files that a Linux shell runs directly must keep LF line endings in a
# Windows checkout (core.autocrlf): a CRLF shebang line does not start.
*.sh text eol=lf
port/android/gradlew text eol=lf
+31 -5
View File
@@ -8,8 +8,8 @@ name: Build
# - one job per platform builds both configurations, so its set-up (tools,
# downloads, caches) is paid once;
# - compiles go through ccache, whose cache carries over from run to run, so
# a commit only recompiles what it changed; downloads (SDL, musl, Gradle)
# are cached too;
# a commit only recompiles what it changed; downloads (SDL, musl, Gradle,
# the Linux build's system root) are cached too;
# - 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;
@@ -24,6 +24,11 @@ on:
paths-ignore:
- "**.md"
workflow_dispatch:
inputs:
profile:
description: "Also build the profiling flavour (configure.py --profile) on Linux"
type: boolean
default: false
concurrency:
group: build-${{ github.ref }}
@@ -41,7 +46,11 @@ env:
jobs:
linux:
# Arch Linux has what the build needs as packages: clang 22 (for the
# profile-guided optimisation in pgo/) and 32-bit SDL 3.
# profile-guided optimisation in pgo/), and CMake, pkg-config and
# wayland-scanner for the SDL 3 that the portable build brings. The build
# links against Debian 11's 32-bit glibc (tools/linux_sysroot.py), not
# Arch's, so that it starts on SteamOS and older distributions; Arch's
# 32-bit glibc is for the tests.
runs-on: ubuntu-24.04
container: archlinux:latest
timeout-minutes: 40
@@ -50,7 +59,7 @@ jobs:
run: |
printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf
pacman -Syu --noconfirm --needed git zstd python python-pytest ninja ccache \
clang lld gcc lib32-glibc lib32-gcc-libs lib32-sdl3
clang lld gcc lib32-glibc lib32-gcc-libs cmake pkgconf wayland
- uses: actions/checkout@v4
@@ -60,15 +69,28 @@ jobs:
key: ccache-linux-${{ github.sha }}
restore-keys: ccache-linux-
# the system root and SDL's source, which the build downloads
- uses: actions/cache@v4
with:
path: build/linux/third_party
key: linux-third-party-${{ hashFiles('tools/linux_sysroot.py') }}
- name: Debug
run: python tools/ci_build.py linux debug
# (the profiling build, configure.py --profile, which no other step
# compiles: only when a run started by hand asks for it, as it costs a
# build of its own)
- name: Profile
if: github.event_name == 'workflow_dispatch' && inputs.profile
run: python tools/ci_build.py linux profile
- name: Release
run: python tools/ci_build.py linux release
- name: Test
run: |
python -m pytest -q tools/test_linux_port.py tools/harness
python -m pytest -q tools/test_linux_port.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python tools/test_light_storage.py
- name: Compile cache
@@ -147,6 +169,10 @@ jobs:
HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
run: python3 tools/ci_build.py android release
# the touch controls' layout and gyroscope maths (the runner's JDK)
- name: Test the touch layout
run: python3 tools/test_touch_layout.py
- name: Compile cache
run: ccache --show-stats
+209
View File
@@ -0,0 +1,209 @@
# Notes for agents
This repository ports the Halo: Combat Evolved decompilation (Xbox build
01.01.14.2342, `cachebeta.exe`) to Linux, Windows and Android. The game's C
sources are the decompilation; the port adds a platform layer that
implements the Xbox APIs the game calls, and features the Xbox game did not
have (a new netcode, internet play, the PC version's menus, Custom Edition
maps, touch controls). The project is published as OpenCE.
Start with [README.md](README.md). The other documents are listed at the
end.
## Layout
| Path | Contents |
| --- | --- |
| `source/` | The game: 466 C files of the decompilation, compiled as they are with few changes (each marked, below) |
| `port/linux/src/` | The platform layer the three ports share: Direct3D 8 on OpenGL (`d3d8_gl.c`), DirectSound and XInput on SDL3, files, threads, memory, sockets, settings (`port_config.c`), internet play (`p2p*.c`) |
| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, the menus (`menu_tags.c`, `menu_functions.c`), Custom Edition maps (`custom_edition_*.c`, `cache_file_formats.c`), the tag validator (`tag_validate.c`, `tag_schema_*.c`), touch, FOV and more |
| `port/linux/include/` | The port's headers: the limits (`halo_port_limits.h`, with `HALO_PORT_NETWORK_VERSION`), capacities (`halo_port_capacity.h`), the prefix header, `halo_linux_source_fixups.h` (port functions the game's sources call) |
| `port/include/xdk/` | Stand-ins for the Xbox SDK's headers (declarations only) |
| `port/windows/` | The Windows build's own files (`win32_*.c`, headers) |
| `port/android/` | The Android app: the guest runtime, the host library (`host/`), the Java app (`app/`), host imports (`host_imports.list`) |
| `port/assets/` | What the builds embed or ship: high-res HUD (`hud/`), fonts, titles, menus (`menus/`), icons, network brokers |
| `port/third_party/` | Vendored libraries, each with a README naming its upstream, version and checksum |
| `port/tools/` | Stand-alone tools (`cache_file_report.c`) |
| `tools/` | Build scripts (`linux_build.py`, `windows_build.py`, `android_build.py`, `ci_build.py`), generators (`ce_menus.py`, `port_settings.py`, `hud_assets.py`, `title_assets.py`, `embed_assets.py`, `xdk_headers.py`) and tests |
| `docs/` | Longer design notes (`custom_edition_caches.md`) |
| `pgo/` | Profile-guided optimisation profiles |
| `assets/` | Game data for local runs (gitignored: `maps/`, `custom_maps/`) |
| `.github/workflows/` | CI: builds every platform on each push, publishes releases from `main` |
## Building
```sh
python configure.py # debug build; add --release, --portable, --profile, --pgo=off, --lto=off
ninja linux # build/linux/halo
ninja android # the Android guest image and native libraries
ninja android_apk # the APK (Gradle)
ninja windows # on Windows only
python tools/ci_build.py linux release # what CI builds
```
- `configure.py` writes `build.ninja`; `configure_args` at its top records
the options it was run with. Restore them after trying other options.
- The Linux and Windows builds are 32-bit x86 with clang, in MSVC's ABI
(`-fms-extensions -fshort-wchar -malign-double -fcommon`), C89 (`gnu89`):
declarations come before statements.
- Every native build compiles without fused multiply-add and with the
vendored musl maths (`halo_math.h`), so that every machine of a network
game computes the same results. Do not call the system's maths functions.
- Android: the game is an ILP32 AArch64 guest (`arm64_32`) in a 64-bit app.
Guest code can call only the host imports declared in
`port/android/host_imports.list` and `port/android/guest/runtime/guest_host.h`,
and only the SDL functions `guest_sdl.c` bridges. Run `ninja android`
after changing anything it compiles: `port/linux/src`, `port/linux/game`,
`port/android`, `source/`, the build scripts.
### Defines
| Define | Set by | Meaning |
| --- | --- | --- |
| `HALO_ANDROID` | Android | The app: its display, input, files and lifecycle |
| `HALO_GLES` | Android | The OpenGL ES renderer |
| `HALO_ARM64_GUEST` | Android | The guest's ABI (ILP32 AArch64) |
| `HALO_WINDOWS` | Windows (`halo_windows_prefix.h`) | The Windows build |
| `HALO_RELEASE` | `--release` | No assertions checked |
| `HALO_PROFILE` | `--profile` | The profiling build; a normal build must be unchanged by it |
`HALO_LINUX` and `HALO_CUSTOM_EDITION` are **not** defined by any build:
code guarded by them compiles to nothing. Custom Edition support is a
setting (`game.custom_edition`), checked at run time with
`custom_edition_cache_tags_loaded()`.
## Testing
```sh
python -m pytest -q tools/harness tools/test_touch_menu.py # the engine's functions in a fake world
python -m pytest -q tools/test_linux_port.py # the build, the maps' checks (needs assets/maps)
python -m pytest -q tools/test_cache_file_formats.py tools/test_bmp_files.py tools/test_profile.py
python tools/test_touch_layout.py # the Android touch layout (JDK 17+)
python tools/test_light_storage.py
python tools/test_death_timing.py
```
`tools/harness/README.md` explains how to add an asset-free test: the code
under test is taken from the sources, not copied.
Running the game without a person at it:
- `debug.network_test` (`HALO_NETWORK_TEST=host:<map>` or `join`) hosts or
joins a game without the menus and logs each player's state every second;
several copies play on one machine on different loopback addresses
(`HALO_NET_ADDRESS`, `HALO_NET_BROADCAST`). `NETCODE.md` describes the
test settings.
- `debug.exit_after`, `debug.hidden_window`, `debug.null_renderer`,
`debug.menu_open` (start on one menu screen), `debug.telnet_console`.
- Use a virtual display (Xvfb, with `SDL_VIDEO_DRIVER=x11`) for windowed
runs, never the desktop of the person you work for, and stop only the
processes you started.
## Rules of the project
### The game's sources
- Change `source/` as little as possible, and mark each change from the
original with a comment that starts `port:`. Write like the surrounding
code: its naming, its comment density, and the project's plain,
descriptive comment style.
- A port function the game's sources call is declared in
`halo_linux_source_fixups.h` or included with a note naming its file
(`#include "view_fov.h" /* port: port/linux/game/view_fov.c */`).
- Notes such as `BUG (original, preserved for exact matching)` mark the
original's bugs. Fixing one is allowed; say so in a `port:` comment.
### The Xbox SDK
`port/include/xdk` was written without the SDK's headers, from the January
build's debug information and public documentation. Never read the SDK's
headers or anything derived from them, never copy from them, and do not
read `xbox/include`. To add a name, follow `port/include/xdk/README.md`.
### Networking
- Raise `HALO_PORT_NETWORK_VERSION` (`port/linux/include/halo_port_limits.h`)
with any change to what the machines send each other, or to anything they
must agree on, and say why in the commit. Machines of different versions
do not play together.
- Only the host decides; clients predict their own players. Read
`port/linux/NETCODE.md` before changing the netcode.
- Anything another machine sends is untrusted: check sizes, indices and
rates, as the existing handlers do. `debug.network_corrupt` tests this.
### Menus
- The menus must match Halo Custom Edition's. Their XML (`port/assets/menus/ce/`)
is generated by `tools/ce_menus.py` from the PC version's tags; the
settings screens come from `tools/port_settings.py`. Do not edit the XML
by hand: change the generator and run it (the command is in
`port/assets/menus/README.md`, "Writing them again"), then check that
`git status` shows only the screens meant to change.
- Functions the menus call but the port has not wired are listed in
`port/assets/menus/UNWIRED.md`.
### Art and credit
- Replacement HUD, title and menu art is made of hand-drawn redraws.
- Prefer an existing redraw (the ui-svg-handmade set), then a new redraw. A
Custom Edition picture that cannot reasonably be redrawn (a 3D render, a
screenshot, a logo) may be used only if it is listed in
`port/assets/menus/NON_HANDDRAWN.md`.
- `AUTHORS.md` credits art only.
- Fonts: Overpass and OpenCE (Newtown, respaced) only; see
`port/assets/fonts/README.md`. Never ship the maps' commercial typefaces.
### Custom Edition maps
Custom Edition maps live in the data root's `custom_maps` folder with
`bitmaps.map`, `sounds.map` and `loc.map`; their level names are
`custom_maps\<name>`. OpenSauce features are not supported and must not be
added: a map that needs them is refused, and one that only carries
OpenSauce's header runs as stock Custom Edition runs it. Map files are
untrusted input: read them through bounds-checked readers. See
`docs/custom_edition_caches.md`.
### Settings
A setting is an entry in `port/linux/src/port_config.c`, with a default, an
environment variable and a comment that `config.toml` gets. Document it in
the settings table of `port/linux/README.md` (and the Android README for an
Android-only one). A departure from the stock game's look or behaviour
should be a setting, off or stock by default, unless the project decides
otherwise.
### Third-party code and licences
The repository is CC0. Vendored code goes in `port/third_party/<name>`,
copied unchanged, with a README naming the upstream, the version or commit,
the checksum and what uses it. GPL code (OpenSauce, Reclaimer) may be read
as documentation of file formats, never copied.
### Commits and pull requests
- Commit subjects are plain sentences saying what changed; the body says
why and what was checked.
- A pull request is merged with a merge commit (`Merge pull request #N from
<owner>/<branch>`), and follow-up changes go in separate commits whose
subject ends with `(#N)`.
- A commit taken from a fork keeps its author and is made with
`git cherry-pick -x`.
## Documents
| Document | About |
| --- | --- |
| [README.md](README.md) | Downloads, game data, building, build options, profiling builds |
| [port/linux/README.md](port/linux/README.md) | The Linux build, controls, menus, every setting, updates, frame rate, field of view, system link, internet play, voice chat, the server browser, security, map checks, how the port operates, the game source changes |
| [port/linux/NETCODE.md](port/linux/NETCODE.md) | The distributed netcode, network versions, joining a game in progress, transport, testing |
| [port/windows/README.md](port/windows/README.md) | The Windows build, its headers, inline functions, crash reports |
| [port/android/README.md](port/android/README.md) | The Android build, the guest and host, touch controls, Android settings, finding problems |
| [port/include/xdk/README.md](port/include/xdk/README.md) | How the SDK declarations were written and are checked |
| [docs/custom_edition_caches.md](docs/custom_edition_caches.md) | Loading and converting Custom Edition maps, and the evidence for each layout |
| [port/assets/menus/README.md](port/assets/menus/README.md) | The menu files' format and how to write them again |
| [port/assets/menus/NON_HANDDRAWN.md](port/assets/menus/NON_HANDDRAWN.md) | Menu pictures that are not redraws |
| [port/assets/menus/UNWIRED.md](port/assets/menus/UNWIRED.md) | Menu functions that do nothing yet |
| [port/assets/fonts/README.md](port/assets/fonts/README.md) | The fonts and their licences |
| [tools/harness/README.md](tools/harness/README.md) | Asset-free regression tests |
| `port/third_party/*/README.md` | Each vendored library |
| [AUTHORS.md](AUTHORS.md) | Art credits |
+14
View File
@@ -0,0 +1,14 @@
# Authors
## Art
- **The game's icon** (the Android app's launcher icon, `halo.exe`'s icon
and the desktop windows' icon): by ORION (`port/assets/icon`).
## Third-party art
- **Voice chat's speaker icons** (who talks, and who is muted, in the lobby,
on the scoreboard and in the game): `volume-2` and `volume-x` from
[Lucide](https://lucide.dev) 1.53.0, copyright (c) 2026 Lucide Icons and
Contributors, under the ISC licence
(`port/assets/icons/lucide/LICENSE`).
+17 -2
View File
@@ -12,7 +12,7 @@ from types import SimpleNamespace
from tools import ninja_syntax
from tools.android_build import android_configure_inputs, generate_android_build
from tools.linux_build import generate_linux_build, linux_configure_inputs
from tools.linux_build import check_profile_options, generate_linux_build, linux_configure_inputs
from tools.windows_build import generate_windows_build, windows_configure_inputs
# arguments
@@ -33,6 +33,12 @@ parser.add_argument(
action="store_true",
help="release builds (Linux, Windows, Android): assertions are not checked",
)
parser.add_argument(
"--profile",
action="store_true",
help="profiling builds (Linux, Windows, Android): CPU scopes recorded on a "
"console command or launch setting (README, \"Profiling builds\"); not with --pgo=train",
)
parser.add_argument(
"--lto",
choices=["full", "thin", "off"],
@@ -44,7 +50,11 @@ parser.add_argument(
"--portable",
action="store_true",
help="x86 builds (Linux, Windows): code for any x86-64 processor (SSE2) rather than for this "
"machine's (-march=native, the default); use it for builds that run on other computers",
"machine's (-march=native, the default); use it for builds that run on other computers. On Linux it also "
"builds against Debian 11's glibc 2.31 rather than this machine's, and brings its own SDL 3 (libSDL3.so.0, "
"beside the executable), so that it starts on SteamOS and older distributions: the first build downloads "
"the Debian packages and SDL's source (build/linux/third_party, tools/linux_sysroot.py), and needs CMake, "
"pkgconf and wayland-scanner, not the 32-bit SDL 3",
)
parser.add_argument(
"--pgo",
@@ -74,6 +84,10 @@ parser.add_argument(
help="clang with the arm64_32 target for the Android guest (default: clang)",
)
args = parser.parse_args()
try:
check_profile_options(args.profile, args.pgo)
except ValueError as error:
parser.error(str(error))
# the settings the builds read
sln = SimpleNamespace(
@@ -81,6 +95,7 @@ sln = SimpleNamespace(
linux_cc=args.linux_cc,
compiler_launcher=args.compiler_launcher,
port_release=args.release,
port_profile=args.profile,
port_lto=args.lto,
port_portable=args.portable,
port_pgo=args.pgo,
+39 -1
View File
@@ -115,10 +115,11 @@ Give these options to `configure.py`:
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. Use this option for builds that you give to other persons. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](../port/linux/README.md#portable-build). Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
@@ -143,3 +144,40 @@ To record a new profile:
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
### Profiling builds
A profiling build records where the game spends its time.
1. Enter `python configure.py --profile`, then build as usual.
2. Start a recording. Enter `profile_record` in the console (or the telnet
console), or set `debug.profile_record = true` in `config.toml`. To
record each game of a session, also set
`debug.profile_record_when = "game"`.
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
and logs their paths. Parts are retained after the recording stops. Logs go to
standard error; a Windows release build without standard error writes them to
`halo.log` next to `halo.exe`.
`tools/net_report.py` is a helper for developers. Enter
`python tools/net_report.py <recording.part1.json>` to read a recording
as tables. The command also writes `<recording>.summary.txt`, a short text
file.
| Command | Result |
| --- | --- |
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
| `profile_stop` | Stops the recording. |
`debug.profile_memory` sets the memory of a recording in MB (default 256,
from 4 to 1024). A recording has no length limit:
the game writes a part each time the memory is full. Refer to "Settings" in
[port/linux/README.md](../port/linux/README.md).
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
Enter `adb pull` to copy the recording parts to a computer.
A profiling build plays with normal builds. It does not use
`--pgo=train`.
+140 -14
View File
@@ -8,8 +8,9 @@ its Java classes keep the decompilation's package, `com.halo.decomp`).
The game shows its graphics with OpenGL ES 3. It plays sound through SDL3
(AAudio). It accepts input from game controllers, for example a PlayStation
5 DualSense on Bluetooth. The app needs Android 9 (API 28) or later. It
operates on 64-bit-only devices, for example the Pixel 9 Pro XL.
5 DualSense on Bluetooth, and from the touchscreen (refer to "Controls").
The app needs Android 9 (API 28) or later. It operates on 64-bit-only
devices, for example the Pixel 9 Pro XL.
The Android build uses the platform layer of the Linux build
(`port/linux/src`). Refer to [port/linux/README.md](../linux/README.md).
@@ -33,6 +34,36 @@ ninja) and these items:
1.2.5 and SDL 3.4.16 to `build/android/third_party`. Gradle downloads the
Android Gradle Plugin.
On Windows, build in WSL2 (Ubuntu 24.04). The Android build does not run
on Windows itself. Enter
`wsl -d Ubuntu -- bash tools/wsl_setup_android.sh` one time: it installs
the tools and the Android SDK and NDK in WSL (apt asks for your
password). Then enter
`wsl -d Ubuntu -- bash tools/wsl_build_android.sh` for each build. The
script copies the repository into WSL (`~/halo-build/src`) with LF line
endings (a Windows checkout has CRLF line endings, which the build's text
tools do not accept), builds there, and puts the APK in
`dist/android/app-debug.apk`. Only the changed files are copied, so the
next build is incremental. Install the APK with the adb of Windows.
A build that you make yourself has a different signature from the builds
of GitHub Actions. Android installs it only after you remove the app, and
removing the app deletes its data folder (`maps/`, `save/` and
`config.toml`). Thus a change between the builds of GitHub Actions and your
own builds loses the saved games, unless you copy them first (below).
Your own builds all have the same signature (the debug key of WSL,
`~/.android`), so they install over each other and keep the data
(`adb install -r`).
To keep the saved games before you remove the app, enter
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
After the new install, before the first start of the app, enter
`adb push u /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u`. The app reads
and overwrites what adb pushes. adb cannot add files to `save/u` or `save/z` once the app made them, or
change files that the app wrote, so push before the first start. adb cannot read
some files that the app writes (the `blam.lst` files of `save/z`), so `adb pull`
of the whole `save` folder stops there.
## Build and install the app
1. Go to the root folder of the repository.
@@ -71,7 +102,9 @@ To install the data from a computer:
| Settings | `config.toml` |
To make a copy of the saved games, enter
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save`.
`adb pull /sdcard/Android/data/io.github.cesarus85.ringfall/files/save/u` (the profiles).
An `adb pull` of the whole `save` folder stops at the `blam.lst` files of
`save/z`, which adb cannot read.
## Controls
@@ -97,8 +130,82 @@ with the positions on the Xbox controller:
| Create | back | |
The controller gets the rumble. The back gesture of Android is the B
button. A Bluetooth or USB keyboard operates as on Linux. The screen does
not accept touch input.
button. A Bluetooth or USB keyboard operates as on Linux.
The touchscreen operates the menus: tap an item to select it (on a
setting with values, tap its left or right half), tap a button of the key
at the bottom of a screen (for example "B = Back") to push it, and drag to
scroll a list (down or right steps back, up or left steps forward). A drag
stops at the first and the last item, and it does not change a setting's
value. Touches that start in the edge-gesture zones of Android do not tap
or scroll at the sides, and do not scroll at the top and bottom, because the
first swipe from an edge in full screen only shows the system bars.
A tap during a cinematic that can be skipped skips it, as A does. On the
on-screen keyboard, tap a key to press it, "B =BACK" to cancel and
"A =ENTER" to accept the name.
### Touch controls
In a game, the app shows touch controls over the picture. They are a
controller for player 1. With the default buttons of the profile:
| Control | Function in the game |
| --- | --- |
| stick (lower left) | move |
| swipe on the screen away from the buttons | look |
| Fire | right trigger |
| Grenade | left trigger |
| A / Jump, B / Melee, X / Reload, Y / Weapon | A, B, X, Y |
| Crouch, Zoom | left and right stick clicks |
| Light, Gren. type | white, black |
| Pause, Back | start, back |
| Up, Down, Left, Right | D-pad |
The names on the buttons follow the profile's "Button layout" (Settings >
Gamepads): with "Swap triggers", the right trigger's button says "Grenade".
The stick moves the player with every "Stick layout", southpaw too.
A finger that holds a button can also swipe to look, so you can fire and
aim with one thumb. The controls are a finger wide (48 dp) or larger, but
on a small screen of high density they stay apart rather than reach that
size. A short tap reaches the game even when it is shorter than one frame.
Swipes that start in the edge-gesture zones of Android do not turn the
view. The swipe aims as the controller's stick does, not as a mouse: the
aim slows over a target and follows a moving one, as with a controller,
and it follows the profile's "invert look" (the gyroscope turns the view
as the phone turns, never inverted). The setting `input.touch_aim_assist`
turns the aim assist off. "Look sensitivity" sets how far a swipe turns;
the mouse settings do not apply.
The touch controls show only in a game. In the menus and during
cinematics they hide, and the touchscreen operates the menus as described
above. They also hide when a controller is connected, for example the
built-in controller of a handheld. A device without a touchscreen (a TV)
never shows them. The setting `input.touch_controls` changes this (refer
to "Settings").
The buttons at the top of the screen:
- "Hide" removes the controls (and stops the gyroscope aiming and the
vibration) until you push "Touch"; the app remembers it.
- "Options" opens these items:
- "General": the phone's vibration (on by default; it follows the
vibration setting of the game's profile), aiming with the gyroscope
(off by default), a floating move stick (off by default: the stick goes
where your thumb lands in the lower left of the screen), the opacity of
the controls, "Hide or add buttons" (hide a button, add a copy of a
button) and "Edit buttons size".
- "Edit buttons layout": drag the controls to new positions, then push
"Save". "Export" and "Import" write and read a layout file with the
file picker of the system.
- "Look sensitivity".
"Hide" and "Options" act when the finger lifts on them: a swipe that
starts on them turns the view instead.
The app keeps the layout in its own preferences, not in `config.toml`.
Removing the app's data or the app removes the layout.
## Settings
@@ -118,8 +225,11 @@ These settings are only for Android:
| Setting | Function |
| --- | --- |
| `input.touch_aim_assist` | `true` (the default): the touch controls' swipe aiming gets the aim assist of a controller (the aim slows over a target and follows a moving one). `false`: none, as with a mouse; the bullets' own autoaim stays. |
| `input.touch_controls` | The touch controls in a game. `"auto"` (the default): shown on a touchscreen while no controller is connected. `"on"`: also shown with a controller. `"off"`: never shown. A device without a touchscreen never shows them. The menus take taps in any case. |
| `display.screen_width` | The number of columns of the 480-line picture. `0` (the default): the shape of the display (1068 on a 20:9 phone). `640`: the 4:3 shape of the Xbox. |
| `debug.sample_seconds` | Refer to "Find problems". |
| `debug.memory_watch` | `true` (the default): the app notices the game's writes to textures and vertices by page protection. `false`: it compares page contents once a frame instead, which is slower. Refer to "Limits". |
## Internet play
@@ -170,7 +280,7 @@ key when `port/android/signing.properties` (not in the repository) names it:
Keep the keystore and its password safe: a lost key means every player must
remove the app before the next version. If you installed a build that has a different signature, remove
that build before you install a new build. Removing the app deletes its data
folder: first make a copy of `maps/` and `save/`.
folder: first make a copy of `maps/` and `save/u`.
## Widescreen
@@ -184,6 +294,8 @@ the Xbox:
- Black bars and fades cover all of the screen, and so do the menus' dims
and backgrounds (the pause menu's dim, dialogs, the menus' gradient).
All the ports draw this way.
## VR
On a VR headset with an OpenXR runtime (Meta Quest 3 and 3S; the Steam
@@ -253,12 +365,6 @@ Not done yet: aiming with the controller and the weapon in the hand, the
HUD's parts on the controllers, zoom through the scope, a first-person view
in vehicles, and a separate depth for the HUD's reticle.
## How it works
The changes are in `#ifdef HALO_ANDROID` in `rasterizer_xbox.c`, `render.c`,
`ui_widget.c`, `cinematics.c`, `main.c` and
`rasterizer_xbox_screen_effect.c`.
## How the port operates
### ILP32 code
@@ -362,9 +468,17 @@ floating-point contraction, as on x86.
### Game source changes
The port's code tells three things apart, each its own macro, which the
Android build defines all of (`tools/android_build.py`):
- `HALO_ARM64_GUEST`: the guest's ABI (ILP32 AArch64 code in a 64-bit
process);
- `HALO_GLES`: the OpenGL ES renderer;
- `HALO_ANDROID`: the app (its display, input, files and lifecycle).
The x86 inline assembly is replaced by C (refer to
[port/linux/README.md](../linux/README.md#game-source-changes)).
These changes are in `#ifdef HALO_ANDROID`:
These changes are in `#ifdef HALO_ARM64_GUEST`:
- Seven `#pragma bss_seg(".bss")` lines are removed. The Darwin target does
not accept them.
@@ -391,6 +505,8 @@ assembly of the port is necessary:
at this interval. This finds hangs on devices without root access.
- Set `gl_debug = true` in `[debug]` of `config.toml`. The log then shows
the OpenGL ES errors.
- The log says which write tracking the app uses: "page protection"
(devices) or "page hashes" (the emulator, or `debug.memory_watch = false`).
## Limits
@@ -398,6 +514,16 @@ assembly of the port is necessary:
- The device must let the app reserve the fixed guest addresses, from
`0x80000000` to approximately `0x89000000`. If the addresses are not
available, the app shows a message.
- The game does not accept touch input. Use a controller or a keyboard.
- Touch operates the menus and skips cinematics. In the game, the touch
controls appear when no controller is connected; their size follows the
height of the screen (larger on a tablet than on a phone), never smaller
than a finger.
- Kernels with 16 KB pages (a developer option of Android 15) do not
operate. The Xbox memory uses 4 KB pages.
- The x86 Android emulator runs the app through its ARM translation. The
translation cannot deliver the page faults that the renderer uses to
notice changed textures and vertices. On the emulator the app compares
page contents once a frame instead ("write tracking: page hashes" in the
log). This is slower, and a write shows one frame later, so moving
geometry can glitch briefly. `debug.memory_watch = false` does the same
on a device.
+1 -1
View File
@@ -23,7 +23,7 @@ named where code is taken over.
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
| Comfort | Snap or smooth turning, a comfort vignette in vehicles, VR options in the main and pause menus (Y), a page of every controller button |
| Menus | The PC version's menus in VR (`vr.menus`), clicked with a laser; text typed on the on-screen keyboard |
| Multiplayer | Up to date with OpenCE (network version 24, 2026-10-08): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Multiplayer | Up to date with OpenCE (network version 25, 2026-10-10): cross-play, its server browser, co-op campaign, password lobbies; join codes and the VR online list |
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
@@ -19,9 +19,15 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- voice chat's microphone (asked for only when voice chat first talks) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- installing a new version of the game (Updater) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- enableOnBackInvokedCallback: the back button and the back gesture
stay KEYCODE_BACK, which SDL passes to the game as B; with
predictive back (the default once an app targets Android 16) the
system would take them and the game would never see them -->
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
@@ -29,7 +35,8 @@
android:isGame="true"
android:appCategory="game"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
android:hardwareAccelerated="true">
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="false">
<!-- a game, for the system (isGame and appCategory above; the game
modes' configuration) and for launchers that look for this -->
@@ -0,0 +1,71 @@
package com.halo.decomp;
/**
* Gyroscope aiming (TouchControls): integrates the sensor's angular velocity,
* turned into the display's coordinates for its rotation, into radians of
* view turn between samples.
*/
final class GyroscopeAim {
/** below this (radians a second) a reading is the sensor's noise at rest, which must not drift the view */
private static final float NOISE = 0.01f;
/** a gap longer than this (seconds) starts over rather than turn at once */
private static final float LONGEST_GAP = 0.1f;
private long previousTime;
private int previousRotation = -1;
private float previousX;
private float previousY;
void reset() {
previousTime = 0;
previousRotation = -1;
}
/**
* takes one sample (its time in nanoseconds, the angular velocity about
* the device's x and y axes, the display's rotation 0..3) and puts the turn
* since the last into delta {yaw, pitch}; false while there is none
*/
boolean sample(long time, float x, float y, int rotation, float[] delta) {
delta[0] = 0;
delta[1] = 0;
if (!Float.isFinite(x) || !Float.isFinite(y) || rotation < 0 || rotation > 3) {
reset();
return false;
}
float screenX = x;
float screenY = y;
switch (rotation) {
case 1:
screenX = y;
screenY = -x;
break;
case 2:
screenX = -x;
screenY = -y;
break;
case 3:
screenX = -y;
screenY = x;
break;
default:
break;
}
if (Math.abs(screenX) < NOISE)
screenX = 0;
if (Math.abs(screenY) < NOISE)
screenY = 0;
float seconds = (time - previousTime) * 1e-9f;
boolean valid = previousTime != 0 && rotation == previousRotation && seconds > 0 && seconds <= LONGEST_GAP;
if (valid) {
// the trapezoid rule over the two samples
delta[0] = -(screenY + previousY) * 0.5f * seconds;
delta[1] = -(screenX + previousX) * 0.5f * seconds;
}
previousTime = time;
previousRotation = rotation;
previousX = screenX;
previousY = screenY;
return valid && (delta[0] != 0 || delta[1] != 0);
}
}
@@ -1,10 +1,25 @@
package com.halo.decomp;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Insets;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import org.libsdl.app.SDLActivity;
@@ -15,6 +30,16 @@ import org.libsdl.app.SDLActivity;
public class HaloActivity extends SDLActivity {
/** lets system link's broadcasts in over Wi-Fi while the game runs */
private WifiManager.MulticastLock multicastLock;
/**
* the latest system gesture insets {left, top, right, bottom}, in pixels;
* written on the UI thread, read by the game's thread: replaced as a
* whole, never changed in place
*/
private volatile int[] gestureInsets = new int[] { 0, 0, 0, 0 };
/** the on-screen touch controls, over SDL's surface */
private TouchControls touchControls;
private static final int EXPORT_LAYOUT = 401, IMPORT_LAYOUT = 402;
private String pendingLayoutExport;
@Override
protected String[] getLibraries() {
@@ -24,21 +49,208 @@ public class HaloActivity extends SDLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
pendingLayoutExport = savedInstanceState.getString("pending-layout-export");
if (mLayout != null) {
touchControls = new TouchControls(this);
mLayout.addView(touchControls, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
preferHighestRefreshRate();
trackGestureInsets();
acquireMulticastLock();
// a new version looked for while the game starts
Updater.start(this);
}
/** the system's file picker, for the touch layout; no storage permission */
public void chooseLayoutFile(boolean export, String configuration) {
String message = export
? "Choose the folder and the name of the touch layout file."
: "Choose a touch layout file. It replaces your buttons, the look sensitivity and the General settings.";
new AlertDialog.Builder(this)
.setTitle(export ? "Export the layout" : "Import a layout")
.setMessage(message)
.setNegativeButton("Cancel", null)
.setPositiveButton(export ? "Choose the place" : "Choose the file", (dialog, which) -> {
Intent intent = new Intent(export ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(export ? "text/plain" : "*/*");
if (export) {
pendingLayoutExport = configuration;
intent.putExtra(Intent.EXTRA_TITLE, "halo-touch-layout.halolayout");
}
try {
startActivityForResult(intent, export ? EXPORT_LAYOUT : IMPORT_LAYOUT);
} catch (android.content.ActivityNotFoundException e) {
pendingLayoutExport = null;
Toast.makeText(this, "This device has no file picker.", Toast.LENGTH_LONG).show();
}
})
.show();
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putString("pending-layout-export", pendingLayoutExport);
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
if (request != EXPORT_LAYOUT && request != IMPORT_LAYOUT) {
super.onActivityResult(request, result, data);
return;
}
String exported = pendingLayoutExport;
pendingLayoutExport = null;
if (result != RESULT_OK || data == null || data.getData() == null)
return;
Uri document = data.getData();
// (the file is read and written away from the UI thread)
new Thread(() -> {
try {
if (request == EXPORT_LAYOUT)
writeLayoutFile(document, exported);
else
readLayoutFile(document);
} catch (Exception e) {
runOnUiThread(() -> layoutFileError(e));
}
}, "halo-touch-layout-file").start();
}
private void writeLayoutFile(Uri document, String configuration) throws IOException {
if (configuration == null)
throw new IOException("the layout to export is gone");
try (OutputStream output = getContentResolver().openOutputStream(document, "wt")) {
if (output == null)
throw new IOException("the file cannot be opened");
output.write(configuration.getBytes(StandardCharsets.UTF_8));
}
runOnUiThread(() -> Toast.makeText(this, "The layout is exported.", Toast.LENGTH_SHORT).show());
}
private void readLayoutFile(Uri document) throws IOException {
ByteArrayOutputStream contents = new ByteArrayOutputStream();
try (InputStream input = getContentResolver().openInputStream(document)) {
if (input == null)
throw new IOException("the file cannot be opened");
byte[] buffer = new byte[4096];
int size;
while ((size = input.read(buffer)) != -1) {
if (contents.size() + size > 65536)
throw new IOException("the file is too large");
contents.write(buffer, 0, size);
}
}
String configuration = new String(contents.toByteArray(), StandardCharsets.UTF_8);
// checked here, then applied to the overlay at once
TouchLayout.importConfiguration(configuration);
runOnUiThread(() -> {
if (isFinishing() || isDestroyed() || touchControls == null)
return;
try {
touchControls.importLayout(configuration);
Toast.makeText(this, "The layout is imported and saved.", Toast.LENGTH_SHORT).show();
} catch (IllegalArgumentException e) {
layoutFileError(e);
}
});
}
private void layoutFileError(Exception error) {
if (isFinishing() || isDestroyed())
return;
new AlertDialog.Builder(this)
.setTitle("Touch layout file")
.setMessage("The operation did not complete: " + error.getMessage())
.setPositiveButton("OK", null)
.show();
}
@Override
protected void onResume() {
super.onResume();
if (touchControls != null && getWindow().getDecorView().hasWindowFocus())
touchControls.startDeviceInput();
}
@Override
protected void onPause() {
if (touchControls != null)
touchControls.stopDeviceInput();
super.onPause();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (touchControls != null) {
if (hasFocus)
touchControls.startDeviceInput();
else
touchControls.stopDeviceInput();
}
super.onWindowFocusChanged(hasFocus);
}
@Override
protected void onDestroy() {
if (touchControls != null)
touchControls.stopDeviceInput();
if (multicastLock != null && multicastLock.isHeld())
multicastLock.release();
multicastLock = null;
super.onDestroy();
}
/**
* keeps gestureInsets current; Android sends the insets again when the
* activity rotates (after the game's native code has started on a phone
* launched from portrait), so a single read at startup would keep the
* portrait values; the listener hands the insets on so SDL's own
* handling still sees them
*/
private void trackGestureInsets() {
getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
gestureInsets = readGestureInsets(insets);
return view.onApplyWindowInsets(insets);
}
});
}
@SuppressWarnings("deprecation") // getSystemGestureInsets is the only call on Android 10
private static int[] readGestureInsets(WindowInsets insets) {
Insets gesture;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
gesture = insets.getInsets(WindowInsets.Type.systemGestures());
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
gesture = insets.getSystemGestureInsets();
else
return new int[] { 0, 0, 0, 0 };
return new int[] { gesture.left, gesture.top, gesture.right, gesture.bottom };
}
/**
* the edges of the screen where Android keeps its gestures; in sticky
* full screen the first swipe from an edge only shows the system bars,
* and Android hands that swipe to the game as an ordinary finger, so
* the game (port/linux/src/touch_input.c) must ignore touches that
* begin there; returns a copy of {left, top, right, bottom} in pixels:
* all 0 before Android 10 (which has no insets) and until the first
* insets arrive. The game's native code calls this through JNI by name
* and signature (host_main.c GetMethodID(...,
* "getSystemGestureInsetsPixels", "()[I")), at every finger down, so
* it must not be renamed, retyped or removed as unused
*/
public int[] getSystemGestureInsetsPixels() {
return gestureInsets.clone();
}
/**
* Many phones drop the Wi-Fi's broadcast and multicast datagrams to
* save power unless an app holds this: without it they would not see
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,335 @@
package com.halo.decomp;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Properties;
/**
* The touch controls' layout (TouchControls): each control's kind, place,
* size and whether it shows, with the settings that go with them, in the
* overlay's logical units. Places are kept for the display the layout is
* on and saved on a 960 x 540 grid, so a layout moves between displays of
* other shapes. A layout file is Java properties text (version 3; 1 and 2
* are still read).
*/
final class TouchLayout {
/** the kinds of control: a button of TouchControls's list, by index */
static final int LEFT = 16;
static final int FIRE_LEFT = 17;
static final int BASE_COUNT = 18;
static final int MAX_CONTROLS = 64;
static final float MIN_SIZE = 0.5f;
static final float MAX_SIZE = 2f;
static final float MIN_OPACITY = 0.2f;
static final float MAX_OPACITY = 1f;
static final float MIN_SENSITIVITY = 0.25f;
static final float MAX_SENSITIVITY = 4f;
/**
* the largest minimum radius: a finger's width is capped here on a small,
* dense display, where the D-pad's buttons (69 apart) would overlap
*/
static final float MAX_MINIMUM_RADIUS = 34;
/** the saved grid */
private static final float GRID_WIDTH = 960;
private static final float GRID_HEIGHT = 540;
private static final int VERSION = 3;
private static final int MAX_FILE_LENGTH = 65536;
/** each kind's default place on the grid, and its radius */
private static final float[][] DEFAULTS = {
{856, 447}, {913, 377}, {794, 377}, {850, 312}, {915, 239}, {802, 239},
{236, 449}, {691, 449}, {360, 490}, {438, 490}, {570, 36}, {390, 36},
{100, 237}, {100, 335}, {51, 286}, {149, 286}, {115, 440}, {255, 239}
};
private static final float[] RADII = {
36, 32, 34, 32, 39, 35, 32, 32, 27, 29, 28, 28, 25, 25, 25, 25, 64, 39
};
private static final class Control {
final int type;
float x;
float y;
boolean shown = true;
float size = 1f;
Control(int type, float x, float y) {
this.type = type;
this.x = x;
this.y = y;
}
}
/** a layout and the sensitivity saved with it */
static final class Configuration {
final TouchLayout layout;
final float sensitivity;
Configuration(TouchLayout layout, float sensitivity) {
this.layout = layout;
this.sensitivity = sensitivity;
}
}
private final ArrayList<Control> controls = new ArrayList<>();
private float width = GRID_WIDTH;
private float height = GRID_HEIGHT;
/** the smallest radius a control is drawn and touched at, in logical units */
private float minimumRadius;
boolean rumbleEnabled = true;
boolean gyroscopeEnabled = false;
/** how solid the controls are drawn, MIN_OPACITY..MAX_OPACITY */
float opacity = 1f;
/** the move stick goes where the thumb lands in the lower left, not at its place */
boolean floatingStick = false;
TouchLayout() {
resetDefaults();
}
/** places the layout on a display of this size, in logical units */
void bounds(float width, float height) {
if (!Float.isFinite(width) || !Float.isFinite(height) || width <= 0 || height <= 0)
throw new IllegalArgumentException("Invalid display dimensions");
for (Control control : controls) {
control.x *= width / this.width;
control.y *= height / this.height;
}
this.width = width;
this.height = height;
keepAllInside();
}
/**
* the smallest radius, in logical units, at which a control is drawn and
* touched (a finger's width, whatever the display's density), at most
* MAX_MINIMUM_RADIUS
*/
void setMinimumRadius(float radius) {
minimumRadius = Float.isFinite(radius) && radius > 0 ? Math.min(radius, MAX_MINIMUM_RADIUS) : 0;
keepAllInside();
}
int size() {
return controls.size();
}
int type(int control) {
return controls.get(control).type;
}
float x(int control) {
return controls.get(control).x;
}
float y(int control) {
return controls.get(control).y;
}
float savedX(int control) {
return x(control) * GRID_WIDTH / width;
}
float savedY(int control) {
return y(control) * GRID_HEIGHT / height;
}
float radius(int control) {
return Math.max(RADII[type(control)] * sizeScale(control), minimumRadius);
}
float sizeScale(int control) {
return controls.get(control).size;
}
void setSize(int control, float size) {
if (!Float.isFinite(size) || size < MIN_SIZE || size > MAX_SIZE)
throw new IllegalArgumentException("Button size must be between 50% and 200%");
controls.get(control).size = size;
move(control, x(control), y(control));
}
boolean shown(int control) {
return controls.get(control).shown;
}
void setShown(int control, boolean shown) {
controls.get(control).shown = shown;
}
void setOpacity(float opacity) {
if (!validOpacity(opacity))
throw new IllegalArgumentException("Opacity must be between 20% and 100%");
this.opacity = opacity;
}
/** the default controls, at their default places, sizes and visibility */
void resetDefaults() {
controls.clear();
for (int i = 0; i < DEFAULTS.length; i++)
controls.add(new Control(i, DEFAULTS[i][0] * width / GRID_WIDTH, DEFAULTS[i][1] * height / GRID_HEIGHT));
keepAllInside();
}
/** adds a copy of a control beside it; -1 if there may be no more (or it is the stick) */
int duplicate(int source) {
if (size() >= MAX_CONTROLS || type(source) == LEFT)
return -1;
float offset = radius(source) * 2 + 12;
Control added = new Control(type(source), x(source) + offset, y(source));
added.size = sizeScale(source);
controls.add(added);
int index = size() - 1;
// where there is no room to the right, the copy goes to the left
if (added.x > width - radius(index))
added.x = x(source) - offset;
move(index, added.x, added.y);
return index;
}
/** shows a hidden control of this kind, or else adds a copy; -1 if neither can be */
int add(int type) {
if (type < 0 || type >= BASE_COUNT)
return -1;
for (int i = 0; i < size(); i++) {
if (type(i) == type && !shown(i)) {
setShown(i, true);
return i;
}
}
return duplicate(type);
}
/** moves a control, keeping all of it on the display */
void move(int control, float x, float y) {
if (!Float.isFinite(x) || !Float.isFinite(y))
return;
float radius = radius(control);
controls.get(control).x = Math.max(radius, Math.min(width - radius, x));
controls.get(control).y = Math.max(radius, Math.min(height - radius, y));
}
/** puts a control at a saved place on the grid, if it is a valid one */
void restore(int control, float x, float y) {
if (Float.isFinite(x) && Float.isFinite(y) && x >= 0 && x <= GRID_WIDTH && y >= 0 && y <= GRID_HEIGHT) {
controls.get(control).x = x;
controls.get(control).y = y;
}
}
private void keepAllInside() {
for (int i = 0; i < size(); i++)
move(i, x(i), y(i));
}
/** the layout as a layout file's text, with the look sensitivity */
String exportConfiguration(float sensitivity) {
if (!validSensitivity(sensitivity))
throw new IllegalArgumentException("Invalid sensitivity");
Properties values = new Properties();
values.setProperty("format", "halo-touch-layout");
values.setProperty("version", Integer.toString(VERSION));
values.setProperty("rumble", Boolean.toString(rumbleEnabled));
values.setProperty("gyroscope", Boolean.toString(gyroscopeEnabled));
values.setProperty("opacity", Float.toString(opacity));
values.setProperty("floating-stick", Boolean.toString(floatingStick));
values.setProperty("count", Integer.toString(size()));
values.setProperty("sensitivity", Float.toString(sensitivity));
for (int i = 0; i < size(); i++) {
String key = "control." + i + ".";
values.setProperty(key + "type", Integer.toString(type(i)));
values.setProperty(key + "x", Float.toString(savedX(i)));
values.setProperty(key + "y", Float.toString(savedY(i)));
values.setProperty(key + "visible", Boolean.toString(shown(i)));
values.setProperty(key + "size", Float.toString(sizeScale(i)));
}
StringWriter text = new StringWriter();
try {
values.store(text, "Halo Android touch layout");
} catch (IOException e) {
throw new IllegalStateException(e);
}
return text.toString();
}
/** reads a layout file's text; throws IllegalArgumentException for anything not valid */
static Configuration importConfiguration(String text) {
if (text == null || text.length() > MAX_FILE_LENGTH)
throw new IllegalArgumentException("Invalid layout file");
Properties values = new Properties();
try {
values.load(new StringReader(text));
int version = readVersion(values);
int count = Integer.parseInt(values.getProperty("count"));
float sensitivity = Float.parseFloat(values.getProperty("sensitivity"));
if (count < BASE_COUNT || count > MAX_CONTROLS || !validSensitivity(sensitivity))
throw new IllegalArgumentException("Invalid layout settings");
TouchLayout layout = new TouchLayout();
layout.controls.clear();
if (version >= 2) {
layout.rumbleEnabled = readBoolean(values, "rumble");
layout.gyroscopeEnabled = readBoolean(values, "gyroscope");
}
if (version >= 3) {
float opacity = Float.parseFloat(values.getProperty("opacity"));
if (!validOpacity(opacity))
throw new IllegalArgumentException("Invalid opacity");
layout.opacity = opacity;
layout.floatingStick = readBoolean(values, "floating-stick");
}
for (int i = 0; i < count; i++)
layout.controls.add(readControl(values, i, version));
return new Configuration(layout, sensitivity);
} catch (IOException | NullPointerException e) {
throw new IllegalArgumentException("Incomplete layout file", e);
}
}
private static int readVersion(Properties values) {
if (!"halo-touch-layout".equals(values.getProperty("format")))
throw new IllegalArgumentException("Unsupported layout file");
String version = values.getProperty("version");
for (int known = 1; known <= VERSION; known++) {
if (Integer.toString(known).equals(version))
return known;
}
throw new IllegalArgumentException("Unsupported layout file");
}
private static Control readControl(Properties values, int index, int version) {
String key = "control." + index + ".";
int type = Integer.parseInt(values.getProperty(key + "type"));
float x = Float.parseFloat(values.getProperty(key + "x"));
float y = Float.parseFloat(values.getProperty(key + "y"));
String shown = values.getProperty(key + "visible");
float size = version >= 2 ? Float.parseFloat(values.getProperty(key + "size")) : 1f;
boolean validType = type >= 0 && type < BASE_COUNT && (index >= BASE_COUNT ? type != LEFT : type == index);
boolean validPlace = Float.isFinite(x) && Float.isFinite(y) && x >= 0 && x <= GRID_WIDTH
&& y >= 0 && y <= GRID_HEIGHT;
boolean validSize = Float.isFinite(size) && size >= MIN_SIZE && size <= MAX_SIZE;
if (!validType || !validPlace || !validSize || !("true".equals(shown) || "false".equals(shown)))
throw new IllegalArgumentException("Invalid control in layout file");
Control control = new Control(type, x, y);
control.shown = Boolean.parseBoolean(shown);
control.size = size;
return control;
}
private static boolean readBoolean(Properties values, String key) {
String value = values.getProperty(key);
if (!("true".equals(value) || "false".equals(value)))
throw new IllegalArgumentException("Invalid " + key + " option");
return Boolean.parseBoolean(value);
}
static boolean validSensitivity(float value) {
return Float.isFinite(value) && value >= MIN_SENSITIVITY && value <= MAX_SENSITIVITY;
}
static boolean validOpacity(float value) {
return Float.isFinite(value) && value >= MIN_OPACITY && value <= MAX_OPACITY;
}
}
@@ -53,6 +53,8 @@ final class Updater {
private static final String REPOSITORY = "Cesarus85/ringfall-mjolnir-vr";
private static final String USER_AGENT = "halo-ce-universal-updater";
private static final int TIMEOUT_MILLISECONDS = 20000;
/** the most a download (a release's zip, about 35 MB) or the app in it may be */
private static final long MAXIMUM_UPDATE_SIZE = 256L * 1024 * 1024;
private Updater() {
}
@@ -305,8 +307,10 @@ final class Updater {
int count;
while ((count = in.read(buffer)) > 0) {
out.write(buffer, 0, count);
received += count;
if (received > MAXIMUM_UPDATE_SIZE)
throw new IOException("the download is larger than expected");
out.write(buffer, 0, count);
if (received - reported >= 256 * 1024 || received == total) {
progress.report(received, total);
reported = received;
@@ -330,10 +334,15 @@ final class Updater {
continue;
try (OutputStream out = new FileOutputStream(apk)) {
byte[] buffer = new byte[65536];
long written = 0;
int count;
while ((count = in.read(buffer)) > 0)
while ((count = in.read(buffer)) > 0) {
written += count;
if (written > MAXIMUM_UPDATE_SIZE)
throw new IOException("the app in the download is larger than expected");
out.write(buffer, 0, count);
}
}
return;
}
@@ -135,6 +135,20 @@ final class XisoExtractor {
return true;
}
/** whether a name is one to write in the maps folder: no path, no "." or
* "..", and only printable ASCII that every file system takes as a plain
* name (a disc's maps folder holds .map files and loading.tga) */
static boolean isPlainName(String name) {
if (name.isEmpty() || name.equals(".") || name.equals(".."))
return false;
for (int index = 0; index < name.length(); index++) {
char c = name.charAt(index);
if (c < 0x20 || c > 0x7E || "/\\:*?\"<>|".indexOf(c) >= 0)
return false;
}
return true;
}
/** the partition's volume descriptor: the root directory's sector and size */
private long[] findVolume() throws IOException {
for (long offset : PARTITION_OFFSETS) {
@@ -190,8 +204,9 @@ final class XisoExtractor {
for (int index = 0; index < nameLength; index++)
bytes[index] = table.get(at + ENTRY_HEADER_SIZE + index);
String name = new String(bytes, StandardCharsets.ISO_8859_1);
/* (as extract-xiso refuses them: no name may leave the folder) */
if (!name.equals(".") && !name.equals("..") && name.indexOf('/') < 0 && name.indexOf('\\') < 0)
/* (as extract-xiso refuses them: no name may leave the folder, and
none but a plain file name is written) */
if (isPlainName(name))
out.add(new Entry(name, table.getInt(at + 4) & 0xFFFFFFFFL, table.getInt(at + 8) & 0xFFFFFFFFL));
}
if (right != 0)
+27
View File
@@ -46,6 +46,8 @@ unsigned int host_memory_watch_generation(unsigned int address, unsigned int siz
unsigned int host_memory_watch_serial(void);
void host_memory_watch_prepare_write(unsigned int address, unsigned int size);
void host_memory_watch_forget(unsigned int address, unsigned int size);
/* memory_watch_begin_frame (port/linux/src/platform.h) */
void host_memory_watch_begin_frame(void);
/* ---------- SDL (guest/runtime/guest_sdl.c)
@@ -80,11 +82,16 @@ int host_sdl_gamepad_axis(unsigned int gamepad, int axis);
int host_sdl_gamepad_button(unsigned int gamepad, int button);
int host_sdl_gamepad_type(unsigned int gamepad);
int host_sdl_rumble_gamepad(unsigned int gamepad, unsigned int low, unsigned int high, unsigned int milliseconds);
void host_sdl_close_gamepad(unsigned int gamepad);
/* callback: void (*)(void *userdata, unsigned int stream, int additional, int total),
called on the audio thread */
unsigned int host_sdl_open_audio_stream(unsigned int device, const void *spec, unsigned int callback, unsigned int userdata);
int host_sdl_put_audio_stream_data(unsigned int stream, const void *data, int length);
int host_sdl_resume_audio_stream_device(unsigned int stream);
/* (voice chat's microphone, a stream without a callback) */
int host_sdl_get_audio_stream_data(unsigned int stream, void *data, int length);
int host_sdl_get_audio_stream_available(unsigned int stream);
void host_sdl_destroy_audio_stream(unsigned int stream);
/* ---------- OpenGL ES */
@@ -147,4 +154,24 @@ void host_xr_set_refresh_rate(float wanted);
/* the storage directories the port uses, copied into buffer */
void host_android_path(int which, char *buffer, unsigned int size);
/* the edges where Android keeps its gestures, as left, top, right, bottom
in pixels of the current orientation, into insets[4]; all 0 when unknown */
void host_gesture_insets(int *insets);
/* ---------- the on-screen touch controls (host_touch.c) */
/* the overlay's controller: the SDL axes (left x, y, right x, y, left
trigger, right trigger) and the SDL button bits, into state[7] */
void host_touch_read(int *state);
/* the overlay's view swipe, then its gyroscope turn, since the last read,
into delta[4] */
void host_touch_look_read(float *delta);
/* port 0's motors, for the phone's vibration */
void host_touch_rumble(unsigned int low, unsigned int high);
/* tells the overlay when to show: touch_input.c's _touch_scene_* */
void host_touch_scene(int scene);
/* the game control on each of the 16 controller buttons (touch_game.c),
for the overlay's labels */
void host_touch_bindings(const int *controls);
#endif
@@ -39,3 +39,8 @@ void memory_watch_forget(void *address, unsigned long size)
{
host_memory_watch_forget((unsigned int)address, size);
}
void memory_watch_begin_frame(void)
{
host_memory_watch_begin_frame();
}
+20
View File
@@ -216,6 +216,11 @@ bool SDL_RumbleGamepad(SDL_Gamepad *gamepad, Uint16 low, Uint16 high, Uint32 mil
return host_sdl_rumble_gamepad((unsigned int)gamepad, low, high, milliseconds) != 0;
}
void SDL_CloseGamepad(SDL_Gamepad *gamepad)
{
host_sdl_close_gamepad((unsigned int)gamepad);
}
/* ---------- audio */
SDL_AudioStream *SDL_OpenAudioDeviceStream(SDL_AudioDeviceID device, const SDL_AudioSpec *spec,
@@ -234,3 +239,18 @@ bool SDL_ResumeAudioStreamDevice(SDL_AudioStream *stream)
{
return host_sdl_resume_audio_stream_device((unsigned int)stream) != 0;
}
int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *data, int length)
{
return host_sdl_get_audio_stream_data((unsigned int)stream, data, length);
}
int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream)
{
return host_sdl_get_audio_stream_available((unsigned int)stream);
}
void SDL_DestroyAudioStream(SDL_AudioStream *stream)
{
host_sdl_destroy_audio_stream((unsigned int)stream);
}
+2
View File
@@ -25,6 +25,7 @@ extern void (*__guest_init_end[])(void);
void __guest_start(const struct halo_guest_boot *boot);
void __guest_thread_start(unsigned int thread);
unsigned int __guest_thread_attach(void);
void __guest_thread_detach(void);
void __guest_thread_initialize_main(void);
extern char **__environ;
@@ -41,6 +42,7 @@ const struct halo_guest_header __guest_header =
(uint32_t)__guest_start,
(uint32_t)__guest_thread_start,
(uint32_t)__guest_thread_attach,
(uint32_t)__guest_thread_detach,
(uint32_t)__guest_init_start,
(uint32_t)__guest_init_end,
};
+12
View File
@@ -125,6 +125,18 @@ unsigned int __guest_thread_attach(void)
return (unsigned int)thread;
}
/* a host thread that ran guest code (__guest_thread_attach) is ending:
its struct pthread goes */
void __guest_thread_detach(void)
{
struct guest_thread *thread = (struct guest_thread *)host_get_tp();
if (!thread || thread == &main_thread)
return;
host_set_tp(0);
thread_free(thread);
}
int pthread_create(pthread_t *restrict result, const pthread_attr_t *restrict attributes,
void *(*start)(void *), void *restrict argument)
{
+4
View File
@@ -37,6 +37,10 @@ mappings) from pools of address space it reserves below 4 GB on demand. */
/* reserves the fixed ranges; returns 0 on success */
int host_memory_initialize(uint32_t image_base, uint32_t image_size);
/* Makes the write tracking compare page contents instead of catching page
faults (host_watch_hash.h), for when the app cannot receive its own
SIGSEGV (ARM translation). Call before the guest starts. */
void host_memory_watch_use_hashes(void);
/* page-granular allocations below 4 GB; NULL on failure */
void *host_low_map(size_t size, int protection);
void host_low_unmap(void *address, size_t size);
+111
View File
@@ -25,12 +25,14 @@ that runs here.
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <android/log.h>
#include <jni.h>
#include <errno.h>
#include <ftw.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/system_properties.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
@@ -137,6 +139,52 @@ struct environment
int count;
};
/* asks the activity for Android's system gesture insets via JNI
(HaloActivity.getSystemGestureInsetsPixels); the guest calls it at every
finger down because the insets change when the phone rotates; the method is
found on the activity's own class because the game's native thread has the
system's class loader, which does not know the app's classes; a change is
logged so the log shows the value the game works with; all 0 on any failure
(the touch controls then use the whole screen) */
void host_gesture_insets(int *insets)
{
static int logged[4] = { -1, -1, -1, -1 };
JNIEnv *env = (JNIEnv *)SDL_GetAndroidJNIEnv();
jobject activity = (jobject)SDL_GetAndroidActivity();
jint values[4] = { 0, 0, 0, 0 };
int index;
if (env && activity)
{
jclass activity_class = (*env)->GetObjectClass(env, activity);
jmethodID method = activity_class ? (*env)->GetMethodID(env, activity_class, "getSystemGestureInsetsPixels", "()[I") : NULL;
jintArray array = method ? (jintArray)(*env)->CallObjectMethod(env, activity, method) : NULL;
if (array && !(*env)->ExceptionCheck(env) && (*env)->GetArrayLength(env, array) == 4)
(*env)->GetIntArrayRegion(env, array, 0, 4, values);
/* a Java exception left pending would break the next JNI call of the
thread, and a failed read must leave zeros */
if ((*env)->ExceptionCheck(env))
{
(*env)->ExceptionClear(env);
for (index = 0; index < 4; index++)
values[index] = 0;
}
if (array)
(*env)->DeleteLocalRef(env, array);
if (activity_class)
(*env)->DeleteLocalRef(env, activity_class);
(*env)->DeleteLocalRef(env, activity);
}
for (index = 0; index < 4; index++)
insets[index] = (int)values[index];
if (memcmp(logged, insets, sizeof(logged)))
{
memcpy(logged, insets, sizeof(logged));
host_logf(HOST_LOG_INFO, "system gesture insets %d,%d,%d,%d", insets[0], insets[1], insets[2], insets[3]);
}
}
static void environment_set(struct environment *environment, const char *name, const char *value)
{
size_t length = strlen(name);
@@ -205,6 +253,37 @@ static double config_number(const char *path, const char *key, double fallback)
return value;
}
/* a boolean setting of config.toml (a dotted name), or otherwise when the
file, the setting or a boolean is missing */
static int config_boolean_or(const char *path, const char *name, int otherwise)
{
toml_result_t result = toml_parse_file_ex(path);
int value = otherwise;
if (!result.ok)
return otherwise;
{
toml_datum_t datum = toml_seek(result.toptab, name);
if (datum.type == TOML_BOOLEAN)
value = datum.u.boolean ? 1 : 0;
}
toml_free(result);
return value;
}
/* Whether the app runs through an ARM translator. The x86 emulator runs the
app's ARM code through one, which ro.dalvik.vm.native.bridge names; it
cannot deliver the page faults of the write tracking to the app
(host_memory.c). */
static int native_bridge_active(void)
{
char value[PROP_VALUE_MAX] = "";
__system_property_get("ro.dalvik.vm.native.bridge", value);
return value[0] && strcmp(value, "0") != 0;
}
/* POSIX TZ for the current local offset (the guest's musl has no zone
database) */
static void time_zone(char *buffer, size_t size)
@@ -257,6 +336,11 @@ static uint32_t make_boot(const struct environment *environment)
#define MAIN_STACK_SIZE (16 * 1024 * 1024)
/* the thread that runs the game: passes the display and its density, the
time zone and the data and save folders to the guest through its
environment, chooses the write tracking (page protection, or page hashes when
translated or when debug.memory_watch is false), and runs the guest's main;
never returns normally */
static void *game_main(void *unused)
{
struct environment environment = { { 0 }, 0 };
@@ -370,6 +454,7 @@ static void *game_main(void *unused)
(landscape) unless display.screen_width says otherwise (d3d8_gl.c) */
const SDL_DisplayMode *mode;
char width[16];
char text_density[16];
SDL_InitSubSystem(SDL_INIT_VIDEO);
mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
@@ -382,6 +467,18 @@ static void *game_main(void *unused)
environment_set(&environment, "HALO_DISPLAY_WIDTH", width);
host_logf(HOST_LOG_INFO, "display %dx%d: rendering %sx480", mode->w, mode->h, width);
}
{
/* dp for the touch controls (port/linux/src/touch_input.c): SDL
gives Android's densityDpi / 160 */
float density = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (density > 0.0f)
{
snprintf(text_density, sizeof(text_density), "%g", density);
environment_set(&environment, "HALO_DISPLAY_DENSITY", text_density);
host_logf(HOST_LOG_INFO, "display density %s", text_density);
}
}
}
}
@@ -391,6 +488,20 @@ static void *game_main(void *unused)
if (config_sample_seconds(path, seconds, sizeof(seconds)))
host_debug_start_sampler(seconds);
}
if (native_bridge_active())
{
host_memory_watch_use_hashes();
host_logf(HOST_LOG_INFO, "write tracking: page hashes (ARM translation)");
}
else if (!config_boolean_or(path, "debug.memory_watch", 1))
{
host_memory_watch_use_hashes();
host_logf(HOST_LOG_INFO, "write tracking: page hashes (debug.memory_watch = false)");
}
else
{
host_logf(HOST_LOG_INFO, "write tracking: page protection");
}
boot = make_boot(&environment);
host_logf(HOST_LOG_INFO, "data %s, saves %s", data_root, save_root);
host_run_guest_main(boot);
+55 -2
View File
@@ -17,10 +17,13 @@ This file also implements guest memory write tracking (the interface of
port/linux/src/memory_watch.c): the renderer write-protects the pages behind
the textures it caches, and the SIGSEGV handler here records the first
write to each. Other faults are reported (with guest-relative addresses) and
passed on to the previous handler.
passed on to the previous handler. Under ARM translation (the x86 emulator)
page faults cannot be caught, and the tracking compares page contents
instead (host_watch_hash.c).
*/
#include "host.h"
#include "host_watch_hash.h"
#include <errno.h>
#include <pthread.h>
@@ -478,6 +481,21 @@ static uint8_t page_protected[WATCH_PAGE_COUNT];
static uint32_t page_generation[WATCH_PAGE_COUNT];
static volatile uint32_t current_generation = 1;
static int watch_active;
static int watch_hashing;
static uint8_t page_hash_watched[WATCH_PAGE_COUNT];
static uint64_t page_hash[WATCH_PAGE_COUNT];
static uint32_t page_hashed_frame[WATCH_PAGE_COUNT];
static struct watch_hash page_hashes =
{
.base = (const uint8_t *)(uintptr_t)HALO_GUEST_WINDOW_BASE,
.page_count = WATCH_PAGE_COUNT,
.watched = page_hash_watched,
.hash = page_hash,
.hashed_frame = page_hashed_frame,
.generation = page_generation,
.current_generation = &current_generation,
.frame = 1,
};
static int in_window(uint64_t address)
{
@@ -601,6 +619,14 @@ void host_memory_watch_initialize(void)
watch_active = 1;
}
void host_memory_watch_use_hashes(void)
{
watch_hashing = 1;
}
/* memory_watch_protect: write-protects the pages of a range, or keeps their
hash under host_memory_watch_use_hashes; a size outside the guest window
is ignored */
void host_memory_watch_protect(uint32_t address, uint32_t size)
{
uint64_t first, last, page;
@@ -611,6 +637,11 @@ void host_memory_watch_protect(uint32_t address, uint32_t size)
last = watch_page((uint64_t)address + size - 1);
if (last >= WATCH_PAGE_COUNT)
last = WATCH_PAGE_COUNT - 1;
if (watch_hashing)
{
watch_hash_protect(&page_hashes, (uint32_t)first, (uint32_t)last);
return;
}
for (page = first; page <= last; page++)
{
if (!page_protected[page])
@@ -626,6 +657,8 @@ uint32_t host_memory_watch_serial(void)
return current_generation;
}
/* memory_watch_generation: the newest generation of the pages of a range.
Under hashing this hashes the pages again, once a frame. */
uint32_t host_memory_watch_generation(uint32_t address, uint32_t size)
{
uint64_t first, last, page;
@@ -637,6 +670,8 @@ uint32_t host_memory_watch_generation(uint32_t address, uint32_t size)
last = watch_page((uint64_t)address + size - 1);
if (last >= WATCH_PAGE_COUNT)
last = WATCH_PAGE_COUNT - 1;
if (watch_hashing)
return watch_hash_generation(&page_hashes, (uint32_t)first, (uint32_t)last);
for (page = first; page <= last; page++)
{
if (page_generation[page] > newest)
@@ -645,11 +680,14 @@ uint32_t host_memory_watch_generation(uint32_t address, uint32_t size)
return newest;
}
/* memory_watch_prepare_write: unprotects the pages the host itself is about
to write into. Nothing under hashing: the write shows in the next hash and
there is no protection to fault on. */
void host_memory_watch_prepare_write(uint32_t address, uint32_t size)
{
uint64_t start = address, first, last, page;
if (!watch_active || !size)
if (!watch_active || watch_hashing || !size)
return;
if (start + size <= HALO_GUEST_WINDOW_BASE || start >= (uint64_t)HALO_GUEST_WINDOW_BASE + HALO_GUEST_WINDOW_SIZE)
return;
@@ -666,6 +704,8 @@ void host_memory_watch_prepare_write(uint32_t address, uint32_t size)
}
}
/* memory_watch_forget: the pages of a range were remapped or reprotected, so
treat them as written and unwatched */
void host_memory_watch_forget(uint32_t address, uint32_t size)
{
uint64_t first, last, page;
@@ -676,9 +716,22 @@ void host_memory_watch_forget(uint32_t address, uint32_t size)
last = watch_page((uint64_t)address + size - 1);
if (last >= WATCH_PAGE_COUNT)
last = WATCH_PAGE_COUNT - 1;
if (watch_hashing)
{
watch_hash_forget(&page_hashes, (uint32_t)first, (uint32_t)last);
return;
}
for (page = first; page <= last; page++)
{
page_protected[page] = 0;
page_generation[page] = __sync_add_and_fetch(&current_generation, 1);
}
}
/* memory_watch_begin_frame: pages may be hashed again; nothing under page
protection */
void host_memory_watch_begin_frame(void)
{
if (watch_hashing)
watch_hash_begin_frame(&page_hashes);
}
+108 -3
View File
@@ -32,6 +32,8 @@ struct handle
{
int type;
void *object;
/* an audio stream with a callback: its binding (below) */
void *binding;
};
static struct handle handles[HANDLE_COUNT];
@@ -59,6 +61,7 @@ static uint32_t handle_new(int type, void *object)
{
handles[index].type = type;
handles[index].object = object;
handles[index].binding = NULL;
pthread_mutex_unlock(&handle_lock);
return index;
}
@@ -81,6 +84,30 @@ static void *handle_get(uint32_t handle, int type)
return object;
}
/* the object of a handle, which the handle no longer refers to; NULL when
there is none. *binding gets what handle_new's caller stored (audio) */
static void *handle_release(uint32_t handle, int type, void **binding)
{
void *object = NULL;
if (binding)
*binding = NULL;
if (handle == 0 || handle >= HANDLE_COUNT)
return NULL;
pthread_mutex_lock(&handle_lock);
if (handles[handle].type == type)
{
object = handles[handle].object;
if (binding)
*binding = handles[handle].binding;
handles[handle].type = _handle_free;
handles[handle].object = NULL;
handles[handle].binding = NULL;
}
pthread_mutex_unlock(&handle_lock);
return object;
}
/* ---------- general */
int host_sdl_init(uint32_t flags)
@@ -221,6 +248,15 @@ uint32_t host_sdl_gamepad_from_id(uint32_t id)
return handle_new(_handle_gamepad, SDL_GetGamepadFromID((SDL_JoystickID)id));
}
/* (the event thread, when the controller goes: sdl_platform.c) */
void host_sdl_close_gamepad(uint32_t gamepad)
{
SDL_Gamepad *object = handle_release(gamepad, _handle_gamepad, NULL);
if (object)
SDL_CloseGamepad(object);
}
int host_sdl_gamepad_axis(uint32_t gamepad, int axis)
{
SDL_Gamepad *object = handle_get(gamepad, _handle_gamepad);
@@ -267,6 +303,8 @@ struct audio_binding
pthread_cond_t requested;
pthread_cond_t done;
int pending;
/* the stream is destroyed: the thread frees the binding and ends */
int quit;
int additional;
int total;
unsigned char *buffer;
@@ -286,8 +324,10 @@ static void *audio_thread(void *context)
{
int additional, total;
while (!binding->pending)
while (!binding->pending && !binding->quit)
pthread_cond_wait(&binding->requested, &binding->lock);
if (binding->quit)
break;
additional = binding->additional;
total = binding->total;
pthread_mutex_unlock(&binding->lock);
@@ -298,6 +338,12 @@ static void *audio_thread(void *context)
binding->pending = 0;
pthread_cond_signal(&binding->done);
}
pthread_mutex_unlock(&binding->lock);
pthread_cond_destroy(&binding->done);
pthread_cond_destroy(&binding->requested);
pthread_mutex_destroy(&binding->lock);
SDL_free(binding->buffer);
SDL_free(binding);
return NULL;
}
@@ -343,9 +389,19 @@ static int audio_keep(struct audio_binding *binding, const void *data, int lengt
uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t callback, uint32_t userdata)
{
struct audio_binding *binding = SDL_calloc(1, sizeof(*binding));
struct audio_binding *binding;
SDL_AudioStream *stream;
/* (one without a callback, the microphone's, which the guest reads:
nothing to bind) */
if (!callback)
{
stream = SDL_OpenAudioDeviceStream((SDL_AudioDeviceID)device, spec, NULL, NULL);
return stream ? handle_new(_handle_audio, stream) : 0;
}
binding = SDL_calloc(1, sizeof(*binding));
if (!binding)
return 0;
binding->callback = callback;
binding->userdata = userdata;
pthread_mutex_init(&binding->lock, NULL);
@@ -360,7 +416,19 @@ uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t
}
/* the device starts paused, so no callback can run before this */
binding->handle = handle_new(_handle_audio, stream);
if (callback && host_native_thread_create(audio_thread, binding, 256 * 1024) != 0)
if (!binding->handle)
{
SDL_DestroyAudioStream(stream);
pthread_cond_destroy(&binding->done);
pthread_cond_destroy(&binding->requested);
pthread_mutex_destroy(&binding->lock);
SDL_free(binding);
return 0;
}
pthread_mutex_lock(&handle_lock);
handles[binding->handle].binding = binding;
pthread_mutex_unlock(&handle_lock);
if (host_native_thread_create(audio_thread, binding, 256 * 1024) != 0)
host_fatal("cannot start the audio thread");
return binding->handle;
}
@@ -383,6 +451,43 @@ int host_sdl_resume_audio_stream_device(uint32_t stream)
return object ? SDL_ResumeAudioStreamDevice(object) : 0;
}
/* (voice chat's microphone: port/linux/src/voice_audio.c) */
int host_sdl_get_audio_stream_data(uint32_t stream, void *data, int length)
{
SDL_AudioStream *object = handle_get(stream, _handle_audio);
return object ? SDL_GetAudioStreamData(object, data, length) : -1;
}
int host_sdl_get_audio_stream_available(uint32_t stream)
{
SDL_AudioStream *object = handle_get(stream, _handle_audio);
return object ? SDL_GetAudioStreamAvailable(object) : -1;
}
/* (a stream with a callback has a thread the guest's callbacks run on:
once no callback can run any more, it is told to end, and frees the
binding and its stack as it goes) */
void host_sdl_destroy_audio_stream(uint32_t stream)
{
void *context;
SDL_AudioStream *object = handle_release(stream, _handle_audio, &context);
struct audio_binding *binding = context;
if (!object)
return;
/* (returns once a callback that is running has finished) */
SDL_DestroyAudioStream(object);
if (binding)
{
pthread_mutex_lock(&binding->lock);
binding->quit = 1;
pthread_cond_signal(&binding->requested);
pthread_mutex_unlock(&binding->lock);
}
}
/* ---------- the clipboard (internet play's invite links) */
int host_sdl_set_clipboard_text(const char *text)
+4
View File
@@ -131,6 +131,10 @@ static void *thread_main(void *context)
free(context);
host_debug_thread_started();
start.function(start.argument);
/* (a thread the guest made has let its struct pthread go by now; one
of the host's that ran guest code, the audio thread, has it still) */
if (guest_tp)
((guest_function)(uintptr_t)host_image.header->thread_detach)(0, 0, 0, 0);
host_debug_thread_exited();
guest_tp = 0;
+229
View File
@@ -0,0 +1,229 @@
/*
HOST_TOUCH.C
The on-screen touch controls between the overlay and the game. The overlay
(app/.../TouchControls.java) is an Android view over SDL's surface: it
hands its stick, buttons, view swipes and gyroscope turns over JNI on the UI
thread, and the game reads them through host imports when it reads port 0's
controller (port/linux/src/xinput_sdl.c). The game hands back what the
overlay needs to know: whether it is in a game with no menu or cinematic up
and the input.touch_controls setting (host_touch_scene), the profile's
button mapping, which names the buttons (host_touch_bindings), and how hard
port 0 rumbles.
*/
#include <jni.h>
#include <pthread.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
/* a press shorter than this still reaches the game: it reads the
controller once a frame and acts at 30 ticks a second, and a quick tap can
go down and up between two reads */
#define TOUCH_TAP_NS 60000000LL
/* view motion and rumble older than this are dropped: left over from the
menus, a cinematic or the app in the background */
#define TOUCH_STALE_NS 150000000LL
/* SDL's 15 gamepad buttons, then the left and right triggers */
#define TOUCH_INPUTS 17
static pthread_mutex_t touch_lock = PTHREAD_MUTEX_INITIALIZER;
/* the SDL axes (left x, y, right x, y, left trigger, right trigger), then
the SDL button bits */
static int32_t touch_state[7];
static int64_t touch_pressed_ns[TOUCH_INPUTS];
static float look_delta[2];
static int64_t look_ns;
/* the gyroscope's turn, apart: the profile's invert is the swipe's only */
static float gyro_delta[2];
static int64_t gyro_ns;
static int rumble_amplitude;
static int64_t rumble_ns;
static volatile int touch_scene;
/* the game control on each controller button (-1: none), and a count of
its changes, which the overlay polls */
#define TOUCH_BUTTONS 16
static int32_t touch_bindings[TOUCH_BUTTONS];
static int touch_bindings_serial;
static int64_t now_ns(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
}
/* the inputs of a state as bits: buttons, then the triggers */
static uint32_t touch_inputs(const int32_t *state)
{
return ((uint32_t)state[6] & 0x7fff) | (state[4] ? 1u << 15 : 0) | (state[5] ? 1u << 16 : 0);
}
JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeState(
JNIEnv *env, jclass cls, jint lx, jint ly, jint rx, jint ry,
jint lt, jint rt, jint buttons)
{
int32_t next[] = { lx, ly, rx, ry, lt, rt, buttons };
uint32_t pressed;
int64_t now = now_ns();
int input;
(void)env;
(void)cls;
pthread_mutex_lock(&touch_lock);
pressed = touch_inputs(next) & ~touch_inputs(touch_state);
for (input = 0; input < TOUCH_INPUTS; input++)
{
if (pressed & (1u << input))
touch_pressed_ns[input] = now;
}
memcpy(touch_state, next, sizeof(next));
pthread_mutex_unlock(&touch_lock);
}
/* the guest's: the overlay's controller state, with each press held for
at least TOUCH_TAP_NS */
void host_touch_read(int32_t *state)
{
int64_t now = now_ns();
int input;
pthread_mutex_lock(&touch_lock);
memcpy(state, touch_state, sizeof(touch_state));
for (input = 0; input < TOUCH_INPUTS; input++)
{
if (touch_pressed_ns[input] && now - touch_pressed_ns[input] < TOUCH_TAP_NS)
{
if (input < 15)
state[6] |= 1 << input;
else if (!state[4 + input - 15])
state[4 + input - 15] = 32767;
}
}
pthread_mutex_unlock(&touch_lock);
}
JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLook(
JNIEnv *env, jclass cls, jfloat dx, jfloat dy)
{
(void)env;
(void)cls;
pthread_mutex_lock(&touch_lock);
look_delta[0] += dx;
look_delta[1] += dy;
look_ns = now_ns();
pthread_mutex_unlock(&touch_lock);
}
JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeGyro(
JNIEnv *env, jclass cls, jfloat dx, jfloat dy)
{
(void)env;
(void)cls;
pthread_mutex_lock(&touch_lock);
gyro_delta[0] += dx;
gyro_delta[1] += dy;
gyro_ns = now_ns();
pthread_mutex_unlock(&touch_lock);
}
JNIEXPORT void JNICALL Java_com_halo_decomp_TouchControls_nativeLookReset(JNIEnv *env, jclass cls)
{
(void)env;
(void)cls;
pthread_mutex_lock(&touch_lock);
look_delta[0] = look_delta[1] = 0.0f;
gyro_delta[0] = gyro_delta[1] = 0.0f;
pthread_mutex_unlock(&touch_lock);
}
/* the guest's: the view swipe, then the gyroscope's turn, since the last
read, into delta[4], in the overlay's logical pixels with its sensitivity
applied */
void host_touch_look_read(float *delta)
{
int64_t now = now_ns();
pthread_mutex_lock(&touch_lock);
memcpy(delta, look_delta, sizeof(look_delta));
memcpy(delta + 2, gyro_delta, sizeof(gyro_delta));
if (now - look_ns > TOUCH_STALE_NS)
delta[0] = delta[1] = 0.0f;
if (now - gyro_ns > TOUCH_STALE_NS)
delta[2] = delta[3] = 0.0f;
look_delta[0] = look_delta[1] = 0.0f;
gyro_delta[0] = gyro_delta[1] = 0.0f;
pthread_mutex_unlock(&touch_lock);
}
/* the guest's: port 0's motors (XInputSetState), each 0..65535; the game
sends them every frame, and zero while paused or with the profile's
vibration off */
void host_touch_rumble(unsigned int low, unsigned int high)
{
unsigned int strength = low > high ? low : high;
pthread_mutex_lock(&touch_lock);
rumble_amplitude = strength ? 64 + (int)(strength * 191u / 65535u) : 0;
rumble_ns = now_ns();
pthread_mutex_unlock(&touch_lock);
}
/* the phone's vibration strength, 0 or 64..255 */
JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeRumble(JNIEnv *env, jclass cls)
{
int amplitude;
(void)env;
(void)cls;
pthread_mutex_lock(&touch_lock);
amplitude = now_ns() - rumble_ns > TOUCH_STALE_NS ? 0 : rumble_amplitude;
pthread_mutex_unlock(&touch_lock);
return amplitude;
}
/* the guest's, at every read of port 0: port/linux/src/touch_input.c's
_touch_scene_* bits */
void host_touch_scene(int scene)
{
touch_scene = scene;
}
/* the guest's, when the profile's mapping changes: the game control on
each of the 16 controller buttons (port/linux/game/touch_game.c) */
void host_touch_bindings(const int32_t *controls)
{
pthread_mutex_lock(&touch_lock);
memcpy(touch_bindings, controls, sizeof(touch_bindings));
touch_bindings_serial++;
pthread_mutex_unlock(&touch_lock);
}
/* the mapping into controls[16]; returns its count of changes (0 before the
game has sent it) */
JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeBindings(JNIEnv *env, jclass cls,
jintArray controls)
{
int32_t copy[TOUCH_BUTTONS];
int serial;
(void)cls;
pthread_mutex_lock(&touch_lock);
memcpy(copy, touch_bindings, sizeof(copy));
serial = touch_bindings_serial;
pthread_mutex_unlock(&touch_lock);
if (controls && (*env)->GetArrayLength(env, controls) >= TOUCH_BUTTONS)
(*env)->SetIntArrayRegion(env, controls, 0, TOUCH_BUTTONS, (const jint *)copy);
return serial;
}
/* 0 until the game has read its controller once */
JNIEXPORT jint JNICALL Java_com_halo_decomp_TouchControls_nativeScene(JNIEnv *env, jclass cls)
{
(void)env;
(void)cls;
return touch_scene;
}
+114
View File
@@ -0,0 +1,114 @@
/*
HOST_WATCH_HASH.C
Write tracking by page contents (host_watch_hash.h).
*/
#include "host_watch_hash.h"
/*
FNV-style mixing in four independent lanes, so that the multiplies of
one lane do not wait for another's. A change of one word changes its
lane's state for good (xor and an odd multiply are both invertible), so
a single-word write always shows. Changes of several words could cancel
each other, which is unlikely for game data. (host_watch_hash.h)
*/
uint64_t watch_hash_page(const uint8_t *page)
{
const uint64_t *words = (const uint64_t *)page;
uint64_t lane0 = 0xcbf29ce484222325ULL, lane1 = 0x9e3779b97f4a7c15ULL;
uint64_t lane2 = 0xc2b2ae3d27d4eb4fULL, lane3 = 0x165667b19e3779f9ULL;
uint32_t index;
for (index = 0; index < WATCH_HASH_PAGE_SIZE / 8; index += 4)
{
lane0 = (lane0 ^ words[index + 0]) * 0x100000001b3ULL;
lane1 = (lane1 ^ words[index + 1]) * 0x100000001b3ULL;
lane2 = (lane2 ^ words[index + 2]) * 0x100000001b3ULL;
lane3 = (lane3 ^ words[index + 3]) * 0x100000001b3ULL;
}
return lane0 ^ ((lane1 << 16) | (lane1 >> 48)) ^ ((lane2 << 32) | (lane2 >> 32)) ^
((lane3 << 48) | (lane3 >> 16));
}
/* clamps last to the watched memory; 0 when nothing is left of the range */
static int clamp(const struct watch_hash *watch, uint32_t first, uint32_t *last)
{
if (first >= watch->page_count || first > *last)
return 0;
if (*last >= watch->page_count)
*last = watch->page_count - 1;
return 1;
}
static const uint8_t *page_memory(const struct watch_hash *watch, uint32_t page)
{
return watch->base + (uint64_t)page * WATCH_HASH_PAGE_SIZE;
}
void watch_hash_protect(struct watch_hash *watch, uint32_t first, uint32_t last)
{
uint32_t page;
if (!clamp(watch, first, &last))
return;
for (page = first; page <= last; page++)
{
if (watch->watched[page])
continue;
watch->watched[page] = 1;
watch->hash[page] = watch_hash_page(page_memory(watch, page));
watch->hashed_frame[page] = watch->frame;
}
}
uint32_t watch_hash_generation(struct watch_hash *watch, uint32_t first, uint32_t last)
{
uint32_t page, newest = 0;
if (!clamp(watch, first, &last))
return 0;
for (page = first; page <= last; page++)
{
if (watch->watched[page] && watch->hashed_frame[page] != watch->frame)
{
uint64_t hash = watch_hash_page(page_memory(watch, page));
watch->hashed_frame[page] = watch->frame;
if (hash != watch->hash[page])
{
watch->hash[page] = hash;
watch->generation[page] = __sync_add_and_fetch(watch->current_generation, 1);
}
}
if (watch->generation[page] > newest)
newest = watch->generation[page];
}
return newest;
}
void watch_hash_forget(struct watch_hash *watch, uint32_t first, uint32_t last)
{
uint32_t page;
if (!clamp(watch, first, &last))
return;
for (page = first; page <= last; page++)
{
watch->watched[page] = 0;
watch->generation[page] = __sync_add_and_fetch(watch->current_generation, 1);
}
}
/*
The serial changes each frame, so a cache that skips generation queries
while the serial stays the same (xbox_textures.c's recent_textures) asks
again once a frame. (host_watch_hash.h)
*/
void watch_hash_begin_frame(struct watch_hash *watch)
{
watch->frame++;
if (!watch->frame)
watch->frame = 1;
__sync_add_and_fetch(watch->current_generation, 1);
}
+57
View File
@@ -0,0 +1,57 @@
/*
HOST_WATCH_HASH.H
Write tracking by page contents, for processes that cannot catch their own
page protection faults: the ARM translation of the x86 Android emulator
cannot deliver SIGSEGV to the app, so the protection-based tracking of
host_memory.c would stop the game at its first write to a cached page.
A watched page's hash is kept. Asking for its generation hashes it again,
at most once a frame (watch_hash_begin_frame), and a different hash counts
as a write. A write is therefore seen in the next frame, not at once.
*/
#ifndef HOST_WATCH_HASH_H
#define HOST_WATCH_HASH_H
#include <stdint.h>
#define WATCH_HASH_PAGE_SIZE 0x1000u
struct watch_hash
{
/* the memory of page 0 */
const uint8_t *base;
uint32_t page_count;
/* page_count entries each */
uint8_t *watched;
uint64_t *hash;
uint32_t *hashed_frame;
/* shared with the protection-based tracking (host_memory.c) */
uint32_t *generation;
volatile uint32_t *current_generation;
uint32_t frame;
};
/* a hash of one page (WATCH_HASH_PAGE_SIZE bytes, 8-byte aligned) that
changes whenever a word of it changes */
uint64_t watch_hash_page(const uint8_t *page);
/* Starts watching pages first to last (inclusive), remembering their hashes.
Pages already watched keep their hash; pages beyond page_count are
ignored. */
void watch_hash_protect(struct watch_hash *watch, uint32_t first, uint32_t last);
/* The newest generation among pages first to last (inclusive), after hashing
again the watched ones not yet hashed this frame; a page whose hash
changed gets a new generation. 0 if the range is outside the watched
memory. */
uint32_t watch_hash_generation(struct watch_hash *watch, uint32_t first, uint32_t last);
/* The pages were remapped: treat them as written and unwatched. */
void watch_hash_forget(struct watch_hash *watch, uint32_t first, uint32_t last);
/* A new frame: pages may be hashed again, and the serial changes. */
void watch_hash_begin_frame(struct watch_hash *watch);
#endif
+11
View File
@@ -13,6 +13,7 @@ host_memory_watch_generation
host_memory_watch_serial
host_memory_watch_prepare_write
host_memory_watch_forget
host_memory_watch_begin_frame
host_sdl_init
host_sdl_set_hint
host_sdl_get_error
@@ -38,11 +39,15 @@ host_sdl_gamepad_axis
host_sdl_gamepad_button
host_sdl_gamepad_type
host_sdl_rumble_gamepad
host_sdl_close_gamepad
host_sdl_scancode_name
host_sdl_scancode_from_name
host_sdl_open_audio_stream
host_sdl_put_audio_stream_data
host_sdl_resume_audio_stream_device
host_sdl_get_audio_stream_data
host_sdl_get_audio_stream_available
host_sdl_destroy_audio_stream
host_gl_get_string
host_gl_has_extension
host_gl_read_buffer
@@ -64,5 +69,11 @@ host_xr_copy_layer_array
host_xr_refresh_rates
host_xr_set_refresh_rate
host_android_path
host_gesture_insets
host_touch_read
host_touch_look_read
host_touch_rumble
host_touch_scene
host_touch_bindings
host_errno
+2 -1
View File
@@ -35,7 +35,7 @@ keeps its heaps low in the address space and fills it upwards */
#define HALO_GUEST_WINDOW_SIZE 0x08000000u
#define HALO_GUEST_MAGIC 0x4f4c4148u /* 'HALO' */
#define HALO_GUEST_ABI_VERSION 1
#define HALO_GUEST_ABI_VERSION 2
/* at HALO_GUEST_IMAGE_BASE */
struct halo_guest_header
@@ -49,6 +49,7 @@ struct halo_guest_header
uint32_t start; /* void __guest_start(struct halo_guest_boot *) */
uint32_t thread_start; /* void __guest_thread_start(uint32_t thread) */
uint32_t thread_attach; /* uint32_t __guest_thread_attach(void) */
uint32_t thread_detach; /* void __guest_thread_detach(void) */
uint32_t init_array_start; /* void (*)(void) entries, 4 bytes each */
uint32_t init_array_end;
};
+216
View File
@@ -0,0 +1,216 @@
/*
HOST_WATCH_HASH_TEST.C
Tests of the page-hash write tracking (port/android/host/host_watch_hash.c).
Built and run by tools/test_host_watch_hash.py; exits nonzero on a failure.
*/
#include "../host/host_watch_hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PAGES 4
static int failures;
#define CHECK(condition) \
do \
{ \
if (!(condition)) \
{ \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \
failures++; \
} \
} while (0)
struct fixture
{
uint8_t *memory;
uint8_t watched[PAGES];
uint64_t hash[PAGES];
uint32_t hashed_frame[PAGES];
uint32_t generation[PAGES];
volatile uint32_t current;
struct watch_hash watch;
};
static void setup(struct fixture *f)
{
memset(f, 0, sizeof(*f));
f->memory = aligned_alloc(WATCH_HASH_PAGE_SIZE, PAGES * WATCH_HASH_PAGE_SIZE);
memset(f->memory, 0x5a, PAGES * WATCH_HASH_PAGE_SIZE);
f->current = 1;
f->watch.base = f->memory;
f->watch.page_count = PAGES;
f->watch.watched = f->watched;
f->watch.hash = f->hash;
f->watch.hashed_frame = f->hashed_frame;
f->watch.generation = f->generation;
f->watch.current_generation = &f->current;
f->watch.frame = 1;
}
static void teardown(struct fixture *f)
{
free(f->memory);
}
static uint8_t *byte_of(struct fixture *f, uint32_t page, uint32_t offset)
{
return f->memory + page * WATCH_HASH_PAGE_SIZE + offset;
}
static void test_unchanged_page_keeps_generation(void)
{
struct fixture f;
uint32_t before;
setup(&f);
watch_hash_protect(&f.watch, 0, 0);
before = watch_hash_generation(&f.watch, 0, 0);
watch_hash_begin_frame(&f.watch);
CHECK(watch_hash_generation(&f.watch, 0, 0) == before);
teardown(&f);
}
static void test_write_is_seen_next_frame(void)
{
struct fixture f;
uint32_t before, after;
setup(&f);
watch_hash_protect(&f.watch, 1, 1);
before = watch_hash_generation(&f.watch, 1, 1);
*byte_of(&f, 1, 100) ^= 1;
/* the page was hashed in this frame already */
CHECK(watch_hash_generation(&f.watch, 1, 1) == before);
watch_hash_begin_frame(&f.watch);
after = watch_hash_generation(&f.watch, 1, 1);
CHECK(after > before);
CHECK(f.current >= after);
teardown(&f);
}
static void test_any_byte_is_seen(void)
{
static const uint32_t offsets[] = { 0, 2047, WATCH_HASH_PAGE_SIZE - 1 };
unsigned index;
for (index = 0; index < sizeof(offsets) / sizeof(offsets[0]); index++)
{
struct fixture f;
uint32_t before;
setup(&f);
watch_hash_protect(&f.watch, 2, 2);
watch_hash_begin_frame(&f.watch);
before = watch_hash_generation(&f.watch, 2, 2);
*byte_of(&f, 2, offsets[index]) += 1;
watch_hash_begin_frame(&f.watch);
CHECK(watch_hash_generation(&f.watch, 2, 2) > before);
teardown(&f);
}
}
static void test_hash_at_most_once_per_frame(void)
{
struct fixture f;
uint32_t before;
setup(&f);
watch_hash_protect(&f.watch, 0, 0);
watch_hash_begin_frame(&f.watch);
before = watch_hash_generation(&f.watch, 0, 0);
CHECK(f.hashed_frame[0] == f.watch.frame);
*byte_of(&f, 0, 8) ^= 0xff;
CHECK(watch_hash_generation(&f.watch, 0, 0) == before);
watch_hash_begin_frame(&f.watch);
CHECK(watch_hash_generation(&f.watch, 0, 0) > before);
teardown(&f);
}
static void test_unwatched_page_is_not_hashed(void)
{
struct fixture f;
setup(&f);
*byte_of(&f, 3, 0) ^= 1;
watch_hash_begin_frame(&f.watch);
CHECK(watch_hash_generation(&f.watch, 3, 3) == 0);
CHECK(f.hashed_frame[3] == 0);
teardown(&f);
}
static void test_forget_unwatches_and_bumps(void)
{
struct fixture f;
uint32_t before;
setup(&f);
watch_hash_protect(&f.watch, 2, 2);
before = watch_hash_generation(&f.watch, 2, 2);
watch_hash_forget(&f.watch, 2, 2);
CHECK(f.watched[2] == 0);
CHECK(watch_hash_generation(&f.watch, 2, 2) > before);
teardown(&f);
}
static void test_range_returns_newest(void)
{
struct fixture f;
uint32_t newest;
setup(&f);
watch_hash_protect(&f.watch, 0, 3);
watch_hash_begin_frame(&f.watch);
*byte_of(&f, 3, 5) ^= 1;
watch_hash_begin_frame(&f.watch);
newest = watch_hash_generation(&f.watch, 0, 3);
CHECK(newest == f.generation[3]);
CHECK(watch_hash_generation(&f.watch, 0, 2) < newest);
teardown(&f);
}
static void test_begin_frame_changes_serial(void)
{
struct fixture f;
uint32_t serial;
setup(&f);
serial = f.current;
watch_hash_begin_frame(&f.watch);
CHECK(f.current != serial);
teardown(&f);
}
static void test_ranges_are_clamped(void)
{
struct fixture f;
setup(&f);
watch_hash_protect(&f.watch, 2, 99);
CHECK(f.watched[2] == 1 && f.watched[3] == 1);
CHECK(watch_hash_generation(&f.watch, 5, 9) == 0);
watch_hash_forget(&f.watch, 7, 8);
teardown(&f);
}
int main(void)
{
test_unchanged_page_keeps_generation();
test_write_is_seen_next_frame();
test_any_byte_is_seen();
test_hash_at_most_once_per_frame();
test_unwatched_page_is_not_hashed();
test_forget_unwatches_and_bumps();
test_range_returns_newest();
test_begin_frame_changes_serial();
test_ranges_are_clamped();
if (failures)
printf("%d failures\n", failures);
else
printf("all tests passed\n");
return failures != 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

+43
View File
@@ -0,0 +1,43 @@
ISC License
Copyright (c) 2026 Lucide Icons and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The following Lucide icons are derived from the Feather project:
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
The MIT License (MIT) (for the icons listed above)
Copyright (c) 2013-present Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11
View File
@@ -0,0 +1,11 @@
# Lucide icons
Two icons of [Lucide](https://lucide.dev) 1.53.0 (ISC licence: `LICENSE`),
as `icons/` has them at the `1.53.0` tag of
https://github.com/lucide-icons/lucide:
- `volume-2.svg`: voice chat's speaker, of a player who talks;
- `volume-x.svg`: ... of a player muted.
`tools/ce_menus.py` draws them, white, as the menus' bitmap `voice/speaker`
(port/linux/game/network_voice.c tints it). Credited in `AUTHORS.md`.
+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
<path d="M16 9a5 5 0 0 1 0 6" />
<path d="M19.364 18.364a9 9 0 0 0 0-12.728" />
</svg>

After

Width:  |  Height:  |  Size: 476 B

+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4.702a.7.7 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.7.7 0 0 0 11 19.298z" />
<path d="m16.5 14.5 5-5" />
<path d="m16.5 9.5 5 5" />
</svg>

After

Width:  |  Height:  |  Size: 443 B

+1 -1
View File
@@ -62,7 +62,7 @@ until it is redrawn.
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__6.png` |
| `ce/shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` | 512x256 | `shell/main_menu/settings_select/player_setup/player_profile_edit/profile_options__8.png` |
13 of 157 frames are placeholders.
13 of 161 frames are placeholders.
The PC version's `ui\gamespy` and `ui\ticker` fonts are drawn with `ui\small_ui`, which
the Xbox's map has.
+32
View File
@@ -30,6 +30,38 @@ file, the line and the problem, and the game uses the Xbox's menus. The
setting `display.menus = "xbox"` also uses them. `debug.menu_open` starts on
one screen (by its name, `main_menu/settings_select/...`), for looking at it.
## Writing them again
`tools/ce_menus.py` writes every file in `ce/` and `menus.json` again,
the settings screens too (it runs `tools/port_settings.py`'s):
```sh
python3 tools/ce_menus.py \
--tags <halopc-restored>/tags \
--definitions <invader>/src/tag/hek/definition \
--redraws <ui-svg-handmade> \
--placeholder <placeholder.png> \
--pictures <extracted-ui-assets-fixed>
```
- `--tags`: the PC version's tags (its `ui\shell` widgets, strings and
bitmaps).
- `--definitions`: Invader's tag definitions, which say how the tags are
laid out.
- `--redraws`: the hand-made SVG redraws, copied to `svg/`.
- `--placeholder`: the picture drawn where a picture has no redraw and the
Xbox's map has not got it.
- `--pictures` (optional): the PC version's pictures, of which only where
each sits in its frame is read.
It needs `rsvg-convert`, Pillow, NumPy and SciPy. It writes every file, so
`git status` should show only the screens meant to change; then
`ninja linux` (or `ninja android`) embeds them in the game.
A settings screen (Controls Setup, Gamepads, Mouse, Audio, Video or Network
Setup) is changed in `tools/port_settings.py`, not in its XML, and written
with the command above.
## Elements
A file holds one `<menus>` element (`root` names the main menu's widget, in
+2 -2
View File
@@ -10,7 +10,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
| Function | Uses | |
| --- | --- | --- |
| `common button bar update` | 25 | PC function |
| `common button bar update` | 27 | PC function |
| `direct ip connect init` | 1 | PC function |
| `direct ip connect update` | 1 | PC function |
| `direct ip edit field` | 4 | PC function |
@@ -21,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `gamespy update filter settings` | 2 | PC function |
| `gt edit list update` | 1 | PC function |
| `gt select list update` | 1 | PC function |
| `mouse spinner 1wide click` | 72 | PC function |
| `mouse spinner 1wide click` | 82 | PC function |
| `mp map list update` | 1 | PC function |
| `mp prof init teamplay options` | 1 | PC function |
| `mp prof init vehicle options` | 1 | PC function |
+10
View File
@@ -332,6 +332,12 @@
<bitmap name="main_menu/multiplayer_type_select/lobby/header_lobby">
<frame png="ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options">
<frame png="ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png" width="512" height="64"/>
</bitmap>
<bitmap name="main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options">
<frame png="ce/port/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.png" width="512" height="64"/>
</bitmap>
<bitmap name="pause/pausebox_left">
<frame png="ce/port/pause/pausebox_left__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_left__1.png" width="16" height="256"/>
@@ -344,6 +350,10 @@
<frame png="ce/port/pause/pausebox_right__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_right__1.png" width="16" height="256"/>
</bitmap>
<bitmap name="voice/speaker">
<frame png="ce/port/voice/speaker__0.png" width="24" height="24"/>
<frame png="ce/port/voice/speaker__1.png" width="24" height="24"/>
</bitmap>
<bitmap name="blank">
<frame png="ce/port/blank__0.png" width="4" height="4"/>
</bitmap>
@@ -19,11 +19,8 @@
<child widget="main_menu/multiplayer_type_select/server_settings/op_invite" x="54" y="125"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_listing" x="54" y="151"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_password" x="54" y="177"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" x="54" y="203"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" x="54" y="229"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" x="54" y="255"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" x="54" y="255"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_collisions" x="54" y="281"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_coop_options" x="54" y="203"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_voice_options" x="54" y="229"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_game_type" x="54" y="203"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_player_options" x="54" y="229"/>
<child widget="main_menu/multiplayer_type_select/server_settings/op_item_options" x="54" y="255"/>
@@ -89,64 +86,25 @@
<child widget="main_menu/multiplayer_type_select/server_settings/password_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/password_value" x="230" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="15" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/multiplayer_type_select/server_settings/password_label" type="text" controller="1" width="230" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/password_value" type="text" controller="1" width="280" height="22" font="ui\small_ui" color="#FF2896FF" align="center" text_y="5" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_friendly_fire" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" x="286" y="1"/>
<widget name="main_menu/multiplayer_type_select/server_settings/op_coop_options" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_screen"/>
<on event="start" open="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options_label"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/friendly_fire_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|ON|SHIELD ONLY|EXPLOSIVES ONLY" setting="network.coop_friendly_fire" values="off|on|shields_only|explosives_only" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
<widget name="main_menu/multiplayer_type_select/server_settings/op_voice_options" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_screen"/>
<on event="start" open="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options_label"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_extra_enemies" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/extra_enemies_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="NONE|PER PLAYER|STATIC MULTIPLIER" setting="network.coop_enemies_mode" values="none|per_player|multiplier" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_per_player" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_per_player_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="25%|50%|100%|150%|200%" setting="network.coop_enemies" values="25|50|100|150|200" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_enemies_multiplier" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/enemies_multiplier_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="2X|4X|8X|16X|32X" setting="network.coop_enemies_multiplier" values="2|4|8|16|32" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_player_collisions" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="14" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/player_collisions_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="network.coop_player_collisions" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
<on event="deleted" run="port setting save"/>
<on event="left_mouse" run="mouse spinner 1wide click"/>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/op_game_type" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" run="port setup edit" open="main_menu/settings_select/multiplayer_setup/playlist_edit/game_type_select/gametype_select_screen"/>
@@ -233,11 +191,8 @@
<string text="INDICATOR OPTIONS:"/>
<string text="TEAMPLAY OPTIONS:"/>
<string text="LISTING:"/>
<string text="FRIENDLY FIRE:"/>
<string text="EXTRA ENEMIES:"/>
<string text="PER PLAYER:"/>
<string text="MULTIPLIER:"/>
<string text="PLAYER COLLISIONS:"/>
<string text="CO-OP OPTIONS:"/>
<string text="VOICE AND VOTING:"/>
<string text="PASSWORD:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/help_strings">
@@ -253,6 +208,120 @@
<string text="Friendly fire and team balance, for this game."/>
<string text="Anyone can see and join your game: it is listed\nin everyone's Server Browser."/>
<string text="Only players with your invite link can join."/>
<string text="Friendly fire, the enemies and player collisions,\nfor this game."/>
<string text="Voice chat and votes to kick a player, for this\ngame."/>
<string text="Players from the Server Browser must type it to\njoin (an invite link needs none). Enter sets it."/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_menu"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_help" type="text" controller="1" left="68" top="341" width="482" height="79" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/cap_coop_options" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_help">
<data input="game settings lists text update"/>
<on event="created" run="port setup options init"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_friendly_fire" x="54" y="73"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_extra_enemies" x="54" y="103"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_enemies_per_player" x="54" y="133"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_enemies_multiplier" x="54" y="133"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_player_collisions" x="54" y="163"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port setup options save" back="true"/>
<on event="start" run="port setup options save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_friendly_fire" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_friendly_fire_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_friendly_fire_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_friendly_fire_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_friendly_fire_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_friendly_fire" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_friendly_fire">
<string text="OFF"/>
<string text="ON"/>
<string text="SHIELD ONLY"/>
<string text="EXPLOSIVES ONLY"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_extra_enemies" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_extra_enemies_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_extra_enemies_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_extra_enemies_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_extra_enemies_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_extra_enemies" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_extra_enemies">
<string text="NONE"/>
<string text="PER PLAYER"/>
<string text="STATIC MULTIPLIER"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_enemies_per_player" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_per_player_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_per_player_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_per_player_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_per_player_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_enemies_per_player" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_enemies_per_player">
<string text="25%"/>
<string text="50%"/>
<string text="100%"/>
<string text="150%"/>
<string text="200%"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_enemies_multiplier" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_multiplier_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_multiplier_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_multiplier_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_enemies_multiplier_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_enemies_multiplier" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_enemies_multiplier">
<string text="2X"/>
<string text="4X"/>
<string text="8X"/>
<string text="16X"/>
<string text="32X"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/op_coop_player_collisions" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_player_collisions_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/coop_options/coop_player_collisions_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_player_collisions_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_player_collisions_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_player_collisions" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/var_coop_player_collisions">
<string text="ON"/>
<string text="OFF"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/coop_options_labels">
<string text="FRIENDLY FIRE:"/>
<string text="EXTRA ENEMIES:"/>
<string text="PER PLAYER:"/>
<string text="MULTIPLIER:"/>
<string text="PLAYER COLLISIONS:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/coop_options/cap_coop_options">
<string text="Players can not be hurt by weapons and explosives\nfired by the other players."/>
<string text="Players can be hurt by weapons or explosives\nfired by the other players."/>
<string text="Damage from the other players will only reduce\nshields. Health will be unaffected."/>
@@ -261,9 +330,152 @@
<string text="Enemy squads grow with the players: by the amount\nbelow for each player past the first."/>
<string text="Enemy squads are the amount below times as large,\nhowever many players there are."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="For each player past the first, enemy squads get\nthis much more of themselves (100%: as many again)."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Each enemy squad is this many times as large."/>
<string text="Players bump into each other, as in the campaign."/>
<string text="Players walk through each other, so that no one\nblocks a doorway. The AI's characters still block."/>
<string text="Players from the Server Browser must type it to\njoin (an invite link needs none). Enter sets it."/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_menu"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_help" type="text" controller="1" left="68" top="341" width="482" height="79" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/cap_voice_options" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_help">
<data input="game settings lists text update"/>
<on event="created" run="port setup options init"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_mode" x="54" y="73"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_lobby" x="54" y="103"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_kbps" x="54" y="133"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_proximity" x="54" y="163"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/op_votekick" x="54" y="193"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_button_bar" y="414"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port setup options save" back="true"/>
<on event="start" run="port setup options save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_mode" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_mode_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_mode_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_mode_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_mode_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_mode" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_mode">
<string text="OFF"/>
<string text="TEAM NEAR"/>
<string text="ANYONE NEAR"/>
<string text="TEAM"/>
<string text="TEAM, ENEMIES NEAR"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_lobby" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_lobby_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_lobby_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_lobby_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_lobby_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_lobby" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_lobby">
<string text="ON"/>
<string text="OFF"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_kbps" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_kbps_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_kbps_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_kbps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_kbps_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_kbps" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_kbps">
<string text="8 KBPS"/>
<string text="12 KBPS"/>
<string text="16 KBPS"/>
<string text="24 KBPS"/>
<string text="32 KBPS"/>
<string text="48 KBPS"/>
<string text="64 KBPS"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/op_voice_proximity" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_proximity_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/voice_proximity_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_proximity_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_proximity_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_proximity" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/var_voice_proximity">
<string text="15 M"/>
<string text="30 M"/>
<string text="45 M"/>
<string text="60 M"/>
<string text="90 M"/>
<string text="150 M"/>
</strings>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/op_votekick" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/votekick_label"/>
<child widget="main_menu/multiplayer_type_select/server_settings/voice_options/votekick_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/votekick_label" type="text" controller="1" width="300" height="22" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/multiplayer_type_select/server_settings/voice_options/votekick_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/multiplayer_type_select/server_settings/voice_options/var_votekick" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/var_votekick">
<string text="ON"/>
<string text="OFF"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/voice_options_labels">
<string text="VOICE CHAT:"/>
<string text="LOBBY VOICE CHAT:"/>
<string text="VOICE QUALITY:"/>
<string text="VOICE NEAR DISTANCE:"/>
<string text="VOTE KICK:"/>
</strings>
<strings name="main_menu/multiplayer_type_select/server_settings/voice_options/cap_voice_options">
<string text="No voice chat during the game."/>
<string text="Players hear their teammates who are near them."/>
<string text="Players hear everyone who is near them."/>
<string text="Players hear all their teammates, wherever they\nare."/>
<string text="Players hear all their teammates, and the enemies\nwho are near them."/>
<string text="Everyone hears everyone in the lobby, before and\nafter the game."/>
<string text="No voice chat in the lobby."/>
<string text="Voices at 8 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 12 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 16 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 24 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 32 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 48 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 64 kilobits a second, in the lobby\nand in the game."/>
<string text="Players 15 metres apart or less are near\nfor voice chat."/>
<string text="Players 30 metres apart or less are near\nfor voice chat."/>
<string text="Players 45 metres apart or less are near\nfor voice chat."/>
<string text="Players 60 metres apart or less are near\nfor voice chat."/>
<string text="Players 90 metres apart or less are near\nfor voice chat."/>
<string text="Players 150 metres apart or less are near\nfor voice chat."/>
<string text="Players can vote to kick a player, from the\nscoreboard."/>
<string text="Players cannot vote to kick anyone."/>
</strings>
</menus>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_mode" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_mode_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_mode_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_mode_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_mode_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_mode" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_mode">
<string text="OFF"/>
<string text="TEAM NEAR"/>
<string text="ANYONE NEAR"/>
<string text="TEAM"/>
<string text="TEAM, ENEMIES NEAR"/>
</strings>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_lobby" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_lobby_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_lobby_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_lobby_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_lobby_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_lobby" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_lobby">
<string text="ON"/>
<string text="OFF"/>
</strings>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_kbps" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_kbps_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_kbps_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_kbps_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_kbps_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_kbps" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_kbps">
<string text="8 KBPS"/>
<string text="12 KBPS"/>
<string text="16 KBPS"/>
<string text="24 KBPS"/>
<string text="32 KBPS"/>
<string text="48 KBPS"/>
<string text="64 KBPS"/>
</strings>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_proximity" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_proximity_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_proximity_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_proximity_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/voice_proximity_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_proximity" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_voice_proximity">
<string text="15 M"/>
<string text="30 M"/>
<string text="45 M"/>
<string text="60 M"/>
<string text="90 M"/>
<string text="150 M"/>
</strings>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_votekick" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/votekick_label"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/votekick_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/votekick_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/votekick_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" string_list="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_votekick" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" list_flags="items_from_strings" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="left_mouse" run="mouse spinner 1wide click"/>
</widget>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_votekick">
<string text="ON"/>
<string text="OFF"/>
</strings>
</menus>
@@ -48,6 +48,11 @@
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_friendly_fire" x="54" y="73"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_friendly_fire_penalty" x="54" y="103"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_autobalance" x="54" y="133"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_mode" x="54" y="163"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_lobby" x="54" y="193"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_kbps" x="54" y="223"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_voice_proximity" x="54" y="253"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/op_votekick" x="54" y="283"/>
<child widget="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
@@ -13,11 +13,22 @@
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="133"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="193"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_output_device" x="54" y="73" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_input_device" x="54" y="103" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="133" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" x="54" y="73" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="163" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_music_volume" x="54" y="103" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="193" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_effects_volume" x="54" y="133" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="223" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_reverb" x="54" y="163" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="253" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_enabled" x="54" y="193" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_chat" x="54" y="283" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_chat" x="54" y="223" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_volume" x="54" y="313" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_volume" x="54" y="253" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -25,11 +36,29 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_output_device" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/output_device_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/output_device_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/output_device_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/output_device_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="SYSTEM DEFAULT" setting="audio.output_device" values="default" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_input_device" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/input_device_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/input_device_spinner" x="286" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/input_device_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/input_device_spinner" type="spinner" top="2" width="206" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="SYSTEM DEFAULT" setting="audio.input_device" values="default" font="ui\large_ui" color="#FF2896FF" align="center" text_y="4" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -13 19 -7" footer_bounds="7 208 19 214">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_volume" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/volume_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/volume_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -38,7 +67,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/music_volume_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/music_volume_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/music_volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/music_volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/music_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.music_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -47,7 +76,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/effects_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.effects_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -56,7 +85,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/reverb_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.reverb" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
@@ -65,11 +94,29 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="6" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/enabled_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="audio.enabled" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_chat" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_chat_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_chat_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_chat_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_chat_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="PUSH TO TALK|OPEN MIC|OFF" setting="audio.voice_chat" values="push_to_talk|open_mic|off" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/op_voice_volume" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_volume_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_volume_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_volume_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/voice_volume_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0|1|2|3|4|5|6|7|8|9|10" setting="audio.voice_volume" values="0|0.1|0.2|0.3|0.4|0.5|0.6|0.7|0.8|0.9|1" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
@@ -81,18 +128,26 @@
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/labels">
<string text="OUTPUT DEVICE:"/>
<string text="INPUT DEVICE:"/>
<string text="MASTER VOLUME:"/>
<string text="MUSIC VOLUME:"/>
<string text="EFFECTS VOLUME:"/>
<string text="REVERB:"/>
<string text="SOUND:"/>
<string text="VOICE CHAT:"/>
<string text="VOICE VOLUME:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/audio_settings/help_strings">
<string text=""/>
<string text="Where the game's sound and the voices play."/>
<string text="The microphone voice chat listens to."/>
<string text="The volume of everything."/>
<string text="The music's volume."/>
<string text="The volume of every other sound: effects and\nspeech."/>
<string text="Echo sounds as the place you are in does, and\nmuffle those behind walls, as the Xbox did."/>
<string text="Play sound at all; from the next time the game\nstarts."/>
<string text="Talk in network games: while PUSH TO TALK is held\n(Controls), whenever you speak, or never."/>
<string text="The other players' voices."/>
</strings>
</menus>
@@ -30,7 +30,7 @@
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_sensitivity_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_sensitivity_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0.25|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" setting="input.mouse_sensitivity" values="0.25|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_sensitivity_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="0.1|0.15|0.25|0.4|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" setting="input.mouse_sensitivity" values="0.1|0.15|0.25|0.4|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/op_mouse_vertical_sensitivity" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
@@ -39,7 +39,7 @@
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_vertical_sensitivity_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_vertical_sensitivity_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="SAME|0.25|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" setting="input.mouse_vertical_sensitivity" values="0|0.25|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/mouse_vertical_sensitivity_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="SAME|0.1|0.15|0.25|0.4|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" setting="input.mouse_vertical_sensitivity" values="0|0.1|0.15|0.25|0.4|0.5|0.75|1|1.25|1.5|1.75|2|2.5|3|4" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/mouse_settings/op_invert_mouse" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's settings screen, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_viewmodel_settings_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/header_profile_video_settings"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/options_menu"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/header_profile_video_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_fov" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_viewmodel_fov" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_viewmodel_visible" x="54" y="133"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_defaults" y="1"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_fov" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="DEFAULT|80|85|90|95|100|105|110|115|120|125|130|135|140|145|150" setting="display.fov" values="0|80|85|90|95|100|105|110|115|120|125|130|135|140|145|150" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_viewmodel_fov" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_fov_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_fov_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_fov_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_fov_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="DEFAULT|80|85|90|95|100|105|110|115|120|125|130|135|140|145|150" setting="display.viewmodel_fov" values="0|80|85|90|95|100|105|110|115|120|125|130|135|140|145|150" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/op_viewmodel_visible" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_visible_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_visible_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_visible_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/viewmodel_visible_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.viewmodel_visible" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings save" back="true"/>
<on event="start" run="port settings save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/labels">
<string text="FOV:"/>
<string text="VIEWMODEL FOV:"/>
<string text="VIEWMODELS:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/help_strings">
<string text=""/>
<string text="On foot, horizontal at 16:9, in degrees.\nDefault keeps the stock view."/>
<string text="The weapon and hands, horizontal at 16:9.\nDefault keeps the weapon's stock view."/>
<string text="Draw first-person weapons, hands and attached\nvisuals. Gameplay and sound continue when off."/>
</strings>
</menus>
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The port's settings screen, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/graphics_settings_screen" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game" bitmap="bitmaps/gradient">
<on event="b" back="true"/>
<on event="back" back="true"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/header_profile_video_settings"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/options_menu"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/header_profile_video_settings" controller="1" left="35" top="11" width="605" height="59" bitmap="main_menu/settings_select/player_setup/player_profile_edit/video_settings/header_profile_video_settings">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/help" type="text" controller="1" left="68" top="350" width="482" height="60" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/help_strings" font="ui\large_ui" color="#FFFFFFFF">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_high_res_hud" x="54" y="73"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_high_res_text" x="54" y="103"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_anti_aliasing" x="54" y="133" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_anti_aliasing_android" x="54" y="133" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_shadow_resolution" x="54" y="163"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_per_pixel_lighting" x="54" y="193"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_defaults" y="1"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_ok" x="380" y="1"/>
<child widget="common_button_cancel" x="510" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_high_res_hud" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_hud_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_hud_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="0" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_hud_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_hud" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_high_res_text" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_text_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_text_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="1" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/high_res_text_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_text" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_anti_aliasing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="2" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|SMAA|SSAA 2X|MSAA 2X|MSAA 4X|MSAA 8X" setting="display.anti_aliasing" values="off|fxaa|smaa|ssaa2x|msaa2x|msaa4x|msaa8x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_anti_aliasing_android" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="android">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_android_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_android_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_android_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="3" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/anti_aliasing_android_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|MSAA 2X|MSAA 4X" setting="display.anti_aliasing" values="off|fxaa|msaa2x|msaa4x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_shadow_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/shadow_resolution_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/shadow_resolution_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/shadow_resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="4" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/shadow_resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="128|256|512|1024" setting="display.shadow_resolution" values="128|256|512|1024" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/op_per_pixel_lighting" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/per_pixel_lighting_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/per_pixel_lighting_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/per_pixel_lighting_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels" string_index="5" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/per_pixel_lighting_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.per_pixel_lighting" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/button_ok" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="1" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings save" back="true"/>
<on event="start" run="port settings save" back="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/labels">
<string text="HIGH-RES HUD:"/>
<string text="HIGH-RES TEXT:"/>
<string text="ANTI-ALIASING:"/>
<string text="ANTI-ALIASING:"/>
<string text="SHADOW RESOLUTION:"/>
<string text="PER-PIXEL LIGHTING:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/help_strings">
<string text=""/>
<string text="Draw the HUD from the high-res redraws; off\ndraws the game's own pictures."/>
<string text="Draw text and titles with high-res fonts; off\ndraws the game's own."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nand SMAA are cheap; SSAA and MSAA are sharper."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nis cheap; MSAA is sharper."/>
<string text="The size objects' shadows are drawn at: 128 as on\nthe Xbox; larger for smoother, as soft, edges."/>
<string text="Light models for each pixel, without the facets\nof the Xbox's lighting for each vertex."/>
</strings>
</menus>
@@ -14,19 +14,19 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/options_menu" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help">
<data input="port settings help"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_mode" x="54" y="73" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="97" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="97" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="121" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="145"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="169" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="193"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="217" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" x="54" y="241"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" x="54" y="265"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" x="54" y="289" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" x="54" y="289" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" x="54" y="313"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" x="54" y="337"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution" x="54" y="103" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_window_size" x="54" y="103" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_resolution_scaling" x="54" y="133" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="163" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_vsync" x="54" y="73" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_max_fps" x="54" y="193" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="223" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_interpolation" x="54" y="103" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_direct_camera" x="54" y="253" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_graphics" x="54" y="283" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_graphics" x="54" y="133" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_fov_viewmodels" x="54" y="313" platform="desktop"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_fov_viewmodels" x="54" y="163" platform="android"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -106,59 +106,21 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/direct_camera_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.direct_camera" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_hud" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" x="320" y="1"/>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_graphics" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/graphics_settings_screen"/>
<on event="start" open="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics/graphics_settings_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics_label"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/graphics_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="8" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_hud_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_hud" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_fov_viewmodels" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<on event="a" open="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_viewmodel_settings_screen"/>
<on event="start" open="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels/fov_viewmodel_settings_screen"/>
<on event="left_mouse" run="mouse emit accept event"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels_label"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_high_res_text" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/high_res_text_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.high_res_text" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="desktop">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="10" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|SMAA|SSAA 2X|MSAA 2X|MSAA 4X|MSAA 8X" setting="display.anti_aliasing" values="off|fxaa|smaa|ssaa2x|msaa2x|msaa4x|msaa8x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_anti_aliasing_android" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF" platform="android">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="11" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/anti_aliasing_android_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="OFF|FXAA|MSAA 2X|MSAA 4X" setting="display.anti_aliasing" values="off|fxaa|msaa2x|msaa4x" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_shadow_resolution" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="12" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/shadow_resolution_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="128|256|512|1024" setting="display.shadow_resolution" values="128|256|512|1024" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/op_per_pixel_lighting" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" x="320" y="1"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="13" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/per_pixel_lighting_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.per_pixel_lighting" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/fov_viewmodels_label" type="text" controller="1" width="512" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/video_settings/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
@@ -179,12 +141,8 @@
<string text="FRAME RATE LIMIT:"/>
<string text="SMOOTH MOTION:"/>
<string text="INSTANT AIM:"/>
<string text="HIGH-RES HUD:"/>
<string text="HIGH-RES TEXT:"/>
<string text="ANTI-ALIASING:"/>
<string text="ANTI-ALIASING:"/>
<string text="SHADOW RESOLUTION:"/>
<string text="PER-PIXEL LIGHTING:"/>
<string text="GRAPHICS:"/>
<string text="FOV AND VIEWMODELS:"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/video_settings/help_strings">
<string text=""/>
@@ -196,11 +154,7 @@
<string text="With V-Sync off, the most frames a second. Auto:\ntwice the display's refresh rate."/>
<string text="Draw a frame for every display refresh, blending\nbetween the game's 30 ticks a second."/>
<string text="In first person, turn the view the moment the\nmouse moves, not up to two ticks later."/>
<string text="Draw the HUD from the high-res redraws; off\ndraws the game's own pictures."/>
<string text="Draw text and titles with high-res fonts; off\ndraws the game's own."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nand SMAA are cheap; SSAA and MSAA are sharper."/>
<string text="Smooth jagged edges, which the Xbox did not. FXAA\nis cheap; MSAA is sharper."/>
<string text="The size objects' shadows are drawn at: 128 as on\nthe Xbox; larger for smoother, as soft, edges."/>
<string text="Light models for each pixel, without the facets\nof the Xbox's lighting for each vertex."/>
<string text="The HUD, text, anti-aliasing, shadows and\nlighting."/>
<string text="The field of view and the first-person\nweapon. Their defaults keep the stock view."/>
</strings>
</menus>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

+27 -1
View File
@@ -1023,11 +1023,38 @@
<string text="Players who kill a teammate will be assessed\na fifteen-second respawn penalty the next time\nthey die."/>
<string text="Team autobalancing will be disabled. Players will\nbe free to change teams as they wish."/>
<string text="Team autobalancing will be enabled. Players will\nnot be able to change to a more full team."/>
<string text="No voice chat during the game."/>
<string text="Players hear their teammates who are near them."/>
<string text="Players hear everyone who is near them."/>
<string text="Players hear all their teammates, wherever they\nare."/>
<string text="Players hear all their teammates, and the enemies\nwho are near them."/>
<string text="Everyone hears everyone in the lobby, before and\nafter the game."/>
<string text="No voice chat in the lobby."/>
<string text="Voices at 8 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 12 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 16 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 24 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 32 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 48 kilobits a second, in the lobby\nand in the game."/>
<string text="Voices at 64 kilobits a second, in the lobby\nand in the game."/>
<string text="Players 15 metres apart or less are near\nfor voice chat."/>
<string text="Players 30 metres apart or less are near\nfor voice chat."/>
<string text="Players 45 metres apart or less are near\nfor voice chat."/>
<string text="Players 60 metres apart or less are near\nfor voice chat."/>
<string text="Players 90 metres apart or less are near\nfor voice chat."/>
<string text="Players 150 metres apart or less are near\nfor voice chat."/>
<string text="Players can vote to kick a player, from the\nscoreboard."/>
<string text="Players cannot vote to kick anyone."/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/teamplay_options_labels">
<string text="FRIENDLY FIRE:"/>
<string text="FRIENDLY FIRE PENALTY:"/>
<string text="AUTO TEAM BALANCE:"/>
<string text="VOICE CHAT:"/>
<string text="LOBBY VOICE CHAT:"/>
<string text="VOICE QUALITY:"/>
<string text="VOICE NEAR DISTANCE:"/>
<string text="VOTE KICK:"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/teamplay_options_edit/var_friendly_fire">
<string text="OFF"/>
@@ -1111,7 +1138,6 @@
<string text="ROCKET WARTHOGS"/>
<string text="BANSHEES"/>
<string text="GUN TURRETS"/>
<string text="PC"/>
<string text="CUSTOM"/>
</strings>
<strings name="main_menu/settings_select/multiplayer_setup/vehicle_options_edit/var_vehicles_respawn">
+7
View File
@@ -35,6 +35,7 @@
"ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml",
"ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml",
"ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.xml",
@@ -45,6 +46,8 @@
"ce/main_menu.settings_select.player_setup.player_profile_edit.mouse_settings.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.name_edit.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.network_setup.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.fov_viewmodels.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.graphics.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.video_settings.xml",
"ce/main_menu.settings_select.player_setup.player_profile_edit.xml",
"ce/main_menu.solo_level_select.xml",
@@ -58,12 +61,16 @@
"ce/port/main_menu/multiplayer_type_select/join_game/header_server_browser.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_add_player.png",
"ce/port/main_menu/multiplayer_type_select/lobby/header_lobby.png",
"ce/port/main_menu/multiplayer_type_select/server_settings/coop_options/header_coop_options.png",
"ce/port/main_menu/multiplayer_type_select/server_settings/voice_options/header_voice_options.png",
"ce/port/pause/pausebox_center__0.png",
"ce/port/pause/pausebox_center__1.png",
"ce/port/pause/pausebox_left__0.png",
"ce/port/pause/pausebox_left__1.png",
"ce/port/pause/pausebox_right__0.png",
"ce/port/pause/pausebox_right__1.png",
"ce/port/voice/speaker__0.png",
"ce/port/voice/speaker__1.png",
"ce/shell/bitmaps/alert_bkd__0.png",
"ce/shell/bitmaps/arrow_sm_down__0.png",
"ce/shell/bitmaps/arrow_sm_down__1.png",
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="278.68" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="64" viewBox="0 0 512 64">
<defs>
<filter id="soft" filterUnits="userSpaceOnUse" x="-60" y="-60" width="632" height="184">
<feGaussianBlur stdDeviation="14.23 13.06"/>
</filter>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="64">
<rect width="512" height="59" fill="#fff"/>
</mask>
</defs>
<g mask="url(#cut)">
<rect x="29.08" y="28.05" width="326.41" height="40.95" fill="#021931" fill-opacity="0.893" filter="url(#soft)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 563 B

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
<path d="M16 9a5 5 0 0 1 0 6" />
<path d="M19.364 18.364a9 9 0 0 0 0-12.728" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#FFFFFF"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4.702a.7.7 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.7.7 0 0 0 11 19.298z" />
<path d="m16.5 14.5 5-5" />
<path d="m16.5 9.5 5 5" />
</svg>

After

Width:  |  Height:  |  Size: 438 B

+48 -4
View File
@@ -90,7 +90,9 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
- **Corrections.** The host sends each client the authoritative state of
the players' units and the game's moving objects; a client moves its
copies toward it, a small error half of the way each tick, a larger one
at once, drawn gliding from where they were. A client's own unit and
at once, drawn gliding from where they were (a player's moved at once
is on the teleporter it lands on, as a teleporter leaves one it sends,
so the client's does not send it back). A client's own unit and
vehicle are only corrected past a tolerance, so prediction does not
rubber-band: the host tells the client which of the client's ticks it
has its player at (its prediction come back), and the client compares
@@ -231,6 +233,45 @@ machine tells them: anyone with administrator or root access can change
them, and players behind one address share it. The console's `kick <player
name>` drops a player as `ban` does (every machine told), but adds no line
and keeps no address out: the player may join again at once.
The players' votes to kick a player (`network_votekick.c`): a client sends
`_distributed_message_votekick` (the player's absolute index) reliably, and
the host takes it only from the client's stream. The host alone counts:
one vote per real address (an internet play peer's endpoint, not its
stand-in), and votes again deduplicated by hardware id, which only ever
removes votes (the players who may vote are counted by address alone, so a
copied hardware id cannot lower the votes needed). A vote needs more than
half of the players who may vote, the target counted, and at least two;
starting one needs `network.votekick_minutes` played on this host in its
own ticks, voting two minutes; one vote runs at a time, and a failed vote
cools its starter down and protects its target. A vote that passes kicks as
the `kick` command does, and the host refuses the address and hardware id
for `network.votekick_ban_minutes`. The host sends each client
`_distributed_message_votekick_status` every second while a vote runs (and
as it changes), for the scoreboard, whose right click frees the mouse to
pick a player; the host's own scoreboard adds Kick and Ban. Builds without
votes drop both kinds as unknown, so the network version is unchanged.
Voice chat (`network_voice.c`, the sound in `port/linux/src/voice_audio.c`)
travels as distributed messages too, but in the lobby as well as in a game:
`network_voice_handles_message` takes them before the netcode's checks that
a game is running (`network_distributed_handle_message`, and the host's
stream and datagram paths in `network_server_message_handler.c`). A client
sends its 20 ms Opus frames to the host unreliably
(`_distributed_message_voice_up`), each with the key the host gave it over
its stream (`_distributed_message_voice_config`, with the host's settings,
again every ten seconds and when they change), so a datagram sent as from
another machine is dropped; an empty one every two seconds says it hears
voices. The host checks the key, the frame's size against its quality, a
rate of 50 frames a second (a burst of 10), and at most 8 machines talking,
then sends the frame to each machine that may hear it
(`_distributed_message_voice_down`, with that machine's key) by its
`network.voice_mode`, `network.voice_lobby` and `network.voice_proximity`,
or plays it itself. The host never decodes a frame it only relays, and
never sends a machine its own. A listener sets the gain and pan of a voice
heard for being near by its own view of the speaker's unit. Mutes are each
machine's own. Builds without voice chat never say they hear voices, so the
host sends them none, and drop the kinds as unknown.
A speed hack of less than a tenth is let be: the host's bounds on how far
and how fast a client's player moves and fires hold it to the host's time
anyway.
@@ -325,7 +366,9 @@ a pregame keep-alive every five seconds from the host
indices from the upper half of the object array, clear of the host's.
- Ten times a second, what every unit carries (the host's weapons, slot
for slot, their ammunition, the weapon in hand, the grenades); a
client moves the same weapon objects in and out of its units. A
client moves the same weapon objects in and out of its units, which
carry no grenades until the host says (not the unit tag's: a player
spawned with the button held threw one the host's copy had not). A
change of weapons or grenades goes to every client at once; one of
ammunition only to the unit's player's machine at once, and to the
others as often as they are sent that player. A client takes its own
@@ -379,8 +422,9 @@ a pregame keep-alive every five seconds from the host
player; damage one of their weapons (a vehicle's a driver's or
gunner's; now or in the last ten seconds), a grenade the host's own game
saw them throw in the last ten seconds that has not gone off (each
throw's explosion is taken once, its other hits that tick with it:
holding grenades deals nothing) or the vehicle they drove (in the
throw's explosion is taken once, its other hits and its other damage
(a frag grenade's shock wave) that tick with it: holding grenades
deals nothing) or the vehicle they drove (in the
last ten seconds: its collisions) can deal (its projectiles' impacts
and detonations, followed through the tags), no harder than it can be
(all of it, but an airborne melee blow's half again); of the shape the
+236 -10
View File
@@ -21,13 +21,65 @@ To build:
- The 32-bit glibc development files: `lib32-glibc` on Arch Linux,
`gcc-multilib` and `libc6-dev-i386` on Debian and Ubuntu.
- The 32-bit SDL3: `lib32-sdl3` on Arch Linux, `libsdl3-dev:i386` on Debian
and Ubuntu.
and Ubuntu. The portable build does not need it: refer to "Portable
build".
To start the game:
- glibc 2.29 or later (32-bit), for the builds from GitHub Actions, which
are built against glibc 2.31. They start on SteamOS 3 (Steam Deck). They
should also start on Debian 11, Ubuntu 20.04, Fedora 32 and later
distributions, but this is not tested. A build that you make without
`--portable` needs the glibc of the computer that built it, or a later
one.
- The 32-bit OpenGL libraries (`lib32-mesa`).
- The 32-bit PipeWire or PulseAudio client libraries (`lib32-pipewire` or
`lib32-libpulse`).
- The 32-bit X11 libraries (`lib32-libx11`, `lib32-libxext`). In a Wayland
session, the game uses them through XWayland. SDL uses Wayland itself
only if the 32-bit Wayland libraries are version 1.20 or later
(`lib32-wayland`, `lib32-libxkbcommon`; Debian 11, Ubuntu 20.04 and
Fedora 32 have older ones) and the compositor has the fifo-v1 protocol.
On GNOME, the window then has borders only with the 32-bit libdecor
(`lib32-libdecor`).
SteamOS has all of these. Its system is read-only, and the builds from
GitHub Actions need no package: they bring their own SDL3
(`libSDL3.so.0`, next to the executable).
### Portable build
The builds from GitHub Actions are portable builds (`--portable`; refer to
"Build options" in the main [README](../../README.md#build-options)). The
portable build starts on more systems than the build machine's:
- It is compiled and linked against the 32-bit glibc 2.31 of Debian 11, the
glibc of the Steam Runtime 3 ("sniper"), not against the glibc of the
build machine. An executable needs the glibc version that it was built
against, or a later one.
- It brings SDL 3 (`libSDL3.so.0`), built from source against the same
glibc. The executable looks for it in its own folder first (a `DT_RPATH`
of `$ORIGIN`, which comes before `LD_LIBRARY_PATH`), so that it uses this
SDL even when Steam sets `LD_LIBRARY_PATH`. Few distributions have a
32-bit SDL 3. This SDL loads X11, Wayland, libdecor, PipeWire, PulseAudio
and ALSA when it starts, so it starts with whichever of them the system
has. It is linked only to glibc. Its license is `SDL3-LICENSE.txt`.
At the first build, `tools/linux_sysroot.py` downloads the Debian packages
(about 25 MB) and the source of SDL. Each package comes from
`archive.debian.org` or `deb.debian.org`, or else from
`snapshot.debian.org`; the source of SDL comes from GitHub or from
`libsdl.org`. It checks each download against its SHA-256 sum. The system root is in
`build/linux/third_party/sysroot`. `tools/linux_sysroot.cmake` builds SDL
against it.
To make the portable build you need, in addition to the tools above,
CMake, `pkg-config` (`pkgconf`) and `wayland-scanner` (`wayland` on Arch
Linux, `libwayland-bin` on Debian and Ubuntu). You do not need the 32-bit
SDL3. You need the 32-bit glibc only for the tests.
To see the glibc version that a build needs, enter
`readelf -V build/linux/halo | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1`.
## Build the game
@@ -39,6 +91,11 @@ To start the game:
Enter `build/linux/halo`.
To play on a Steam Deck, unpack `halo-linux-release.zip` into a folder (in
Desktop Mode), and add `halo` to Steam as a non-Steam game ("Add a Game" in
the Games menu of Steam). The game then starts in Game Mode as well. Refer
to "Steam Deck".
The game data is the folder that contains `maps/`, from an Xbox disc image
of any version of the game. The game looks for this folder in this
sequence:
@@ -60,6 +117,38 @@ The game writes the copy to `maps.partial`. When the copy is complete, the
game changes the name to `maps`. If the copy stops before it is complete,
the game asks for the disc image again at the next start.
### Steam Deck
Do the set-up in Desktop Mode: unpack the release, put `maps/` next to
`halo` (or start the game one time to copy it from a disc image), and add
`halo` to Steam. Then start the game from the library in Game Mode.
- Screen: the game fills the screen without borders (`display.mode` empty,
thus borderless). The 3D view and the HUD have the shape of the screen
(16:10) and are drawn at its resolution; the menus are at the center.
- Controls: Steam Input gives the game a virtual controller, which the game
operates as the controller of the Xbox. With the template that Steam selects
("Gamepad With Joystick Trackpad"), the buttons, sticks and triggers have
the functions of the same controls on an Xbox controller, the right
trackpad operates as the right stick, and the back buttons (L4, L5, R4,
R5) do nothing until you assign them in the controller settings of the
game in Steam. With a template that makes a trackpad a mouse, the mouse
aims in the game and moves the pointer in the menus.
- Text: names of profiles and gametypes use the keyboard of the game, which
the controller operates. The text fields of the menus (the name and the
password in Server Setup, and the password of a game in the server
browser) open the keyboard of Steam. Type the text, then select Enter on
that keyboard.
- Frame rate: the game shows one frame for each refresh of the display. It
follows the refresh rate and the frame limit of Quick Access >
Performance (40 to 60 Hz on the LCD model, up to 90 Hz on the OLED model).
The world is calculated at 30 Hz at all rates. Keep `display.vsync =
true`. Refer to "Frame rate".
- Sleep: the clocks of the game do not count the time that the Deck sleeps,
so the game continues from where it stopped. A network game does not
wait: the host drops a machine that it has not heard from for 15 seconds,
and while a Deck that hosts sleeps, the other players have no host.
## Files and folders
| Xbox drive | Folder |
@@ -121,9 +210,15 @@ gamepads' only.
| zoom | Z, middle mouse button |
| show the scores (hold) | tab |
| pause menu | escape |
| screenshot | F10 |
| talk in voice chat (hold) | V |
Always: \` opens the developer console, F12 releases or captures the mouse,
F11 changes between fullscreen and window.
F11 changes between fullscreen and window. Screenshot (default F10,
rebindable under Controls Setup > Actions, below Pause Menu; not on Android) saves a PNG of the completed
frame to `screenshots/` beside `maps/`, named `YYYY-MM-DD_HH.MM.SS.png` in
local time, and prints the filename in the console. Captures in the same
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.
@@ -134,6 +229,8 @@ In the menus, the mouse moves a pointer:
- A left click selects the item. On a setting with values, a click on the
left or right half changes the value. On a button in the key of a screen
(for example "B = Back"), a click pushes that button.
- On the on-screen keyboard (a profile's name), a left click presses the
key below the pointer, or pushes the "B =BACK" or "A =ENTER" legend.
- A right click goes back.
- The mouse wheel moves through the items.
@@ -194,6 +291,12 @@ stay. The Xbox's pause box is drawn taller to hold them (a redraw,
what is below its list moves down. The few pictures of the settings that
come from the main menu's map are not drawn there.
In a single-player campaign, the pause menu has SETTINGS too, before REVERT
TO SAVED, in the same box: a list with room for it centres its rows, and
one without keeps its size, its rows closer. It opens the same settings,
while the game stays paused, with only Controls, Gamepads, Mouse, Audio and
Video Setup; Gamepads' OK saves the profile at once.
The menus are XML files in `port/assets/menus` (`tools/ce_menus.py` writes
them from the PC version's tags), which the game contains. To change them,
put files in a `menus` folder next to `config.toml`: a file with the same
@@ -233,6 +336,9 @@ the setting for one start of the game. It has priority over the file.
| `debug.gpu_flush_draws` | `-1` | `HALO_GPU_FLUSH_DRAWS` | Flush the GPU's pipeline every this many draws. `-1`: every 3 on Intel graphics with Mesa's driver, which can otherwise hang in the game's long runs of small draws and reset the desktop's graphics too. `0`: never. |
| `display.interpolation` | `true` | `HALO_INTERPOLATION` | `true`: one frame for each refresh of the display. `false`: 30 frames each second, as on the Xbox. Refer to "Frame rate". |
| `display.direct_camera` | `true` | `HALO_DIRECT_CAMERA` | `true`: in first person, on foot, the view points where the player aims in each frame, not where the last tick left it. Refer to "Frame rate". |
| `display.fov` | `0.0` | `HALO_FOV` | The first-person view's field of view on foot, in degrees across at 16:9, from 20 to 150. `0`: the stock view. Refer to "Field of view". |
| `display.viewmodel_fov` | `0.0` | `HALO_VIEWMODEL_FOV` | The first-person weapon's and hands' field of view, in degrees across at 16:9, from 20 to 150. `0`: the weapon's stock view, also when `display.fov` widens the world. Refer to "Field of view". |
| `display.viewmodel_visible` | `true` | `HALO_VIEWMODEL_VISIBLE` | `true`: the first-person weapon, hands and what is attached to them are drawn. `false`: they are not; firing, animation, sound and lights go on, and other players' models are drawn. |
| `display.high_res_hud` | `true` | `HALO_HIGH_RES_HUD` | `true`: the HUD (meters, counters, panels and their outlines, the motion sensor, reticles, waypoints, scopes) is drawn from the high-res assets in `port/assets/hud`, 8x the size of the maps' bitmaps. The bitmaps with English text keep the maps' own. `false`: the maps' own bitmaps. |
| `display.high_res_text` | `true` | `HALO_HIGH_RES_TEXT` | `true`: the menus' and HUD's text is drawn with the fonts in `port/assets/fonts` (Overpass, in place of the maps' Interstate) at the resolution the game draws at, laid out as before, and the menus' titles are drawn from the high-res pictures in `port/assets/titles`. `false`: the maps' bitmap fonts and titles. |
| `display.shadow_resolution` | `128` | `HALO_SHADOW_RESOLUTION` | The size of the maps that the shadows of the objects are drawn in, in pixels each way: `128`, `256`, `512` or `1024` (other values go down to one of these). The game draws the shadow of each object into a map of 128x128 pixels, blurs it and projects it onto the ground. On a large screen, the edges of these shadows show steps that move when the object moves. A larger map makes the edges smooth; the blur is made wider to match, so the shadows are as soft as on the Xbox. Each doubling adds two passes of the blur. `128`: as on the Xbox. |
@@ -248,14 +354,18 @@ the setting for one start of the game. It has priority over the file.
| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. |
| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. |
| `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). |
| `audio.voice_chat` | `"push_to_talk"` | `HALO_VOICE_CHAT` | How you talk in voice chat: `"push_to_talk"` (while `controls.push_to_talk` is held; the microphone opens when you first press it), `"open_mic"` (when the microphone hears speech), or `"off"`. You hear the other players in every case. Refer to "Voice chat". |
| `audio.voice_volume` | `1.0` | `HALO_VOICE_VOLUME` | The volume of the voices of the other players, `0` to `2`. |
| `audio.output_device`, `audio.input_device` | `"default"` | `HALO_AUDIO_OUTPUT_DEVICE`, `HALO_AUDIO_INPUT_DEVICE` | The speakers and the microphone, by the name that Settings > Audio shows, or `"default"` for the device of the system. If the device is not found, the game uses the device of the system. Not on Android. |
| `audio.loose_sounds` | `false` | `HALO_LOOSE_SOUNDS` | For those who make sounds. `true`: each sound of a map that has a sound tag file of its name in `tags/` in the data root (for example `tags/sound/sfx/weapons/assault rifle/fire.sound`) plays from that file. The files are Halo PC tag files, as the Halo Editing Kit and Invader write them. At the console, `loose_sounds_reload` reads the files again, and `loose_sounds false` plays the sounds of the map again. When a file changes, all sounds stop. |
| `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. |
| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. |
| `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. |
| `input.mouse_aim_assist` | `false` | `HALO_MOUSE_AIM_ASSIST` | `true`: the magnetism of the controller also operates for the mouse. `false`: when the mouse moved after the right stick, the view is not slowed or dragged by a target. The autoaim of the bullets operates in both cases. |
| `controls.<action>` | (the table in "Controls") | `HALO_KEY_<ACTION>` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. |
| `controls.<action>` | (the table in "Controls") | `HALO_KEY_<ACTION>` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`, `screenshot`, `push_to_talk`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. |
| `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. |
| `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. |
| `game.enhanced_animations` | `true` | `HALO_ENHANCED_ANIMATIONS` | `true`: the player bipeds' grenade throws keep their legs moving, blended by speed and direction (crouched throws stay crouched, throws in the air use the jump's legs), Warthog and Scorpion riders stay seated to throw and let go of the grips to throw and reload, and a player turns with the aim while throwing, as while meleeing. `false`: the original animations, which freeze the legs during a throw and stand a rider up. Only in config.toml, not in the menus. |
| `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". |
| `paths.saves` | `""` | `HALO_SAVE_ROOT` | The save root. Refer to "Files and folders". |
| `network.address` | `""` | `HALO_NET_ADDRESS` | The IPv4 address of this machine for system link. Refer to "Play on one computer". |
@@ -266,11 +376,18 @@ the setting for one start of the game. It has priority over the file.
| `network.allow_upnp` | `true` | `HALO_NET_ALLOW_UPNP` | `true`: internet play can ask the router to forward its port (UPnP). `false`: the game does not ask. Refer to "Internet play". |
| `network.public_lobby` | `true` | `HALO_NET_PUBLIC_LOBBY` | `true`: the server browser. Public games are listed, and Join Game > Server Browser shows them. `false`: no games are listed or shown. Refer to "Server browser". |
| `network.host_public` | `true` | `HALO_NET_HOST_PUBLIC` | `true`: a new game of Create Game > Internet starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in Server Setup changes it for each game. Refer to "Server browser". |
| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup writes its choice here. Their AI allies they always can, as in the campaign. |
| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. |
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. |
| `network.coop_friendly_fire` | `"on"` | `HALO_NET_COOP_FRIENDLY_FIRE` | Whether the players of an online co-op game hurt each other: `"off"`, `"on"`, `"shields_only"` or `"explosives_only"`. FRIENDLY FIRE in co-op's Server Setup > Co-op Options writes its choice here. Their AI allies they always can, as in the campaign. |
| `network.coop_player_collisions` | `true` | `HALO_NET_COOP_PLAYER_COLLISIONS` | Whether the players of an online co-op game bump into each other. `false`: they walk through each other, so that one cannot block a doorway or stand on another; they still bump into the AI's characters. PLAYER COLLISIONS in co-op's Server Setup > Co-op Options writes its choice here. |
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup > Co-op Options writes its choice here. |
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup > Co-op Options writes its choice here. |
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup > Co-op Options writes its choice here. |
| `network.voice_lobby` | `true` | `HALO_NET_VOICE_LOBBY` | When you host: `true`, all players hear all players in the lobby, before and after a game. |
| `network.voice_mode` | `"team_global_enemy_proximity"` | `HALO_NET_VOICE_MODE` | When you host: who hears whom during a game. `"off"`; `"team_proximity"` (teammates who are near); `"team_enemy_proximity"` (all players who are near); `"team_global"` (all teammates); `"team_global_enemy_proximity"` (all teammates, and enemies who are near). Refer to "Voice chat". |
| `network.voice_kbps` | `24` | `HALO_NET_VOICE_KBPS` | When you host: the voice quality in the lobby and in a game, in kilobits per second, `8` to `64`. |
| `network.voice_proximity` | `15.0` | `HALO_NET_VOICE_PROXIMITY` | When you host: the distance in world units (1 unit is approximately 3 metres) at which players are near, for voice chat. `5` to `100`. |
| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. VOTE KICK in Server Setup > Teamplay Options (in co-op, Voice and Voting) writes its choice here. |
| `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. |
| `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). |
| `network.coop_public` | `false` | `HALO_NET_COOP_PUBLIC` | `true`: an online co-op game (Create Game > Internet, a SINGLEPLAYER map) starts as PUBLIC. `false`: it starts as PRIVATE. LISTING in co-op's Server Setup writes its choice here. Refer to "Server browser". |
| `network.brokers_file` | `"brokers.txt"` | `HALO_NET_BROKERS_FILE` | The file of the public MQTT brokers that let the machines of an invite find each other, and that carry the listings of the server browser: next to `config.toml`, unless a full path. One `host:port` on each line, up to 4; `#` starts a comment. |
| `network.stun_servers` | Google and Cloudflare | `HALO_NET_STUN` | The public STUN servers (`host:port`, with commas between them) that give the internet address of a machine. |
@@ -285,8 +402,12 @@ the setting for one start of the game. It has priority over the file.
| `debug.menu_open` | `""` | `HALO_MENU_OPEN` | Start on this screen of the menus (`main_menu/settings_select/...`, as `port/assets/menus` names it), a player profile being edited, to look at it. |
| `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). |
| `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. |
| `debug.touch_targets` | `false` | `HALO_TOUCH_TARGETS` | Outlines the tap targets of the menus (item green, value blue, list slot yellow, legend button red, the band beside the slots of a list orange, keys of the on-screen keyboard white), marks where the last finger went down and the last tap landed for 3 seconds, and logs each tap with the target that it hit (for a value, also where it splits into previous and next): to judge the accuracy of touch. |
| `debug.network_latency`, `debug.network_loss`, `debug.network_corrupt`, `debug.network_corrupt_stream`, `debug.network_corrupt_after` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS`, `HALO_NETWORK_CORRUPT`, `HALO_NETWORK_CORRUPT_STREAM`, `HALO_NETWORK_CORRUPT_AFTER` | The game holds all the data that it receives for this number of milliseconds, ignores this percentage of the datagrams, and damages this percentage of the datagrams it receives, and this percentage of its reads of streams, at random (bytes changed, cut short, stretched or replaced), from this many seconds after the start. Use the first two to test the netcode as on the internet, and the others to test that nothing another machine sends can crash the game (a damaged stream is closed, so a little goes a long way; a host's messages to its own client are damaged too, so start damaging once the game has started). |
| `debug.voice_test` | `false` | `HALO_VOICE_TEST` | Automatic tests of voice chat: a tone replaces the microphone, and each voice that the game hears is written to the log once each second. |
| `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. |
| `debug.profile_record`, `debug.profile_record_when`, `debug.profile_memory` | `false`, `"start"`, `256` | `HALO_PROFILE_RECORD`, `HALO_PROFILE_RECORD_WHEN`, `HALO_PROFILE_MEMORY` | Profiling builds only (`configure.py --profile`). Record without a command; `"start"` records from the first frame until the first map change, `"game"` records each game outside the main menu. Memory is 4–1024 MB. Numbered `profile_<stamp>_<role>.part<n>.json` files remain in `profiles/` after recording. Refer to "Profiling builds" in the main README. |
With Mesa drivers, the game sends its GL calls through the GL thread of
Mesa. To stop this, set the environment variable `mesa_glthread=false`.
@@ -332,6 +453,10 @@ Each frame shows the world between the last two ticks
the last tick.
- Rotations use quaternions. Positions and scales are linear.
- A teleport, a respawn or a cut of the camera does not mix. It jumps.
- After a long frame (several ticks in one frame), the camera mixes the
last tick only, as the objects do.
- In cinematics, a camera that moves with an object (the lifepod in a30,
a Pelican) moves with the object as it is drawn.
Thus the frames are one tick (33 ms) after the calculation. The calculation
does not change.
@@ -357,6 +482,29 @@ the world (such as `rasterizer_wireframe`). Refer to `NETCODE.md`.
The frame rate shows at the bottom right of the screen. It is the mean over
half a second.
## Field of view
Video Setup's FOV AND VIEWMODELS screen sets the field of view of the
first-person view (`display.fov`) and of the weapon and hands
(`display.viewmodel_fov`), from 80 to 150 degrees in steps of 5, and can
hide the weapon (`display.viewmodel_visible`). DEFAULT, the setting's `0`,
keeps the stock view. `config.toml` takes any angle from 20 to 150.
- An angle is across the screen at 16:9. Another shape keeps the same
angle up and down, as the stock view does.
- The field of view applies on foot in first person only. Vehicles, death,
cinematics and scripted cameras keep their own view.
- A scope's zoom levels keep their stock view: the extra width fades out as
the zoom comes in. A view narrower than the stock one narrows the zoom
too.
- The reticles scale with the view, so that they stay on the aim. The
scopes' pictures keep their place.
- By default the weapon keeps its stock view when the world is wider:
arms and a gun right against the camera stretch at a wide angle.
Only this machine's view changes. Nothing the machines send each other
changes, so players with different settings play together.
## System link
The Xbox game lets 16 players on 4 machines play a system link game. This
@@ -456,6 +604,47 @@ When the machines connect, the game of the host shows in Multiplayer,
System Link. Join the game as on a local network. System link on a local
network does not need an invite.
### Voice chat
Network games have voice chat, on the local network and on the internet.
Hold V (`controls.push_to_talk`, Settings > Controls Setup) to talk, or set
VOICE CHAT in Settings > Audio to OPEN MIC. VOICE VOLUME sets the volume of
the other players. On Android, set VOICE CHAT to OPEN MIC to talk: the game
then asks for the microphone.
OUTPUT DEVICE and INPUT DEVICE in Settings > Audio select the speakers and
the microphone (SYSTEM DEFAULT, the default, follows the system). The list
has the devices that the computer had when the game started.
The host sets voice chat for all players in Server Setup > Teamplay Options
(`network.voice_*` in `config.toml`):
- In the lobby, before and after a game, all players hear all players
(LOBBY VOICE CHAT).
- During a game (VOICE CHAT): OFF; TEAM NEAR; ANYONE NEAR; TEAM; or TEAM,
ENEMIES NEAR (the default). Near is VOICE NEAR DISTANCE (45 metres is the
default), and both players must be alive. A player who is near sounds
quieter when further, and from their direction. In a game without teams
all players are enemies; in co-op, all are teammates.
- The quality (VOICE QUALITY), 8 to 64 kilobits per second (24 is the
default), in the lobby and in the game.
In co-op, Server Setup has no Teamplay Options: the same settings are in
Server Setup > Voice and Voting.
A speaker shows next to the name of a player who talks: in the lobby, on
the scoreboard, in a list at the left of the screen during a game, and
beside the name above their head (when names are shown there:
`display.player_names`). To mute a player, open the scoreboard,
right-click, click the player, and click Mute voice. You no longer hear
them, and nobody else is told.
The host sends each voice on to the players who can hear it. The host
accepts a voice only from the machine of the player (with a key that it
gives each machine on its connection), only at the quality it set, at most
50 frames each second from a machine, and from at most 8 players at a time.
The voices are compressed with Opus (`port/third_party/opus`).
### Server browser
Server Setup in Create Game > Internet has a LISTING row:
@@ -585,6 +774,41 @@ Only machines with the invite can find the game:
Refer to `NETCODE.md`. `kick <player name>` drops the player the same
way, but keeps nothing: no line in `bans.txt`, and the player can join
again at once. In co-op, `bringto` brings every player to the host.
- Players can vote to kick a player. The host turns this on or off with
VOTE KICK in Server Setup > Teamplay Options (`network.votekick`; in
co-op, Server Setup > Voice and Voting). Hold the scoreboard key, right-click to
show the pointer, and click the name of the player. Then click **Start
a vote to kick**. Other players vote in the same way, and see the vote on
the scoreboard. `votekick <player name>` in the developer console does
the same. The host also gets **Kick** and **Ban** in this menu: these do
the same as the `kick` and `ban` commands (click **Ban** two times). The
host counts the votes, and these rules prevent abuse:
- The vote passes when more than half of the players vote for it, and at
least two players. The player of the vote is counted, but cannot vote.
Thus, in a game of two equal teams, one team cannot kick a player of
the other team without help.
- The host counts one vote for each internet address (for internet
play, the real address of the player, not the address of the tunnel).
Two machines at one address, or with one hardware id, have one vote.
- To start a vote, a player must have played for
`network.votekick_minutes` (5) on this server. To vote, a player must
have played for 2 minutes (or less, if that setting is less). The host
counts the time. When a player joins again, the time starts again.
Players who cannot vote yet are not counted.
- The host only accepts a vote that comes on the connection of the
player, not a datagram, which another machine can send with the
address of the player.
- One vote runs at a time, for 45 seconds, with 30 seconds before the
next vote. If a vote fails, the player who started it cannot start a
vote for 5 minutes, and nobody can start a vote against the same player
for 10 minutes.
- Nobody can vote to kick a player of the host.
- A player kicked by a vote cannot join again for
`network.votekick_ban_minutes` (30), by address and hardware id.
- A player who leaves during a vote against them is kept out the same
way, as if the vote passed. While the vote runs, the host refuses the
player if they try to join again. Thus nobody can avoid a vote by
leaving.
So that every player can be named, the host trims the spaces around a
name and removes characters that draw as nothing. A
letter with a mark is typed as the plain letter (`ban jose` for "José").
@@ -754,7 +978,7 @@ These files supply the MSVC functions that clang does not have:
| `port/include/xdk` | The Xbox SDK declarations. The compiler reads this folder after all the other folders. |
| `tools/linux_msvc_semantics.py` | Makes a header that declares each struct tag at file scope, as MSVC does. It also makes the header inline functions weak, as the COMDAT functions of MSVC. `game/msvc_comdat.c` gives one external copy of each. |
| `include/halo_linux_winsock_names.h` | Gives new names to the Winsock functions of the SDK. Thus they do not link to the glibc functions with the same names. |
| `include/halo_linux_source_fixups.h` | Repairs one declaration conflict (`rasterizer_debug_drawing_begin`). |
| `include/halo_linux_source_fixups.h` | Declares the port's functions that the game's sources call. |
`tools/linux_link_check.py` stops the link if a weak reference has no
definition. Without this check, the linker gives the reference the address
@@ -779,7 +1003,9 @@ definition. Without this check, the linker gives the reference the address
performance captures (`gpu-trace.service`), and its Mesa then writes a
marker for each traced driver function: on the Steam Frame, some 480,000
writes a second, which took the game from the headset's 72 Hz to about
50 frames a second. The Steam Deck runs the same service and Mesa.
50 frames a second. The Steam Deck runs the same service, but its Mesa
(25.3, 32-bit and 64-bit) has no markers to write: there the refusal
changes nothing (measured: the same frame times and power either way).
`HALO_GPU_TRACE_MARKERS=1` lets the driver write them, to capture with
gpuvis.
+295 -84
View File
@@ -78,6 +78,8 @@ their handlers open opens.
#include "halo_menus.h"
#include "custom_edition_cache.h"
#include "custom_edition_maps.h"
#include "network_voice.h"
#include "text/draw_string.h"
/* (internet play's server browser: the platform layer's) */
#include "../src/p2p.h"
@@ -101,6 +103,8 @@ void platform_binding_capture_begin(void);
int platform_binding_capture_poll(int *input);
void halo_input_name(int input, char *name, unsigned int size);
short pc_menu_string_index(long definition_index);
/* cseries_windows.c's */
unsigned long system_milliseconds(void);
/* the game's (port) */
boolean ui_widget_port_dispatch_event(struct widget_instance *widget, short event_type, short controller_index,
@@ -815,6 +819,7 @@ static void profile_name_show(struct widget_instance *description)
static struct player_profile profile;
static long read_index = NONE;
static unsigned long read_time;
static boolean read_good;
long index = player_ui_get_active_player_profile_index(0);
if (!description)
@@ -826,13 +831,17 @@ static void profile_name_show(struct widget_instance *description)
}
else if ((index = player_ui_get_player1_last_used_profile_index()) == NONE)
return;
else if (index != read_index || system_milliseconds() - read_time > 1000)
else
{
read_index = NONE;
if (!player_profile_get(index, &profile))
/* (one that cannot be read too: tried again a second later) */
if (index != read_index || system_milliseconds() - read_time > 1000)
{
read_index = index;
read_good = player_profile_get(index, &profile);
read_time = system_milliseconds();
}
if (!read_good)
return;
read_index = index;
read_time = system_milliseconds();
}
text_set(named(description, "current_profile_name", 0), profile.player_name);
}
@@ -1497,6 +1506,17 @@ static boolean color_list_initialize(struct widget_instance *list)
return TRUE;
}
/* (ui_widget.c) whether the pointer moved the menus' focus last */
boolean ui_widget_port_pointer_focused(void);
/* Whether a list whose focus is on its first or last row moves on: when the
d-pad put it there. The pointer resting there would make it move every
frame; the wheel scrolls it instead. */
static boolean list_scrolls_at_end(void)
{
return !ui_widget_port_pointer_focused();
}
/* a list of more items than its rows: the row chosen kept off its ends
while there are more past them, the list moving instead; the item chosen,
or NONE (its buttons) */
@@ -1504,12 +1524,12 @@ static short list_scroll(struct widget_instance *list, short *first, short count
{
short row = focused_row(list);
if (row == rows - 1 && *first + rows < count)
if (row == rows - 1 && *first + rows < count && list_scrolls_at_end())
{
(*first)++;
focus_row(list, --row);
}
else if (row == 0 && *first > 0)
else if (row == 0 && *first > 0 && list_scrolls_at_end())
{
(*first)--;
focus_row(list, ++row);
@@ -1875,6 +1895,8 @@ static struct
{ "controls.flashlight", L"FLASHLIGHT", 2 },
{ "controls.scoreboard", L"SHOW SCORES", 2 },
{ "controls.pause", L"PAUSE MENU", 2 },
{ "controls.screenshot", L"SCREENSHOT", 2 },
{ "controls.push_to_talk", L"PUSH TO TALK", 2 },
};
static struct
@@ -2198,7 +2220,7 @@ void p2p_set_hosting_allowed(int allowed);
int p2p_peer_address(unsigned char const *identifier, unsigned long *address);
int platform_clipboard_get(char *text, int size);
void platform_clipboard_set(char const *text);
void platform_text_field(int typing);
void platform_text_field(int typing, int password);
#ifdef HALO_ANDROID
/* (VR: the Xbox's on-screen keyboard, virtual_keyboard.c) */
int halo_vr_active(void);
@@ -2229,9 +2251,10 @@ static struct
starts with COOPERATIVE_DEFAULT_PLAYERS players at most) */
short cooperative_maximum_players_index;
boolean cooperative_maximum_players_set;
/* co-op's FRIENDLY FIRE shown (network.coop_friendly_fire's) */
/* the co-op game's FRIENDLY FIRE, and its PLAYER COLLISIONS OFF, as it
starts (CO-OP OPTIONS': network.coop_friendly_fire's and
network.coop_player_collisions': server_start) */
short cooperative_friendly_fire;
/* co-op's PLAYER COLLISIONS shown, OFF (network.coop_player_collisions's) */
boolean cooperative_no_player_collisions;
/* the browser's games */
struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
@@ -2297,8 +2320,8 @@ static boolean text_field_editing(struct widget_instance *row)
return text_field.row && (!row || text_field.row == row);
}
static void text_field_begin(struct widget_instance *row, char const *text, short maximum,
void (*done)(char const *text))
static void text_field_open(struct widget_instance *row, char const *text, short maximum,
void (*done)(char const *text), boolean masked)
{
struct key_stroke key;
@@ -2307,30 +2330,35 @@ static void text_field_begin(struct widget_instance *row, char const *text, shor
snprintf(text_field.before, sizeof(text_field.before), "%s", text);
text_field.maximum = (short)MIN(maximum, TEXT_FIELD_LENGTH - 1);
text_field.done = done;
text_field.masked = FALSE;
text_field.masked = masked;
text_field_shown_time = system_milliseconds();
while (input_get_key(&key))
;
platform_text_field(TRUE);
platform_text_field(TRUE, masked);
#ifdef HALO_ANDROID
if (halo_vr_active())
text_field_keyboard_launch();
#endif
}
static void text_field_begin(struct widget_instance *row, char const *text, short maximum,
void (*done)(char const *text))
{
text_field_open(row, text, maximum, done, FALSE);
}
/* a password's field: as text_field_begin, its text shown as stars */
static void text_field_begin_masked(struct widget_instance *row, char const *text, short maximum,
void (*done)(char const *text))
{
text_field_begin(row, text, maximum, done);
text_field.masked = TRUE;
text_field_open(row, text, maximum, done, TRUE);
}
static void text_field_end(boolean keep)
{
void (*done)(char const *text) = text_field.done;
platform_text_field(FALSE);
platform_text_field(FALSE, FALSE);
text_field.row = NULL;
text_field.done = NULL;
if (!keep)
@@ -2717,13 +2745,13 @@ static void gametype_list_update(struct widget_instance *list)
if (row == list->focused_child && row_index >= 1 && row_index <= GAMETYPE_ROWS)
focused = (short)(row_index - 1);
}
if (focused == count - 1 && multiplayer.gametype_first + count < multiplayer.bank_count)
if (focused == count - 1 && multiplayer.gametype_first + count < multiplayer.bank_count && list_scrolls_at_end())
{
multiplayer.gametype_first++;
focus_row(list, (short)focused);
focused--;
}
else if (focused == 0 && multiplayer.gametype_first > 0)
else if (focused == 0 && multiplayer.gametype_first > 0 && list_scrolls_at_end())
{
multiplayer.gametype_first--;
focus_row(list, 2);
@@ -2819,18 +2847,15 @@ static char const *const server_settings_gametype_rows[] =
/* the most players a co-op game hosted starts with (maximum_players') */
#define COOPERATIVE_DEFAULT_PLAYERS 16
/* Server Setup's help for co-op's FRIENDLY FIRE and EXTRA ENEMIES, by
their choices, and EXTRA ENEMIES' PER PLAYER and MULTIPLIER (its
help_strings, tools/port_settings.py) */
#define COOPERATIVE_FRIENDLY_FIRE_HELP 12
#define COOPERATIVE_EXTRA_ENEMIES_HELP 16
#define COOPERATIVE_ENEMIES_PER_PLAYER_HELP 19
#define COOPERATIVE_ENEMIES_MULTIPLIER_HELP 20
#define COOPERATIVE_PLAYER_COLLISIONS_HELP 21
#define SERVER_PASSWORD_HELP 23
/* Server Setup's help for co-op's options' rows (CO-OP OPTIONS, VOICE AND
VOTING), and PASSWORD's (its help_strings, tools/port_settings.py) */
#define COOPERATIVE_OPTIONS_HELP 12
#define COOPERATIVE_VOICE_OPTIONS_HELP 13
#define SERVER_PASSWORD_HELP 14
/* co-op's EXTRA ENEMIES' choices (port_settings.COOP_ENEMIES_MODES, in this
order): its amount's row is the choice's */
/* co-op's EXTRA ENEMIES' choices (network.coop_enemies_mode's values, as
gametype_options' coop_extra_enemies_spinner has them, in this order): its
amount's row is the choice's */
enum
{
_cooperative_enemies_none,
@@ -2839,8 +2864,9 @@ enum
NUMBER_OF_COOPERATIVE_ENEMIES_MODES
};
/* co-op's FRIENDLY FIRE's choices (network.coop_friendly_fire's values,
port_settings.COOP_FRIENDLY_FIRE_VALUES, in this order) */
/* co-op's FRIENDLY FIRE's choices (network.coop_friendly_fire's values, as
gametype_options' coop_friendly_fire_spinner and server_start have them, in
this order) */
static short const cooperative_friendly_fire_modes[] =
{
_friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only
@@ -2954,54 +2980,17 @@ static void server_settings_update(struct widget_instance *list)
for (row = 0; row < NUMBEROF(server_settings_gametype_rows); row++)
visible_set(named(list, server_settings_gametype_rows[row], 0), !cooperative);
/* co-op's FRIENDLY FIRE, in their place: its help is its choice's */
visible_set(named(list, "op_friendly_fire", 0), cooperative);
if (cooperative && (spinner = named(list, "friendly_fire_spinner", 0)) != NULL)
{
short choice = (short)PIN(spinner->parameters.list.selected_index, 0,
NUMBEROF(cooperative_friendly_fire_modes) - 1);
struct widget_instance *help = list->parameters.list.extended_description;
multiplayer.cooperative_friendly_fire = cooperative_friendly_fire_modes[choice];
if (help && list->focused_child == named(list, "op_friendly_fire", 0))
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_FRIENDLY_FIRE_HELP + choice);
}
/* ... and its EXTRA ENEMIES, with the row of the amount of the one
chosen below it (network.coop_enemies_mode and its amounts, which the
host reads as the game begins: coop_enemies.c) */
/* co-op's options in their place, rows opening their screens
(tools/port_settings.py's COOP_SETUP_SCREENS), each its help */
{
struct widget_instance *help = list->parameters.list.extended_description;
short mode = _cooperative_enemies_none;
if ((spinner = named(list, "extra_enemies_spinner", 0)) != NULL)
{
mode = (short)PIN(spinner->parameters.list.selected_index, 0,
NUMBER_OF_COOPERATIVE_ENEMIES_MODES - 1);
}
visible_set(named(list, "op_extra_enemies", 0), cooperative);
visible_set(named(list, "op_enemies_per_player", 0), cooperative && mode == _cooperative_enemies_per_player);
visible_set(named(list, "op_enemies_multiplier", 0), cooperative && mode == _cooperative_enemies_multiplier);
if (cooperative && help)
{
if (list->focused_child == named(list, "op_extra_enemies", 0))
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_EXTRA_ENEMIES_HELP + mode);
else if (list->focused_child == named(list, "op_enemies_per_player", 0))
help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_PER_PLAYER_HELP;
else if (list->focused_child == named(list, "op_enemies_multiplier", 0))
help->parameters.text_box.string_list_index = COOPERATIVE_ENEMIES_MULTIPLIER_HELP;
}
}
/* ... and its PLAYER COLLISIONS, below them: ON or OFF, its help its
choice's */
visible_set(named(list, "op_player_collisions", 0), cooperative);
if (cooperative && (spinner = named(list, "player_collisions_spinner", 0)) != NULL)
{
short choice = (short)PIN(spinner->parameters.list.selected_index, 0, 1);
struct widget_instance *help = list->parameters.list.extended_description;
multiplayer.cooperative_no_player_collisions = choice == 1;
if (help && list->focused_child == named(list, "op_player_collisions", 0))
help->parameters.text_box.string_list_index = (short)(COOPERATIVE_PLAYER_COLLISIONS_HELP + choice);
visible_set(named(list, "op_coop_options", 0), cooperative);
visible_set(named(list, "op_voice_options", 0), cooperative);
if (help && list->focused_child == named(list, "op_coop_options", 0))
help->parameters.text_box.string_list_index = COOPERATIVE_OPTIONS_HELP;
else if (help && list->focused_child == named(list, "op_voice_options", 0))
help->parameters.text_box.string_list_index = COOPERATIVE_VOICE_OPTIONS_HELP;
}
}
/* LISTING (an internet game's): PUBLIC, listed in everyone's server
@@ -3096,7 +3085,21 @@ static boolean server_start(void)
and its FRIENDLY FIRE) */
if (hosting_cooperative())
{
/* (as CO-OP OPTIONS left them: network.coop_friendly_fire, in
cooperative_friendly_fire_modes' order, and
network.coop_player_collisions) */
static char const *const friendly_fire_names[] = { "off", "on", "shields_only", "explosives_only" };
char const *friendly_fire = config_string("network.coop_friendly_fire");
short index;
gametype_setup_end();
multiplayer.cooperative_friendly_fire = _friendly_fire_on;
for (index = 0; friendly_fire && index < (short)NUMBEROF(friendly_fire_names); index++)
{
if (!strcmp(friendly_fire, friendly_fire_names[index]))
multiplayer.cooperative_friendly_fire = cooperative_friendly_fire_modes[index];
}
multiplayer.cooperative_no_player_collisions = !config_boolean("network.coop_player_collisions");
network_game_server_port_set_cooperative_friendly_fire(multiplayer.cooperative_friendly_fire);
network_game_server_port_set_cooperative_player_collisions(!multiplayer.cooperative_no_player_collisions);
}
@@ -3302,8 +3305,8 @@ scenario's path or name */
static void map_display_name(char const *map_name, wchar_t *text)
{
char const *const *names;
short last, index;
short count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, &last));
short index;
short count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, NULL));
/* (a Custom Edition map's, if this machine has it: custom_edition_maps.c) */
if (custom_edition_level_name(map_name))
@@ -3493,12 +3496,12 @@ static void lobby_browser_update(struct widget_instance *list)
lobby_browser.count = lobby_browser_valid_games(lobby_browser.games,
(short)p2p_lobby_games(lobby_browser.games, LOBBY_BROWSER_GAMES));
if (focused == BROWSER_ROWS - 1 && lobby_browser.first + BROWSER_ROWS < lobby_browser.count)
if (focused == BROWSER_ROWS - 1 && lobby_browser.first + BROWSER_ROWS < lobby_browser.count && list_scrolls_at_end())
{
lobby_browser.first++;
lobby_browser_focus_row(list, --focused);
}
else if (focused == 0 && lobby_browser.first > 0)
else if (focused == 0 && lobby_browser.first > 0 && list_scrolls_at_end())
{
lobby_browser.first--;
lobby_browser_focus_row(list, ++focused);
@@ -4009,6 +4012,27 @@ static boolean profile_save_changes(struct widget_instance *widget, boolean *wid
return TRUE;
}
/* "port profile settings save" (Gamepads' OK in a single-player campaign:
menu_tags.c's pause_settings_patch): the profile saved at once, not on
Settings' OK, so that the campaign's next save of the player's profile
keeps it; saving makes it the player's own (player_ui_save_profile), and it
is edited again from what was saved, for Settings to go on with */
static boolean profile_settings_save(struct widget_instance *widget)
{
long index = player_ui_get_edit_profile_index();
settings_each(screen_of(widget), setting_changed_save);
if (!player_ui_get_edit_player_profile() || !player_ui_edit_profile_is_dirty())
return TRUE;
if (!player_ui_save_profile())
{
platform_log("menus: could not save the profile's changes");
return campaign_fail();
}
player_ui_begin_editing_profile(index);
return TRUE;
}
/* "port pause end game" (the in-game pause menu's END GAME, the host's:
menu_tags.c's pause_patch): the game ends as its time limit would, its
players staying for the next (the carnage report, then the host's PICK GAME) */
@@ -4251,7 +4275,7 @@ static void lobby_row_text(short row, wchar_t *text)
static void lobby_map_show(struct widget_instance *description, char const *map_name)
{
char const *const *names;
short last, count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, &last)), map = 19, index;
short count = xbox_multiplayer_map_count(ui_widget_port_multiplayer_maps(&names, NULL)), map = 19, index;
short level = campaign_level_of(map_name);
struct widget_instance *widget;
@@ -4282,6 +4306,59 @@ static void lobby_map_show(struct widget_instance *description, char const *map_
widget->parameters.text_box.string_list_index = level;
}
/* the lobby's rows' texts as last updated, and their players' machines,
for their speaker icons (menu_functions_text_box_drawn); and when */
static struct
{
struct widget_instance *texts[LOBBY_ROWS];
long machines[LOBBY_ROWS];
short count;
unsigned long updated_at;
} lobby_icons;
/* ui_widget.c's: a text box drawn, at the bounds its text was drawn in
(its font and justification still set): a lobby row's player talking in
voice chat (or muted) has a speaker just left of their name
(port/linux/game/network_voice.c) */
void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d const *bounds)
{
short row;
/* (as the lobby last updated them: the rows' widgets go with it) */
if (!lobby_icons.count || system_milliseconds() - lobby_icons.updated_at > 250)
return;
for (row = 0; row < lobby_icons.count; row++)
{
long machine_index = lobby_icons.machines[row];
rectangle2d icon;
short size;
if (lobby_icons.texts[row] != widget)
continue;
if (!network_voice_machine_speaking(machine_index) && !network_voice_machine_muted(machine_index))
return;
size = (short)MIN(14, bounds->y1 - bounds->y0);
icon.x1 = (short)(bounds->x1 - 4);
icon.y0 = (short)((bounds->y0 + bounds->y1 - size) / 2);
/* (the name's ink: the icon just left of it, centred on its
capitals) */
if (widget->parameters.text_box.text && widget->parameters.text_box.text[0])
{
rectangle2d text;
rectangle2d cursor;
draw_unicode_string_compute_bounds(bounds, widget->parameters.text_box.text, &text, &cursor);
if (text.x0 - size - 6 >= bounds->x0)
icon.x1 = (short)(text.x0 - 6);
icon.y0 = (short)(draw_unicode_string_capital_middle(bounds, widget->parameters.text_box.text) - size / 2);
}
icon.x0 = (short)(icon.x1 - size);
icon.y1 = (short)(icon.y0 + size);
network_voice_draw_icon(&icon, network_voice_machine_muted(machine_index), 1.0f);
return;
}
}
/* "port lobby update" */
static void lobby_update(struct widget_instance *list)
{
@@ -4302,6 +4379,21 @@ static void lobby_update(struct widget_instance *list)
if (multiplayer.lobby_first > MAX(0, lobby_player_count - LOBBY_ROWS))
multiplayer.lobby_first = (short)MAX(0, lobby_player_count - LOBBY_ROWS);
rows_update(list, (short)MIN(lobby_player_count, LOBBY_ROWS), lobby_row_text);
/* (the rows' players, for their speaker icons) */
{
struct widget_instance *row = list->child;
lobby_icons.count = 0;
for (index = 0; row && index < MIN(lobby_player_count, LOBBY_ROWS); row = row->next, index++)
{
if (strncmp(row->name, "list_item_", 10))
break;
lobby_icons.texts[index] = named(row, "list_item_text", 0);
lobby_icons.machines[index] = lobby_players[multiplayer.lobby_first + index]->machine_index;
lobby_icons.count = (short)(index + 1);
}
lobby_icons.updated_at = system_milliseconds();
}
lobby_join_help(list);
visible_set(named(list, "lobby_button_team", 0), game && game->variant.universal_variant.teams);
/* (the buttons' focus, off Switch Team when it is hidden) */
@@ -4536,7 +4628,8 @@ enum
_option_health, /* the variant's health, tenths */
_option_short, /* a short of the options */
_option_option_byte, /* a byte of the options */
_option_radar /* the options' radar players, and the variant's flag */
_option_radar, /* the options' radar players, and the variant's flag */
_option_setting /* the host's own (config.toml): Server Setup's copy only */
};
struct gametype_option
@@ -4547,6 +4640,9 @@ struct gametype_option
unsigned long argument;
short count;
long values[16];
/* (_option_setting: the setting, and its values' text) */
char const *setting;
char const *setting_values[16];
};
#define VARIANT_FIELD(field) (short)offsetof(struct game_variant, field)
@@ -4638,11 +4734,52 @@ static struct gametype_option const gametype_options[] =
{ _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only } },
{ "friendly_fire_penalty_spinner", _option_short, OPTIONS_FIELD(friendly_fire_penalty), 0, 4, { 0, 5, 10, 15 } },
{ "autobalance_spinner", _option_option_byte, OPTIONS_FIELD(auto_team_balance), 0, 2, { 0, 1 } },
/* (the host's voice chat, below them: port/linux/game/network_voice.c;
tools/port_settings.py's TEAMPLAY_ROWS) */
{ "voice_mode_spinner", _option_setting, 0, 0, 5, { 0 }, "network.voice_mode",
{ "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity" } },
{ "voice_lobby_spinner", _option_setting, 0, 0, 2, { 0 }, "network.voice_lobby", { "true", "false" } },
{ "voice_kbps_spinner", _option_setting, 0, 0, 7, { 0 }, "network.voice_kbps",
{ "8", "12", "16", "24", "32", "48", "64" } },
{ "voice_proximity_spinner", _option_setting, 0, 0, 6, { 0 }, "network.voice_proximity",
{ "5", "10", "15", "20", "30", "50" } },
/* (and the players' votes to kick: port/linux/game/network_votekick.c) */
{ "votekick_spinner", _option_setting, 0, 0, 2, { 0 }, "network.votekick", { "true", "false" } },
/* co-op's options (Server Setup's CO-OP OPTIONS: tools/port_settings.py's
COOP_SETUP_SCREENS; coop_enemies.c) */
{ "coop_friendly_fire_spinner", _option_setting, 0, 0, 4, { 0 }, "network.coop_friendly_fire",
{ "off", "on", "shields_only", "explosives_only" } },
{ "coop_extra_enemies_spinner", _option_setting, 0, 0, 3, { 0 }, "network.coop_enemies_mode",
{ "none", "per_player", "multiplier" } },
{ "coop_enemies_per_player_spinner", _option_setting, 0, 0, 5, { 0 }, "network.coop_enemies",
{ "25", "50", "100", "150", "200" } },
{ "coop_enemies_multiplier_spinner", _option_setting, 0, 0, 5, { 0 }, "network.coop_enemies_multiplier",
{ "2", "4", "8", "16", "32" } },
{ "coop_player_collisions_spinner", _option_setting, 0, 0, 2, { 0 }, "network.coop_player_collisions",
{ "true", "false" } },
/* vehicle options (the side's set and counts: vehicles_update) */
{ "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7,
{ 0, 30, 60, 90, 120, 180, 300 } },
};
/* an option of the host's own setting: the value shown for it (the one
set, else the nearest) */
static short gametype_option_setting_index(struct gametype_option const *option)
{
struct pc_menu_setting setting;
char text[64];
short index;
csmemset(&setting, 0, sizeof(setting));
setting.setting = option->setting;
setting.value_count = option->count;
for (index = 0; index < option->count; index++)
setting.values[index] = option->setting_values[index];
if (!setting_text(option->setting, text, sizeof(text), FALSE))
return 0;
return setting_value_index(&setting, text);
}
/* (an instance's name is its definition's cut to 31 characters:
"item_options_infinite_grenades_") */
#define WIDGET_NAME_LENGTH 31
@@ -4748,6 +4885,25 @@ static void gametype_options_each(struct widget_instance *list, boolean save)
if (!option)
continue;
/* (the host's own settings: Server Setup's alone, their rows
hidden in Edit Gametypes', which edits a gametype for any game;
written on OK, as the gametype's are kept) */
if (option->kind == _option_setting)
{
short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1);
visible_set(row, gametype_edit.setup);
if (!gametype_edit.setup)
continue;
if (!save)
spinner->parameters.list.selected_index = gametype_option_setting_index(option);
else if (index != gametype_option_setting_index(option) &&
!setting_write(option->setting, option->setting_values[index]))
{
platform_log("menus: could not set %s", option->setting);
}
continue;
}
if (save)
{
short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1);
@@ -4761,6 +4917,36 @@ static void gametype_options_each(struct widget_instance *list, boolean save)
}
}
/* each of a screen's rows of the host's own settings (_option_setting:
co-op's options' screens, tools/port_settings.py's _setup_option_screen),
shown, or (save) set */
static void setting_options_each(struct widget_instance *list, boolean save)
{
struct widget_instance *row;
for (row = list->child; row; row = row->next)
{
struct widget_instance *spinner;
for (spinner = row->child; spinner; spinner = spinner->next)
{
struct gametype_option const *option = spinner->type == 2 ? gametype_option_named(spinner->name) : NULL;
short index;
if (!option || option->kind != _option_setting)
continue;
index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1);
if (!save)
spinner->parameters.list.selected_index = gametype_option_setting_index(option);
else if (index != gametype_option_setting_index(option) &&
!setting_write(option->setting, option->setting_values[index]))
{
platform_log("menus: could not set %s", option->setting);
}
}
}
}
/* the options' list of a button of its bar (OK's): the one above it whose
rows are its options */
static struct widget_instance *gametype_options_list(struct widget_instance *widget)
@@ -4999,7 +5185,7 @@ static void gametype_edit_list_update(struct widget_instance *list)
gametype_edit_read();
focused = list_scroll(list, &gametype_edit.first, gametype_edit.count, GAMETYPE_EDIT_ROWS);
if (focused != NONE)
gametype_edit.chosen = (short)MIN(focused, gametype_edit.count - 1);
gametype_edit.chosen = (short)MAX(0, MIN(focused, gametype_edit.count - 1));
rows_update(list, (short)MIN(gametype_edit.count, GAMETYPE_EDIT_ROWS), gametype_edit_row_text);
visible_set(named(description, "gametype_right_item", 0), gametype_edit.count > 0);
if (gametype_edit.chosen < gametype_edit.count)
@@ -5093,6 +5279,15 @@ static void gametype_option_help(struct widget_instance *list)
visible_set(named(list, "op_primary_weapon", 0), custom);
visible_set(named(list, "op_secondary_weapon", 0), custom);
}
/* (CO-OP OPTIONS: the row of the amount of the extra enemies chosen,
PER PLAYER's or MULTIPLIER's, in the one place) */
if (named(list, "coop_extra_enemies_spinner", 0))
{
short mode = named(list, "coop_extra_enemies_spinner", 0)->parameters.list.selected_index;
visible_set(named(list, "op_coop_enemies_per_player", 0), mode == _cooperative_enemies_per_player);
visible_set(named(list, "op_coop_enemies_multiplier", 0), mode == _cooperative_enemies_multiplier);
}
/* (the server browser's filters, hidden: their helps are fewer than
their values) */
if (!description || !list->focused_child || !strncmp(list->name, "filters", 7))
@@ -5261,6 +5456,18 @@ boolean pc_menu_event_function_invoke(
{
return gametype_setup_edit();
}
/* (co-op's options' screens: their settings shown, and OK's) */
else if (!strcmp(name, "port setup options init"))
{
setting_options_each(widget, FALSE);
}
else if (!strcmp(name, "port setup options save"))
{
struct widget_instance *list = gametype_options_list(widget);
if (list)
setting_options_each(list, TRUE);
}
else if (!strcmp(name, "mp profile save changes"))
{
return ui_widget_port_gametype_save(widget, widget_deleted);
@@ -5311,6 +5518,10 @@ boolean pc_menu_event_function_invoke(
{
return profile_save_changes(widget, widget_deleted);
}
else if (!strcmp(name, "port profile settings save"))
{
return profile_settings_save(widget);
}
else if (!strcmp(name, "direct ip connect go"))
{
return direct_link_from_clipboard();
+295 -17
View File
@@ -28,10 +28,14 @@ A multiplayer map gets them too, for its in-game pause menu's SETTINGS
(pause_patch): there the map's tags they name that it has not (ui.map's
pictures, sounds and screens) are none, a missing picture drawn blank, and
no screen pauses the game (a network game's clock runs on).
So does a single-player campaign's map (pause_campaign_patch), whose screens
pause it as its pause menu does.
*/
#include "cseries.h"
#include "bitmaps/bitmap_group.h"
#include "game/game.h"
#include "tag_files/tag_groups.h"
#include "text/text_group.h"
#include "rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.h"
@@ -47,9 +51,12 @@ void platform_log(char const *format, ...);
char const *config_string(char const *name);
int platform_display_resolutions(long *widths, long *heights, int maximum);
int platform_window_sizes(long *widths, long *heights, int maximum);
/* (sdl_platform.c's PLATFORM_AUDIO_DEVICE_NAME_SIZE) */
int platform_audio_devices(int recording, char (*names)[128], int maximum);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
boolean network_game_is_active(void);
/* cache_files.c's (port) */
void *cache_files_tag_instances(long *count);
@@ -73,6 +80,8 @@ boolean pc_menu_frame_placement(struct bitmap_data const *bitmap, short *x, shor
/* the multiplayer maps' widget collection, whose pause screens pause_patch
adds to */
#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
/* the campaign's full screen pause screen, which pause_campaign_patch adds to */
#define CAMPAIGN_PAUSE_SCREEN "ui\\shell\\solo_game\\pause_game\\pause_game"
/* the tags pause_patch adds: two buttons and their texts */
#define PAUSE_PATCH_TAGS 4
/* what a frame of ui.map's is drawn as in a game map, which has not got it */
@@ -353,6 +362,7 @@ static char const *const port_function_names[] =
"mp level select", "mp profiles list initialize", "mp profiles list dispose", "mp profile set for game",
"port lobby preview join",
"port setup edit",
"port setup options init", "port setup options save",
"port map list back",
"port pause end game",
"port coop begin", "port coop player 2 list initialize", "port coop player 2",
@@ -373,6 +383,8 @@ static char const *const port_function_names[] =
"player profile save changes",
/* (the server browser's password screen) */
"port password init", "port password edit", "port password join", "port password back",
/* (Gamepads' OK in a single-player campaign: pause_settings_patch) */
"port profile settings save",
};
/* the PC version's game data functions that the Xbox's have not, from
@@ -428,9 +440,11 @@ static struct
long *text_tags;
long *spinner_tags;
boolean failed;
/* a multiplayer map's build (the pause menu's SETTINGS): what the map
has not got is none, not a failure */
/* a game map's build (the pause menu's SETTINGS): what the map has not
got is none, not a failure */
boolean game_map;
/* a single-player campaign's map's, whose screens pause the game */
boolean campaign;
} build;
/* ---------- private code */
@@ -735,13 +749,50 @@ static long split(char const *text, char const **pieces)
/* a spinner's strings, or its values, split: its own, but Video Setup's
sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set
"1920x1080": its resolutions after Resolution's own (NATIVE), and the
window sizes that fit it in place of Window Size's own */
window sizes that fit it in place of Window Size's own; and Audio Setup's
devices SDL finds, after their SYSTEM DEFAULT, each set by its name and
shown by it (in capitals, as the menus' words are, and cut short) */
static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces)
{
long count = split(values ? widget->values : widget->strings, pieces);
long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS];
long added, index;
if (widget->setting && (!strcmp(widget->setting, "audio.output_device") ||
!strcmp(widget->setting, "audio.input_device")))
{
static char names[MAXIMUM_STRINGS][128];
added = platform_audio_devices(!strcmp(widget->setting, "audio.input_device"), names,
(int)(MAXIMUM_STRINGS - count));
for (index = 0; index < added; index++)
{
char shown[32];
char const *name = names[index];
char *piece;
long length;
if (!values)
{
long at;
for (at = 0; name[at] && at < (long)sizeof(shown) - 1; at++)
shown[at] = name[at] >= 'a' && name[at] <= 'z' ? (char)(name[at] - 'a' + 'A') : name[at];
shown[at] = 0;
/* (cut short: what the spinner shows) */
if (strlen(name) > 18)
strcpy(shown + 15, "...");
name = shown;
}
length = (long)strlen(name);
piece = allocate(length + 1);
if (!piece)
break;
strcpy(piece, name);
pieces[count++] = piece;
}
return count;
}
if (widget->setting && !strcmp(widget->setting, "display.resolution"))
added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count));
else if (widget->setting && !strcmp(widget->setting, "display.window_size"))
@@ -862,7 +913,18 @@ static void *bitmap_build(struct halo_menu_bitmap const *source, long tag_index)
}
bitmap->width = (short)width;
bitmap->height = (short)height;
menu_tags.bitmaps = realloc(menu_tags.bitmaps, (menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps));
{
struct bitmap_data **bitmaps = realloc(menu_tags.bitmaps,
(menu_tags.bitmap_count + 1) * sizeof(*menu_tags.bitmaps));
if (!bitmaps)
{
rasterizer_bitmap_delete(bitmap);
problem(source->file, source->line, "out of memory for the bitmaps of", source->name);
return group;
}
menu_tags.bitmaps = bitmaps;
}
menu_tags.bitmaps[menu_tags.bitmap_count++] = bitmap;
/* (none without a renderer: debug.null_renderer) */
if (bitmap->hardware_format)
@@ -992,8 +1054,15 @@ static void handler_build(struct ui_widget_event_handler_reference *handler, str
static void setting_add(struct halo_menu_widget const *source, long definition_index)
{
struct pc_menu_setting *setting;
struct pc_menu_setting *settings = realloc(menu_tags.settings,
(menu_tags.setting_count + 1) * sizeof(*menu_tags.settings));
menu_tags.settings = realloc(menu_tags.settings, (menu_tags.setting_count + 1) * sizeof(*menu_tags.settings));
if (!settings)
{
problem(source->file, source->line, "out of memory for the settings of", source->name);
return;
}
menu_tags.settings = settings;
setting = &menu_tags.settings[menu_tags.setting_count++];
memset(setting, 0, sizeof(*setting));
setting->definition_index = definition_index;
@@ -1048,7 +1117,7 @@ static void *widget_build(long widget_index)
definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names),
source->file, source->line);
/* (in a network game the clock runs on) */
if (build.game_map)
if (build.game_map && !build.campaign)
definition->flags &= ~FLAG(1); /* pause_game */
definition->milliseconds_to_auto_close = source->auto_close;
definition->auto_close_fade_time = source->auto_close_fade;
@@ -1492,9 +1561,10 @@ static void *pause_button(struct cache_file_tag_instance *instances, struct ui_w
}
/* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE
GAME (quit); returns how many were added */
GAME (quit); returns how many were added, and how much taller the list
grew for them (growth) */
static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit,
boolean host)
boolean host, short *growth)
{
struct ui_widget_child_reference *children = list->child_widgets.address;
long count = list->child_widgets.count, added = host ? 2 : 1, child;
@@ -1536,8 +1606,25 @@ static long pause_list_patch(struct cache_file_tag_instance *instances, struct u
}
list->child_widgets.address = grown;
list->child_widgets.count = count + added;
/* (the list draws within its bounds) */
list->bounds.y1 = (short)(list->bounds.y1 + added * spacing);
/* (a list with room for them, Halo PC's of five rows that Custom Edition
maps keep two of: the buttons centred in it; else it grows, as the
Xbox's of two rows does, and draws within its bounds) */
{
short room = (short)(list->bounds.y1 - list->bounds.y0);
short column = (short)((count + added - 1) * spacing + model->bounds.y1 - model->bounds.y0);
*growth = 0;
if (column <= room)
{
for (child = 0; child < count + added; child++)
grown[child].vertical_offset = (short)((room - column) / 2 + child * spacing);
}
else
{
*growth = (short)(added * PAUSE_BUTTON_SPACING);
list->bounds.y1 = (short)(list->bounds.y1 + added * spacing);
}
}
return added;
}
@@ -1585,6 +1672,7 @@ static void pause_patch(struct cache_file_tag_instance *instances)
boolean host = global_network_game_server_get() != NULL;
struct tag_block const *screens;
long patched_list = NONE, added = 0, buttons = 0, screen;
short grow = 0;
boolean box_redrawn = FALSE;
if (collection == NONE || quit_function == NONE)
@@ -1596,7 +1684,7 @@ static void pause_patch(struct cache_file_tag_instance *instances)
struct ui_widget_definition *definition;
struct ui_widget_child_reference *children;
long child, list_child = NONE, box_child = NONE;
short grow, list_top;
short list_top;
if (screen_tag == NONE)
continue;
@@ -1621,7 +1709,7 @@ static void pause_patch(struct cache_file_tag_instance *instances)
quit = pause_quit_button(list, quit_function);
if (quit == NONE || patched_list != NONE)
continue;
added = pause_list_patch(instances, list, quit, host);
added = pause_list_patch(instances, list, quit, host, &grow);
if (!added)
return;
buttons = list->child_widgets.count;
@@ -1639,8 +1727,8 @@ static void pause_patch(struct cache_file_tag_instance *instances)
}
}
/* the stock box: taller, centred where it was, the list with it, and
what is below it moved down; else only what is below the list */
grow = (short)(added * PAUSE_BUTTON_SPACING);
what is below it moved down; else only what is below the list (as far
as the list grew) */
list_top = children[list_child].vertical_offset;
for (child = 0; child < definition->child_widgets.count; child++)
{
@@ -1659,6 +1747,191 @@ static void pause_patch(struct cache_file_tag_instance *instances)
platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : "");
}
/* ---------- the single-player campaign's pause menu
Its list gets the same SETTINGS (pause_list_patch) before REVERT TO SAVED, a
copy of it; a list with room centres its rows (Halo PC's), and one that has
none keeps its size, its rows closer, as its box beside the mission
objectives' does. Settings there has only the items that work in a game
(pause_settings_patch), and Gamepads' OK saves the profile itself. */
#define PAUSE_SETTINGS_LIST "main_menu/settings_select/player_setup/player_profile_edit/profile_edit_select_list"
#define PAUSE_SETTINGS_GAMEPADS_OK "main_menu/settings_select/player_setup/player_profile_edit/gamepad_setup/button_ok"
/* Change Name's keyboard and Change Color's pictures are ui.map's, Network
Setup's settings are not for a game under way, and About is nothing to it */
static char const *const pause_settings_hidden_items[] =
{
"main_menu/settings_select/player_setup/player_profile_edit/name_profile_item",
"main_menu/settings_select/player_setup/player_profile_edit/network_settings_profile_item",
"main_menu/settings_select/player_setup/player_profile_edit/color_profile_item",
"main_menu/settings_select/player_setup/player_profile_edit/about_item",
};
/* the list's rows spaced evenly over the span its rows had (a list that grew
for SETTINGS, back to its own size) */
static void pause_list_fit(struct ui_widget_definition *list, short span)
{
struct ui_widget_child_reference *children = list->child_widgets.address;
long count = list->child_widgets.count, child;
for (child = 1; child < count; child++)
children[child].vertical_offset = (short)(children[0].vertical_offset + child * span / (count - 1));
}
static boolean pause_settings_item_hidden(long tag_index)
{
short index;
for (index = 0; index < (short)NUMBEROF(pause_settings_hidden_items); index++)
{
long widget = widget_named(pause_settings_hidden_items[index]);
if (widget != NONE && build.widget_tags[widget] == tag_index)
return TRUE;
}
return FALSE;
}
/* the hidden items stay in the list, empty and taking no events (which it
passes over), as an item's description is the one at its place in the list;
the others move up into the first places */
static void pause_settings_patch(void)
{
long list_widget = widget_named(PAUSE_SETTINGS_LIST);
long ok_widget = widget_named(PAUSE_SETTINGS_GAMEPADS_OK);
long settings_save = function_index("port settings save", "pause", 0);
struct ui_widget_definition *list, *ok;
struct ui_widget_child_reference *children;
struct ui_widget_event_handler_reference *handlers;
short places[16];
long child, handler, place_count = 0, place = 0;
if (list_widget == NONE || ok_widget == NONE)
return;
list = tag_get(UI_WIDGET_DEFINITION_TAG, build.widget_tags[list_widget]);
children = list->child_widgets.address;
for (child = 0; child < list->child_widgets.count && place_count < (long)NUMBEROF(places); child++)
{
struct ui_widget_definition *item = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
if (item->type == _widget_type_text_box)
places[place_count++] = (short)(item->bounds.y0 + children[child].vertical_offset);
}
for (child = 0; child < list->child_widgets.count && place < place_count; child++)
{
struct ui_widget_definition *item = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
if (item->type != _widget_type_text_box)
continue;
if (pause_settings_item_hidden(children[child].widget_tag.index))
{
item->event_handlers.count = 0;
reference_clear(&item->background_bitmap, BITMAP_GROUP_TAG);
reference_clear(&item->text_label_string_list, UNICODE_STRING_LIST_TAG);
/* (nowhere for the mouse to focus it) */
item->bounds.y1 = item->bounds.y0;
item->bounds.x1 = item->bounds.x0;
}
else
{
children[child].vertical_offset = (short)(places[place++] - item->bounds.y0);
}
}
ok = tag_get(UI_WIDGET_DEFINITION_TAG, build.widget_tags[ok_widget]);
handlers = ok->event_handlers.address;
for (handler = 0; handler < ok->event_handlers.count; handler++)
{
if (handlers[handler].function == settings_save)
handlers[handler].function = (short)function_index("port profile settings save", "pause", 0);
}
}
static void pause_campaign_patch(struct cache_file_tag_instance *instances)
{
long screen = tag_loaded(UI_WIDGET_DEFINITION_TAG, CAMPAIGN_PAUSE_SCREEN);
struct ui_widget_definition *definition;
struct ui_widget_child_reference *children;
long child;
if (screen == NONE)
return;
definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen);
children = definition->child_widgets.address;
for (child = 0; child < definition->child_widgets.count; child++)
{
struct ui_widget_definition *list;
struct ui_widget_child_reference const *rows;
short span, grow, bottom;
long revert;
if (children[child].widget_tag.index == NONE)
continue;
list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
if (list->type != _widget_type_column_list || list->child_widgets.count < 2)
continue;
rows = list->child_widgets.address;
/* (REVERT TO SAVED, by its tag: it opens its confirmation rather
than running a function, and a map's list need not have it second) */
for (revert = 0; revert < list->child_widgets.count; revert++)
{
if (tag_name_ends(rows[revert].widget_tag.index, "\\restart_at_save_point_button"))
break;
}
if (revert == list->child_widgets.count)
return;
span = (short)(rows[list->child_widgets.count - 1].vertical_offset - rows[0].vertical_offset);
bottom = list->bounds.y1;
if (pause_list_patch(instances, list, revert, FALSE, &grow))
{
if (grow)
{
pause_list_fit(list, span);
list->bounds.y1 = bottom;
}
#ifdef HALO_ANDROID
/* port: in VR the rows closer within the span they had even when
the list has room: the pause menu's button hints (B Cancel, A
Select) sit just below its last row, which a list grown longer
and centred would put on their line */
else if (halo_vr_active())
{
pause_list_fit(list, span);
}
#endif
pause_settings_patch();
platform_log("menus: the pause menu has SETTINGS");
}
return;
}
}
/* the campaign played here by one player: not network co-op, nor split
screen */
static boolean single_player_campaign_map(void)
{
return tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE &&
tag_loaded(UI_WIDGET_DEFINITION_TAG, CAMPAIGN_PAUSE_SCREEN) != NONE &&
game_connection() == _game_connection_local && !network_game_is_active() && !game_is_cooperative();
}
/* whether display.menus asks for the PC version's menus: "pc" in any case
(a value neither "pc" nor "xbox" is said once in the log, and is the Xbox's) */
static boolean menus_pc_chosen(void)
{
static boolean said;
char const *value = config_string("display.menus");
if (!csstrcasecmp(value, "pc"))
return TRUE;
if (csstrcasecmp(value, "xbox") && !said)
{
platform_log("menus: display.menus \"%s\" is not \"xbox\" or \"pc\": the Xbox's", value);
said = TRUE;
}
return FALSE;
}
void menu_tags_loaded(
char const *map_name)
{
@@ -1667,10 +1940,12 @@ void menu_tags_loaded(
long widget_count, own_lists = 0, total, index;
boolean game_map = strcmp(map_name, "ui") != 0;
boolean campaign = game_map && single_player_campaign_map();
/* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */
if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) ||
strcmp(config_string("display.menus"), "pc"))
/* (ui.map, and a multiplayer map: its pause menu's SETTINGS; a
single-player campaign's too) */
if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE && !campaign) ||
!menus_pc_chosen())
{
return;
}
@@ -1683,6 +1958,7 @@ void menu_tags_loaded(
memset(&build, 0, sizeof(build));
build.menus = menus;
build.game_map = game_map;
build.campaign = campaign;
widget_count = menus->widget_count;
build.widget_tags = malloc((widget_count + 1) * sizeof(long));
build.text_tags = malloc((widget_count + 1) * sizeof(long));
@@ -1765,6 +2041,8 @@ void menu_tags_loaded(
if (game_map)
{
pause_patch(instances);
if (campaign)
pause_campaign_patch(instances);
if (build.failed)
goto failed;
/* (those it made) */
+22 -13
View File
@@ -1163,20 +1163,28 @@ static short distributed_grenade_throw(
return oldest;
}
/* whether an explosion of the damage is among the player's this tick
(distributed_report_paid): at the epicenter, or (NULL) anywhere */
/* whether an explosion of the type of grenade is among the player's this
tick (distributed_report_paid), of any damage the grenade deals (one
explosion deals more than one, each reported: a frag grenade's explosion
and its shock wave): at the epicenter, or (NULL) anywhere */
static boolean distributed_explosion_this_tick(
short player_index,
long damage_index,
short grenade_type,
real_point3d const *epicenter)
{
struct game_globals_grenade *grenade = TAG_BLOCK_GET_ELEMENT(&scenario_get_game_globals()->grenades,
grenade_type, struct game_globals_grenade);
short index;
if (damage_explosions[player_index].time != game_time_get())
if (damage_explosions[player_index].time != game_time_get() || grenade->projectile.index == NONE)
return FALSE;
for (index = 0; index < damage_explosions[player_index].count; index++)
{
if (damage_explosions[player_index].explosions[index].definition_index == damage_index &&
byte kinds;
struct damage_reach reach;
if (distributed_source_deals(grenade->projectile.index,
damage_explosions[player_index].explosions[index].definition_index, TRUE, &kinds, &reach) > 0.0f &&
(!epicenter || (damage_explosions[player_index].explosions[index].epicenter.x == epicenter->x &&
damage_explosions[player_index].explosions[index].epicenter.y == epicenter->y &&
damage_explosions[player_index].explosions[index].epicenter.z == epicenter->z)))
@@ -1189,10 +1197,11 @@ static boolean distributed_explosion_this_tick(
/* how many hits a second the player could deal of the damage: their
weapons, lately, a grenade the host saw them throw that has not gone off
(or one going off this tick, hitting more than one object), the vehicle
they drove lately; 0 when none of them deals it; how they deal it (the
_damage_source flags); how far from them what they fire deals it; and the
type of grenade when nothing but a grenade of theirs deals it, else NONE */
(or one going off this tick, hitting more than one object, dealing more
than one damage), the vehicle they drove lately; 0 when none of them deals
it; how they deal it (the _damage_source flags); how far from them what
they fire deals it; and the type of grenade when nothing but a grenade of
theirs deals it, else NONE */
static real distributed_player_deals(
short player_index,
long damage_index,
@@ -1255,7 +1264,7 @@ static real distributed_player_deals(
struct game_globals_grenade);
boolean thrown = distributed_grenade_throw(player_index, index) != NONE;
if ((thrown || distributed_explosion_this_tick(player_index, damage_index, NULL)) &&
if ((thrown || distributed_explosion_this_tick(player_index, index, NULL)) &&
grenade->projectile.index != NONE)
{
byte this_kinds;
@@ -1835,10 +1844,10 @@ static boolean distributed_report_valid(
return FALSE;
/* what only a grenade of theirs deals, from one the host saw them throw
that has not gone off: its explosion once (the explosion's other objects
that tick come free, distributed_report_paid), what it hits at a point
while it is about */
that tick come free, distributed_report_paid, and its other damage takes
no throw of its own), what it hits at a point while it is about */
if (grenade_type != NONE &&
!distributed_explosion_this_tick(player_index, report->damage.definition_index, &report->damage.epicenter))
!distributed_explosion_this_tick(player_index, grenade_type, &report->damage.epicenter))
{
throw_index = distributed_grenade_throw(player_index, grenade_type);
if (throw_index == NONE)
+132 -37
View File
@@ -72,6 +72,8 @@ machine (their datum identifiers need not be).
#include "units/bipeds.h"
#include "network_coop.h"
#include "network_distributed.h"
#include "network_voice.h"
#include "profile_sections.h"
#include <limits.h>
#include <stdio.h>
@@ -457,6 +459,28 @@ struct distributed_packer
static long distributed_last_sent_time = NONE;
/* the tick's steps, timed in the profiling build */
PROFILE_SECTION(distributed_tick_apply_predictions_section, "network_distributed_tick.apply_predictions")
PROFILE_SECTION(distributed_tick_apply_vehicle_predictions_section, "network_distributed_tick.apply_vehicle_predictions")
PROFILE_SECTION(distributed_tick_objects_host_tick_section, "network_distributed_tick.objects_host_tick")
PROFILE_SECTION(distributed_tick_plan_players_section, "network_distributed_tick.plan_players")
PROFILE_SECTION(distributed_tick_damage_host_tick_section, "network_distributed_tick.damage_host_tick")
PROFILE_SECTION(distributed_tick_send_statistics_section, "network_distributed_tick.send_statistics")
PROFILE_SECTION(distributed_tick_send_pings_section, "network_distributed_tick.send_pings")
PROFILE_SECTION(distributed_tick_send_structure_bsp_section, "network_distributed_tick.send_structure_bsp")
PROFILE_SECTION(distributed_tick_send_players_section, "network_distributed_tick.send_players")
PROFILE_SECTION(distributed_tick_actors_host_tick_section, "network_distributed_tick.actors_host_tick")
PROFILE_SECTION(distributed_tick_coop_host_tick_section, "network_distributed_tick.coop_host_tick")
PROFILE_SECTION(distributed_tick_send_pickups_section, "network_distributed_tick.send_pickups")
PROFILE_SECTION(distributed_tick_send_game_state_section, "network_distributed_tick.send_game_state")
PROFILE_SECTION(distributed_tick_note_own_positions_section, "network_distributed_tick.note_own_positions")
PROFILE_SECTION(distributed_tick_send_inputs_section, "network_distributed_tick.send_inputs")
PROFILE_SECTION(distributed_tick_send_predictions_section, "network_distributed_tick.send_predictions")
PROFILE_SECTION(distributed_tick_objects_client_tick_section, "network_distributed_tick.objects_client_tick")
PROFILE_SECTION(distributed_tick_damage_client_tick_section, "network_distributed_tick.damage_client_tick")
PROFILE_SECTION(distributed_tick_coop_client_tick_section, "network_distributed_tick.coop_client_tick")
PROFILE_SECTION(distributed_tick_batches_flush_section, "network_distributed_tick.batches_flush")
/* how each player last died, by absolute index: the host's own, which it
sends its clients, and a client's copy of the host's */
static struct distributed_death
@@ -2245,6 +2269,8 @@ static void distributed_handle_unit_state(
if (TEST_FLAG(state->flags, _distributed_unit_placed_bit) &&
object_get(unit_index)->object.parent_object_index == NONE)
{
real_point3d before = object_get(unit_index)->object.position;
if (local)
distributed_correct_own_unit(player, unit_index, state);
else
@@ -2252,6 +2278,16 @@ static void distributed_handle_unit_state(
distributed_apply_state(unit_index, state, &state->position, REMOTE_CORRECTION_TOLERANCE,
REMOTE_BLEND_DISTANCE, 0.0f, 0.0f);
}
/* moved further than a step at once (the host's teleporter sent
it): on the teleporter it lands on, as a teleporter leaves the
player it sends, so that this machine's does not send it back each
time the host's word puts it there until it steps off
(game_engine_update_teleporter) */
if (distance_squared3d(&before, &object_get(unit_index)->object.position) > 1.0f)
{
player->teleporter_index = find_netgame_flag(&object_get(unit_index)->object.position, 1.0f, 0.0f,
_netgame_flag_teleporter_source, NONE);
}
}
}
@@ -3301,6 +3337,7 @@ void network_distributed_new_game(
bots_new_game();
network_actors_new_game();
network_coop_new_game();
network_votekick_new_game();
}
/* after each tick (game_time.c) */
@@ -3333,43 +3370,45 @@ void network_distributed_tick(
decided first: the damage goes where the players it hurt do), and a
kill's statistics before the kill, so that a client announcing it
counts it (a double kill, a killing spree) */
distributed_apply_predictions();
network_objects_apply_vehicle_predictions();
network_objects_host_tick();
distributed_host_plan_players();
network_damage_host_tick();
profile_scope(distributed_tick_apply_predictions_section, distributed_apply_predictions();)
profile_scope(distributed_tick_apply_vehicle_predictions_section, network_objects_apply_vehicle_predictions();)
profile_scope(distributed_tick_objects_host_tick_section, network_objects_host_tick();)
profile_scope(distributed_tick_plan_players_section, distributed_host_plan_players();)
profile_scope(distributed_tick_damage_host_tick_section, network_damage_host_tick();)
if (distributed_statistics_due || game_time_get() % STATISTICS_INTERVAL_TICKS == 0)
distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0);
profile_scope(distributed_tick_send_statistics_section,
distributed_send_statistics(game_time_get() % STATISTICS_REFRESH_TICKS == 0);)
distributed_statistics_due = FALSE;
if (game_time_get() % PING_INTERVAL_TICKS == 0)
distributed_send_pings();
profile_scope(distributed_tick_send_pings_section, distributed_send_pings();)
/* (before the players, so clients load a new BSP before they hear
where the host moved everyone into it) */
if (network_coop_active() && (global_structure_bsp_index_get() != distributed_sent_structure_bsp_index ||
game_time_get() % STRUCTURE_BSP_INTERVAL_TICKS == 0))
{
distributed_send_structure_bsp();
profile_scope(distributed_tick_send_structure_bsp_section, distributed_send_structure_bsp();)
}
distributed_host_send_players();
profile_scope(distributed_tick_send_players_section, distributed_host_send_players();)
network_vr_tick();
bots_tick();
network_actors_host_tick();
network_coop_host_tick();
distributed_send_pickups();
profile_scope(distributed_tick_actors_host_tick_section, network_actors_host_tick();)
profile_scope(distributed_tick_coop_host_tick_section, network_coop_host_tick();)
profile_scope(distributed_tick_send_pickups_section, distributed_send_pickups();)
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
profile_scope(distributed_tick_send_game_state_section, distributed_send_game_state(NONE);)
network_votekick_host_tick();
}
else if (connection == _game_connection_network_client)
{
distributed_note_own_positions();
distributed_client_send_inputs();
profile_scope(distributed_tick_note_own_positions_section, distributed_note_own_positions();)
profile_scope(distributed_tick_send_inputs_section, distributed_client_send_inputs();)
network_vr_tick();
distributed_client_send_predictions();
network_objects_client_tick();
network_damage_client_tick();
network_coop_client_tick();
profile_scope(distributed_tick_send_predictions_section, distributed_client_send_predictions();)
profile_scope(distributed_tick_objects_client_tick_section, network_objects_client_tick();)
profile_scope(distributed_tick_damage_client_tick_section, network_damage_client_tick();)
profile_scope(distributed_tick_coop_client_tick_section, network_coop_client_tick();)
}
distributed_batches_flush();
profile_scope(distributed_tick_batches_flush_section, distributed_batches_flush();)
distributed_machines.in_tick = FALSE;
distributed_machines.valid = FALSE;
}
@@ -3409,6 +3448,7 @@ static boolean distributed_message_stale(
case _distributed_message_damage_animations:
case _distributed_message_coop_screen_effect:
case _distributed_message_coop_device_states:
case _distributed_message_votekick_status:
break;
default:
return FALSE;
@@ -3425,7 +3465,7 @@ static boolean distributed_message_stale(
/* (the host) a text shown in red on every machine's console: its own, and
every client's (_distributed_message_notice) */
static void distributed_send_notice(
void distributed_send_notice(
char const *text)
{
struct
@@ -3445,9 +3485,34 @@ static void distributed_send_notice(
_distributed_to_clients_reliably);
}
/* (the host) ... on one client's console only, or (NONE) the host's own */
void distributed_send_notice_to_machine(
long machine_index,
char const *text)
{
struct
{
struct distributed_message_header header;
char text[MAXIMUM_NOTICE_LENGTH];
} message;
long length = csstrlen(text);
if (length > MAXIMUM_NOTICE_LENGTH - 1)
length = MAXIMUM_NOTICE_LENGTH - 1;
csmemset(&message, 0, sizeof(message));
csmemcpy(message.text, text, length);
if (machine_index == NONE)
{
console_warning("%s", message.text);
return;
}
distributed_send_to_machine_reliably(machine_index, &message, _distributed_message_notice, 0,
(word)(sizeof(message.header) + length + 1));
}
/* (the host) the names of a client machine's players, in ASCII, for a
notice ("?" for what is not ASCII) */
static void distributed_machine_player_names(
void distributed_machine_player_names(
long machine_index,
char *names,
long size)
@@ -3506,6 +3571,31 @@ void distributed_client_send_identity(
_distributed_to_host_reliably);
}
/* (the host) a client machine's address (host byte order) as it really is:
an internet play peer's endpoint for its stand-in in 100.64.0.0/10 (p2p.c,
which numbers the stand-in from the peer's identifier: a peer makes a new
one at will), 0 if that is not known */
unsigned long distributed_real_address(
unsigned long address)
{
if ((address & 0xFFC00000) == 0x64400000)
{
/* (p2p.c's in network byte order: the first number the lowest byte) */
unsigned long network = (address >> 24) | ((address >> 8) & 0xFF00) | ((address << 8) & 0xFF0000) |
(address << 24);
unsigned long real = p2p_peer_endpoint_address(network);
return (real >> 24) | ((real >> 8) & 0xFF00) | ((real << 8) & 0xFF0000) | (real << 24);
}
return address;
}
unsigned long distributed_machine_real_address(
long machine_index)
{
return distributed_real_address(network_game_server_machine_address(machine_index));
}
/* (the host) a client machine's address as text: its real one, for an
internet play peer's stand-in (p2p.c) */
static void distributed_address_text(
@@ -3513,21 +3603,9 @@ static void distributed_address_text(
char *text,
long size)
{
/* 100.64.0.0/10: an internet play peer's, by its real address (network
byte order: its first number the lowest byte) */
if ((address & 0xFFC00000) == 0x64400000)
{
unsigned long network = (address >> 24) | ((address >> 8) & 0xFF00) | ((address << 8) & 0xFF0000) |
(address << 24);
unsigned long real = p2p_peer_endpoint_address(network);
if (real)
{
snprintf(text, size, "%lu.%lu.%lu.%lu", real & 255, (real >> 8) & 255, (real >> 16) & 255,
(real >> 24) & 255);
return;
}
}
/* 100.64.0.0/10: an internet play peer's, by its real address */
if ((address & 0xFFC00000) == 0x64400000 && distributed_real_address(address))
address = distributed_real_address(address);
if (address)
{
snprintf(text, size, "%lu.%lu.%lu.%lu", (address >> 24) & 255, (address >> 16) & 255, (address >> 8) & 255,
@@ -3869,6 +3947,12 @@ void network_distributed_handle_message(
short index;
word entry_size;
/* (voice chat's, in the lobby too: network_voice.c) */
if (network_voice_handles_message(message, size))
{
network_voice_handle_message(machine_index, message, size);
return;
}
/* (none between games: loading, or in the menus) */
if (size < sizeof(header) || !game_in_progress())
return;
@@ -3925,6 +4009,8 @@ void network_distributed_handle_message(
case _distributed_message_coop_screen_effect: entry_size = network_coop_screen_effect_entry_size(); break;
case _distributed_message_coop_device_states: entry_size = network_coop_device_state_entry_size(); break;
case _distributed_message_pickups: entry_size = sizeof(struct distributed_pickup); break;
case _distributed_message_votekick: entry_size = network_votekick_request_entry_size(); break;
case _distributed_message_votekick_status: entry_size = network_votekick_status_entry_size(); break;
case _distributed_message_player_inputs: entry_size = sizeof(struct distributed_player_input); break;
case _distributed_message_relayed_actions: entry_size = DISTRIBUTED_RELAYED_ACTION_MINIMUM_SIZE; break;
case _distributed_message_game_state:
@@ -3949,6 +4035,8 @@ void network_distributed_handle_message(
case _distributed_message_coop_presentation:
case _distributed_message_coop_object_names:
case _distributed_message_coop_skip_vote:
case _distributed_message_votekick:
case _distributed_message_votekick_status:
if (header.count < 1)
return;
break;
@@ -3979,6 +4067,7 @@ void network_distributed_handle_message(
case _distributed_message_vehicle_prediction:
case _distributed_message_player_inputs:
case _distributed_message_coop_skip_vote:
case _distributed_message_votekick:
if (machine_index == NONE || game_connection() != _game_connection_network_server)
return;
break;
@@ -4033,6 +4122,12 @@ void network_distributed_handle_message(
case _distributed_message_coop_skip_vote:
network_coop_handle_skip_vote(machine_index, entries);
break;
case _distributed_message_votekick:
network_votekick_handle_request(machine_index, entries, distributed_handling_stream_message);
break;
case _distributed_message_votekick_status:
network_votekick_handle_status(entries);
break;
case _distributed_message_coop_device_groups:
network_coop_handle_device_groups(entries, header.count);
break;
+37
View File
@@ -106,6 +106,18 @@ enum
/* co-op: where the host's devices are (doors, elevators), when they move
(network_coop.c) */
_distributed_message_coop_device_states = 77,
/* a player's vote to kick a player (network_votekick.c): to the host,
reliably, which takes it only from the voter's own stream */
_distributed_message_votekick = 78,
/* the vote running as the host counts it, to each client, every second
and when it changes (unreliable: network_votekick.c) */
_distributed_message_votekick_status = 79,
/* voice chat (network_voice.c), in the lobby too: a client's frame to
the host, unreliably, with its key; a frame the host relays to a
client; the host's settings and the client's key, reliably */
_distributed_message_voice_up = 80,
_distributed_message_voice_down = 81,
_distributed_message_voice_config = 82,
/* The VR port's own messages, numbered far past the game's (240 on),
so that messages upstream adds never take their numbers: a build
@@ -257,6 +269,31 @@ real distributed_angle_unpack(short value, boolean signed_angle);
word distributed_vitality_pack(real value);
real distributed_vitality_unpack(word value);
/* (the host) a text in red on every machine's console, or on one client's
(machine_index; NONE: the host's own) */
void distributed_send_notice(char const *text);
void distributed_send_notice_to_machine(long machine_index, char const *text);
/* (the host) the names of a client machine's players, in ASCII */
void distributed_machine_player_names(long machine_index, char *names, long size);
/* (the host) a client machine's real IPv4 address (host byte order): an
internet play peer's endpoint, not its stand-in; 0 if not known */
unsigned long distributed_machine_real_address(long machine_index);
/* ... and the real address of a machine's address, as it joined from */
unsigned long distributed_real_address(unsigned long address);
/* ---------- prototypes/NETWORK_VOTEKICK.C */
void network_votekick_new_game(void);
/* after each tick, on the host */
void network_votekick_host_tick(void);
/* (the host) a client's vote; from_stream: it came over the machine's own
stream (anything else is refused) */
void network_votekick_handle_request(long machine_index, void const *entries, boolean from_stream);
word network_votekick_request_entry_size(void);
/* (a client) the host's count */
void network_votekick_handle_status(void const *entries);
word network_votekick_status_entry_size(void);
/* ---------- prototypes/NETWORK_ACTORS.C */
void network_actors_new_game(void);
+52 -5
View File
@@ -64,6 +64,7 @@ same datum index (identifier and all), so that any message can name one:
#include "cutscene/cinematics.h"
#include "network_coop.h"
#include "network_distributed.h"
#include "profile_sections.h"
#include <math.h>
@@ -243,6 +244,10 @@ enum
/* (struct distributed_object_state, the host's) the host has the vehicle
where the client it goes to had it at the tick in time */
_distributed_object_predicted_bit,
/* (struct distributed_object_state) a unit's integrated light is on, and
the host sent it at all: older builds leave both 0, which is not "off" */
_distributed_object_light_sent_bit,
_distributed_object_light_on_bit,
};
struct distributed_object_change
@@ -672,6 +677,18 @@ static boolean distributed_vehicle_unsteered(
unit_get(vehicle->unit.driver_object_index)->unit.player_index == NONE;
}
/* whether this machine's player drives the unit: it is their biped, or the
vehicle they drive */
static boolean distributed_unit_driven_here(
long unit_index)
{
struct unit_datum *unit = unit_get(unit_index);
if (unit->unit.driver_object_index != NONE)
unit = unit_get(unit->unit.driver_object_index);
return unit->unit.player_index != NONE && distributed_player_is_local(unit->unit.player_index);
}
/* Whether a client puts its copy exactly where the host has it, rather than
closing on it: an unsteered vehicle, and anything in a co-op cutscene
(where it keeps up with the host's camera). */
@@ -807,6 +824,12 @@ static void distributed_state_from_object(
SET_FLAG(state->flags, _distributed_object_at_rest_bit, TEST_FLAG(object->object.flags, _object_at_rest_bit));
SET_FLAG(state->flags, _distributed_object_dead_bit, TEST_FLAG(_object_mask_unit, object->object.type) &&
TEST_FLAG(object->object.damage_flags, _object_dead_bit));
if (TEST_FLAG(_object_mask_unit, object->object.type))
{
SET_FLAG(state->flags, _distributed_object_light_sent_bit, TRUE);
SET_FLAG(state->flags, _distributed_object_light_on_bit,
TEST_FLAG(unit_get(object_index)->unit.flags, _unit_integrated_light_on_bit));
}
state->position = object->object.position;
distributed_vector_pack(&object->object.forward, DISTRIBUTED_UNIT_SCALE, &state->forward);
distributed_vector_pack(&object->object.up, DISTRIBUTED_UNIT_SCALE, &state->up);
@@ -1976,15 +1999,22 @@ void network_objects_apply_vehicle_predictions(
}
}
/* the host tick's steps, timed in the profiling build */
PROFILE_SECTION(objects_host_tick_update_objects_section, "network_objects_host_tick.update_objects")
PROFILE_SECTION(objects_host_tick_find_viewers_section, "network_objects_host_tick.find_viewers")
PROFILE_SECTION(objects_host_tick_send_states_section, "network_objects_host_tick.send_states")
PROFILE_SECTION(objects_host_tick_send_inventories_section, "network_objects_host_tick.send_inventories")
PROFILE_SECTION(objects_host_tick_send_damage_animations_section, "network_objects_host_tick.send_damage_animations")
void network_objects_host_tick(
void)
{
distributed_host_update_objects();
distributed_host_find_viewers();
distributed_host_send_states();
profile_scope(objects_host_tick_update_objects_section, distributed_host_update_objects();)
profile_scope(objects_host_tick_find_viewers_section, distributed_host_find_viewers();)
profile_scope(objects_host_tick_send_states_section, distributed_host_send_states();)
if (game_time_get() % INVENTORY_INTERVAL_TICKS == 0)
distributed_host_send_inventories();
distributed_host_send_damage_animations();
profile_scope(objects_host_tick_send_inventories_section, distributed_host_send_inventories();)
profile_scope(objects_host_tick_send_damage_animations_section, distributed_host_send_damage_animations();)
}
/* ---------- a client */
@@ -2316,6 +2346,12 @@ static void distributed_client_create(
return;
}
objects_client_has[absolute_index] = object_index;
/* (a unit's grenades are the host's, which its inventories bring: none
until they come, not the tag's that unit_new gave it, which the host's
game engine replaces with the game's at a player's spawn: a player
spawned with the button held threw one here the host's copy had not) */
if (TEST_FLAG(_object_mask_unit, object_get(object_index)->object.type))
csmemset(unit_get(object_index)->unit.grenade_counts, 0, sizeof(unit_get(object_index)->unit.grenade_counts));
distributed_client_apply_change(object_index, change);
}
@@ -2490,6 +2526,17 @@ void network_objects_handle_states(
if (!distributed_object_index_valid(state->object_index) || !network_objects_client_has(state->object_index))
continue;
object = object_get(state->object_index);
/* (the unit's light as the host has it: only presses toggle it here,
so one missed, or made before this machine joined, left it wrong for
good. Not for what this machine's player drives: their press lands
here first) */
if (TEST_FLAG(state->flags, _distributed_object_light_sent_bit) &&
TEST_FLAG(_object_mask_unit, object->object.type) &&
!distributed_unit_driven_here(state->object_index))
{
SET_FLAG(unit_get(state->object_index)->unit.flags, _unit_integrated_light_on_bit,
TEST_FLAG(state->flags, _distributed_object_light_on_bit));
}
if (TEST_FLAG(state->flags, _distributed_object_dead_bit))
{
distributed_client_dead_biped(state->object_index);
File diff suppressed because it is too large. Load diff
+59
View File
@@ -0,0 +1,59 @@
/*
NETWORK_VOICE.H
Voice chat (network_voice.c): what the main loop, the netcode, the host's
server, the lobby (menu_functions.c) and the scoreboard (game_engine.c) ask
of it.
*/
#ifndef __NETWORK_VOICE_H
#define __NETWORK_VOICE_H
#pragma once
/* ---------- constants */
/* network.voice_mode: who hears whom in a game */
enum
{
_voice_mode_off,
_voice_mode_team_proximity,
_voice_mode_team_enemy_proximity,
_voice_mode_team_global,
_voice_mode_team_global_enemy_proximity,
NUMBER_OF_VOICE_MODES
};
/* how a listener hears a speaker */
enum
{
_voice_route_none,
/* as spoken */
_voice_route_global,
/* quieter further, from the speaker's side */
_voice_route_proximity,
};
/* ---------- prototypes/NETWORK_VOICE.C */
/* every frame (main.c): the microphone, the host's settings told */
void network_voice_update(void);
/* whether a message of the distributed kind is voice chat's, which it takes
in the lobby too; and its handling (machine_index: the sender's on the
host, NONE on a client) */
boolean network_voice_handles_message(word const *message, word size);
void network_voice_handle_message(long machine_index, word const *message, word size);
/* (the host's server) a machine joined at the slot: its new key */
void network_voice_machine_joined(long machine_index);
/* whether this machine has voice chat now (the host's settings heard) */
boolean network_voice_available(void);
/* whether a machine (its index in the network game) is talking now (this
machine's own, as it sends); muted ones never */
boolean network_voice_machine_speaking(long machine_index);
/* this machine's mutes of others (which it tells no one) */
boolean network_voice_machine_muted(long machine_index);
void network_voice_mute_machine(long machine_index, boolean mute);
/* a speaker icon, at the rectangle's left and as tall as it (muted: struck
through), fading with alpha */
void network_voice_draw_icon(rectangle2d const *bounds, boolean muted, real alpha);
#endif // __NETWORK_VOICE_H
File diff suppressed because it is too large. Load diff
+63
View File
@@ -0,0 +1,63 @@
/*
NETWORK_VOTEKICK.H
The players' vote to kick a player (network_votekick.c): what the
scoreboard (game_engine.c), the console (hs.c) and the host's server
(network_server_manager.c) ask of it.
*/
#ifndef __NETWORK_VOTEKICK_H
#define __NETWORK_VOTEKICK_H
#pragma once
/* ---------- constants */
enum
{
/* a player's name in ASCII, with its end */
VOTEKICK_NAME_TEXT_SIZE = 16,
};
/* ---------- structures */
/* the vote running, as the host counts it */
struct network_votekick_status
{
/* the player voted against (absolute index) */
short player_index;
short votes;
short needed;
short seconds_left;
/* this machine has voted; may vote */
boolean voted;
boolean may_vote;
};
/* ---------- prototypes/NETWORK_VOTEKICK.C */
/* whether this machine is in a network game, where votes are */
boolean network_votekick_available(void);
/* the vote running; FALSE for none */
boolean network_votekick_get_status(struct network_votekick_status *status);
/* this machine votes against the player (absolute index): starts a vote,
or votes in the one running; the host answers on the console */
boolean network_votekick_request(short player_index);
/* whether this machine hosts the game (its scoreboard offers Kick and Ban) */
boolean network_votekick_host(void);
/* (the host) kicks, or bans, the machine of the player (absolute index):
its kick and ban commands' (network_server_manager.c) */
boolean network_votekick_host_kick(short player_index, boolean ban);
/* the console's "votekick <name>": the player of that name (or the one
whose name begins with it) voted against */
boolean network_votekick_player_named(char const *text);
/* the names of the game's players but this machine's that begin with the
text (the command's completion: console.c); how many */
short network_votekick_matching_player_names(char const *text, char (*names)[VOTEKICK_NAME_TEXT_SIZE],
short maximum_count);
/* (the host's server) a machine joined at the slot */
void network_votekick_machine_joined(long machine_index);
/* (the host's server) whether a machine of the address (host byte order)
and hardware id is kept out, kicked by a vote */
boolean network_votekick_kept_out(unsigned long address, char const *hardware_id);
#endif // __NETWORK_VOTEKICK_H
+304
View File
@@ -0,0 +1,304 @@
/*
PROFILE_CONSOLE.C
Connect profiling commands and launch settings to game recordings.
*/
#ifdef HALO_PROFILE
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "main/console.h"
#include "networking/network_game_globals.h"
#include "network_distributed.h"
#include "profile_console.h"
#include "profile_console_gametype.h"
#include "profile_trace.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* the platform layer's (platform.h, port_config.h), and main.c's */
void platform_log(const char *format, ...);
const char *platform_data_root(void);
int posix_make_directory(const char *path);
void *posix_directory_open(const char *path);
void posix_directory_close(void *directory);
void p2p_profile_lock(void);
void p2p_profile_unlock(void);
int config_boolean(const char *name);
long config_integer(const char *name);
const char *config_string(const char *name);
boolean main_menu_is_loaded(void);
/* ---------- globals */
static boolean profile_console_installed;
static void profile_console_map_component(const char *map, char *name, int size)
{
const char *component = map;
const char *cursor;
int used = 0;
for (cursor = map; *cursor; cursor++)
if (*cursor == '/' || *cursor == '\\')
component = cursor + 1;
for (cursor = component; *cursor && used + 1 < size; cursor++)
{
unsigned char character = (unsigned char)*cursor;
if (character >= 'A' && character <= 'Z')
character = (unsigned char)(character + ('a' - 'A'));
name[used++] = (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') ||
character == '_' || character == '-' ? (char)character : '-';
}
name[used] = 0;
}
static const char *profile_console_gametype(void)
{
long engine;
if (!game_in_progress() || main_menu_is_loaded() || game_connection() == _game_connection_film_playback)
return "none";
if (!game_engine_running())
return profile_console_engine_gametype(FALSE, 0);
engine = game_engine_get_variant()->game_engine_index;
return profile_console_engine_gametype(TRUE, engine);
}
/* the map the header names: the one loading takes its place only after the
frame boundary that stops a recording on it (profile_console_frame) */
static char profile_console_map[64];
static char profile_console_next_map[64];
/* ---------- private code */
static void profile_console_log(
const char *text)
{
platform_log("profile: %s", text);
}
static void profile_console_notify(
const char *text)
{
console_printf(FALSE, "%s", text);
}
static int profile_console_ready(
void)
{
return game_in_progress() && !main_menu_is_loaded();
}
static void profile_console_session(
struct profile_trace_session *session)
{
short player_index;
const char *scenario = profile_console_scenario_map(profile_console_next_map, profile_console_map);
void *folder;
csmemset(session, 0, sizeof(*session));
snprintf(session->folder, sizeof(session->folder), "%s/profiles", platform_data_root());
posix_make_directory(session->folder);
/* (a folder that can't be made must not lose the recording: the parts go
to the data root itself then) */
folder = posix_directory_open(session->folder);
if (folder)
posix_directory_close(folder);
else
strncpy(session->folder, platform_data_root(), sizeof(session->folder) - 1);
#ifdef HALO_RELEASE
strcpy(session->build, "release profile");
#else
strcpy(session->build, "debug profile");
#endif
session->own_machine = -1;
switch (game_connection())
{
case _game_connection_network_server:
strcpy(session->role, "host");
break;
case _game_connection_network_client:
strcpy(session->role, "client");
session->own_machine = network_game_client_get_local_machine_index();
break;
default:
strcpy(session->role, "local");
break;
}
strncpy(session->map, scenario, sizeof(session->map) - 1);
profile_console_map_component(scenario, session->map_name, sizeof(session->map_name));
strncpy(session->gametype, profile_console_gametype(), sizeof(session->gametype) - 1);
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
{
if (distributed_player(player_index))
session->players++;
}
session->players_most = session->players;
}
/* (once a second on the game thread: unlike the start-time description it
makes and opens no folder, which would land in the measured frame) */
static void profile_console_sample_session(
struct profile_trace_session *session)
{
short player_index;
const char *scenario = profile_console_scenario_map(profile_console_next_map, profile_console_map);
if (!session->map_name[0] && scenario[0])
{
strncpy(session->map, scenario, sizeof(session->map) - 1);
profile_console_map_component(scenario, session->map_name, sizeof(session->map_name));
strncpy(session->gametype, profile_console_gametype(), sizeof(session->gametype) - 1);
}
for (player_index = 0; player_index < MAXIMUM_TRACKED_PLAYERS; player_index++)
if (distributed_player(player_index))
session->players++;
}
/* the recording's seams. Not left to the first frame: init.txt's
profile_record is handled before it, and the recording's name (folder and
role) is chosen from the session when it is requested */
static void profile_console_install(
void)
{
if (profile_console_installed)
return;
profile_console_installed = TRUE;
profile_trace_set_log(profile_console_log);
profile_trace_set_notify(profile_console_notify);
profile_trace_set_ready(profile_console_ready);
profile_trace_set_session(profile_console_session);
profile_trace_set_session_sampler(profile_console_sample_session);
profile_trace_set_track_lock(p2p_profile_lock, p2p_profile_unlock);
/* (the window's close, debug.exit_after and XLaunchNewImage end the
process with exit(), which main_exit never sees) */
atexit(profile_trace_shutdown);
}
/* what follows the command word an expression starts with (past spaces and
an opening parenthesis, as the console and init.txt give it), else NULL */
static char const *profile_console_word(
char const *expression,
char const *word)
{
long length = (long)strlen(word);
long index;
while (*expression == ' ' || *expression == '\t' || *expression == '(')
expression++;
for (index = 0; index < length; index++)
{
char character = expression[index] >= 'A' && expression[index] <= 'Z' ? expression[index] - 'A' + 'a' : expression[index];
if (character != word[index])
return NULL;
}
expression += length;
if (*expression && *expression != ' ' && *expression != '\t' && *expression != ')')
return NULL;
while (*expression == ' ' || *expression == '\t')
expression++;
return expression;
}
/* ---------- public code */
void profile_console_launch(
void)
{
char const *when = config_string("debug.profile_record_when");
strcpy(profile_console_map, profile_console_next_map);
profile_console_install();
profile_trace_thread_register(_profile_track_game);
if (strcmp(when, "start") != 0 && strcmp(when, "game") != 0)
platform_log("profile: debug.profile_record_when is \"%s\": \"start\" taken", when);
if (config_boolean("debug.profile_record"))
{
profile_trace_request_start(0.0, strcmp(when, "game") == 0 ? _profile_trace_when_game : _profile_trace_when_now,
config_integer("debug.profile_memory"));
}
}
void profile_console_frame(
void)
{
strcpy(profile_console_map, profile_console_next_map);
}
void profile_console_map_loaded(
char const *map_name)
{
strncpy(profile_console_next_map, map_name ? map_name : "", sizeof(profile_console_next_map) - 1);
profile_trace_map_loaded();
}
/* Parse these before the script compiler: compiled map scripts index the
script function table, so profiling commands must not add entries to it. */
boolean profile_console_command(
char const *expression)
{
struct profile_trace_status status;
char const *rest;
profile_console_install();
if ((rest = profile_console_word(expression, "profile_record")) != NULL)
{
char *end;
double seconds = strtod(rest, &end);
if (end == rest)
seconds = 0.0;
if ((*rest && end == rest) || seconds < 0.0 ||
(*end && *end != ' ' && *end != '\t' && *end != ')'))
{
console_printf(FALSE, "profile: profile_record takes a number of seconds, 0 or more");
return TRUE;
}
switch (profile_trace_request_start(seconds, _profile_trace_when_now, config_integer("debug.profile_memory")))
{
case _profile_trace_answer_armed:
profile_trace_status(&status);
if (seconds > 0.0)
console_printf(FALSE, "profile: recording %s, %g s", status.name, seconds);
else
console_printf(FALSE, "profile: recording %s, until profile_stop", status.name);
break;
case _profile_trace_answer_still_writing:
profile_trace_status(&status);
console_printf(FALSE, "profile: still writing %s", status.name);
break;
default:
console_printf(FALSE, "profile: already recording");
break;
}
return TRUE;
}
if (profile_console_word(expression, "profile_stop"))
{
profile_trace_status(&status);
switch (profile_trace_request_stop())
{
case _profile_trace_answer_stopping:
console_printf(FALSE, "profile: writing %s, %ld parts", status.name, status.part);
break;
case _profile_trace_answer_disarmed:
console_printf(FALSE, "profile: not waiting for a game any more");
break;
default:
console_printf(FALSE, "profile: not recording");
break;
}
return TRUE;
}
return FALSE;
}
#endif
+25
View File
@@ -0,0 +1,25 @@
/*
PROFILE_CONSOLE.H
Expose the game recording console hooks.
*/
#ifndef __PROFILE_CONSOLE_H
#define __PROFILE_CONSOLE_H
#pragma once
#ifdef HALO_PROFILE
/* profile_record [seconds], profile_stop: TRUE
when the expression was one of them (hs.c, before the script compiler) */
boolean profile_console_command(char const *expression);
/* the first frame: the recording's seams, and the launch settings */
void profile_console_launch(void);
/* each frame, right after the frame boundary */
void profile_console_frame(void);
/* a map starts loading (game_load) */
void profile_console_map_loaded(char const *map_name);
#endif
#endif
+95 -34
View File
@@ -35,6 +35,7 @@ with their unit, and with what it rides.
#include "cseries.h"
#include "math/real_math.h"
#include "objects/objects.h"
#include "camera/camera_scripting.h"
#include "camera/director.h"
#include "camera/observer.h"
#include "cutscene/cinematics.h"
@@ -726,6 +727,48 @@ struct observer_result const *render_interpolation_camera(
render_interpolation_blended_camera(local_player_index, observer));
}
/* between two of the observer's results: the position, the axes (up kept
square to forward) and the field of view */
static void observer_blend(
struct observer_result const *a,
struct observer_result const *b,
real t,
struct observer_result *result)
{
real along, length;
*result = *b;
point_lerp(&a->position, &b->position, t, &result->position);
vector_nlerp(&a->forward, &b->forward, t, &result->forward);
vector_nlerp(&a->up, &b->up, t, &result->up);
along = result->up.i * result->forward.i + result->up.j * result->forward.j + result->up.k * result->forward.k;
result->up.i -= result->forward.i * along;
result->up.j -= result->forward.j * along;
result->up.k -= result->forward.k * along;
length = vector_length(&result->up);
if (length > 1e-6f)
{
result->up.i /= length;
result->up.j /= length;
result->up.k /= length;
}
else
{
result->up = b->up;
}
result->field_of_view = lerp(a->field_of_view, b->field_of_view, t);
}
/* a cut between two of the observer's results (so written that a position
or direction not a number cuts) */
static boolean observer_cut(
struct observer_result const *a,
struct observer_result const *b)
{
return !(distance_squared(&a->position, &b->position) <= CAMERA_CUT_DISTANCE * CAMERA_CUT_DISTANCE) ||
!(a->forward.i * b->forward.i + a->forward.j * b->forward.j + a->forward.k * b->forward.k >= CAMERA_CUT_COSINE);
}
static struct observer_result const *render_interpolation_blended_camera(
short local_player_index,
struct observer_result const *observer)
@@ -738,6 +781,42 @@ static struct observer_result const *render_interpolation_blended_camera(
return observer;
}
camera = &interpolated_cameras[local_player_index];
/* A scripted camera that moves with an object (a cinematic's camera
point relative to a lifepod or dropship, or a unit's eyes in one) is
placed by the object as it stands after the last tick. Blended from its
own snapshots (taken at the first frame after each tick, a different
part of a tick each tick), it did not follow the object as drawn, which
is blended from the object's snapshots, or steps from tick to tick when
its pose snaps: it ran up to 2 m ahead of or behind the object, out
through the lifepod's hull in a30. It is drawn where the observer has
it, moved with the object from where the object is to where it is
drawn. (An animated camera has no such object; a scripted first-person
camera looks along its unit's aim, which turns once a tick, and is
blended from its snapshots as the player's camera is.) */
if (director_get_perspective(local_player_index) == _director_perspective_scripted)
{
long relative_object_index = scripted_camera_object_relative_to();
real_matrix4x3 const *drawn;
if (relative_object_index != NONE &&
!scripted_camera_object_is_first_person_camera(relative_object_index) &&
object_try_and_get_and_verify_type(relative_object_index, _object_mask_all) &&
(drawn = render_interpolation_object_node_matrices(relative_object_index)) != NULL)
{
/* (as of the last tick: object_get_node_matrices gives the drawn
pose while a frame is drawn) */
real_matrix4x3 const *nodes = (real_matrix4x3 const *)object_header_block_get(
relative_object_index, &object_get(relative_object_index)->object.node_matrices);
camera->blended = *observer;
camera->blended.position.x += drawn[0].position.x - nodes[0].position.x;
camera->blended.position.y += drawn[0].position.y - nodes[0].position.y;
camera->blended.position.z += drawn[0].position.z - nodes[0].position.z;
/* (the snapshots start again after) */
camera->valid = FALSE;
return &camera->blended;
}
}
/* the observer as it stood after each tick (the first frame drawn
after the tick) */
if (!camera->valid || camera->tick != interpolation_tick)
@@ -751,22 +830,29 @@ static struct observer_result const *render_interpolation_blended_camera(
camera->has_previous = camera->valid;
camera->previous = camera->latest;
camera->latest = *observer;
/* Several ticks since the last snapshot (a long frame): the objects
are drawn between the last two ticks, so the camera's previous is
where it was a tick ago, as nearly as a steady move from the last
snapshot tells. (Blended across all the ticks since, the camera
moved further each frame than the world it is in, out through a
Pelican's hull for a frame.) A cut stays a cut. */
if (camera->has_previous && interpolation_tick - camera->tick > 1 &&
!observer_cut(&camera->previous, &camera->latest))
{
real ticks = (real)(interpolation_tick - camera->tick);
struct observer_result previous = camera->previous;
observer_blend(&previous, &camera->latest, (ticks - 1.0f) / ticks, &camera->previous);
}
camera->tick = interpolation_tick;
camera->valid = TRUE;
}
/* (so written that a position or direction not a number cuts) */
if (!camera->has_previous ||
!(distance_squared(&camera->previous.position, &camera->latest.position) <=
CAMERA_CUT_DISTANCE * CAMERA_CUT_DISTANCE) ||
!(camera->previous.forward.i * camera->latest.forward.i +
camera->previous.forward.j * camera->latest.forward.j +
camera->previous.forward.k * camera->latest.forward.k >= CAMERA_CUT_COSINE))
if (!camera->has_previous || observer_cut(&camera->previous, &camera->latest))
{
return observer;
}
camera->blended = camera->latest;
point_lerp(&camera->previous.position, &camera->latest.position, t, &camera->blended.position);
observer_blend(&camera->previous, &camera->latest, t, &camera->blended);
if (correction_significant(&camera->correction) || correction_significant(&camera->correction_pending))
{
real_vector3d drawn;
@@ -776,31 +862,6 @@ static struct observer_result const *render_interpolation_blended_camera(
camera->blended.position.y += drawn.j;
camera->blended.position.z += drawn.k;
}
vector_nlerp(&camera->previous.forward, &camera->latest.forward, t, &camera->blended.forward);
vector_nlerp(&camera->previous.up, &camera->latest.up, t, &camera->blended.up);
{
/* keep up perpendicular to forward */
real_vector3d *forward = &camera->blended.forward;
real_vector3d *up = &camera->blended.up;
real along = up->i * forward->i + up->j * forward->j + up->k * forward->k;
real length;
up->i -= forward->i * along;
up->j -= forward->j * along;
up->k -= forward->k * along;
length = vector_length(up);
if (length > 1e-6f)
{
up->i /= length;
up->j /= length;
up->k /= length;
}
else
{
*up = camera->latest.up;
}
}
camera->blended.field_of_view = lerp(camera->previous.field_of_view, camera->latest.field_of_view, t);
return &camera->blended;
}
+10 -4
View File
@@ -1243,12 +1243,18 @@ static int vorbis_validate(uint8 *data)
// (formula implied by specification)
static int lookup1_values(int entries, int dim)
{
int r = (int) floor(exp((float) log((float) entries) / dim));
if ((int) floor(pow((float) r+1, dim)) <= entries) // (int) cast for MinGW warning;
++r; // floor() to avoid _ftol() when non-CRT
int r;
// port: a code book of no dimensions or entries has no values (the
// division below is by zero)
if (dim <= 0 || entries <= 0) return -1;
r = (int) floor(exp((float) log((float) entries) / dim));
// port: the powers compared as they are, not cast to int first (a large
// dim's are past an int, where the cast is undefined)
if (floor(pow((float) r+1, dim)) <= entries)
++r;
if (pow((float) r+1, dim) <= entries)
return -1;
if ((int) floor(pow((float) r, dim)) > entries)
if (floor(pow((float) r, dim)) > entries)
return -1;
return r;
}
+13 -2
View File
@@ -61,6 +61,11 @@ enum
/* how deep blocks go in a schema (the deepest are 6 or 7) */
MAXIMUM_VALIDATION_DEPTH = 32,
/* the longest a tag's name is, with its terminator (Halo's tools' paths:
the game's buffers for them, game_state.c's, objects.c's, are this
size) */
MAXIMUM_TAG_NAME_LENGTH = 256,
/* how many corrections are logged one by one, for each map or bsp */
MAXIMUM_LOGGED_CORRECTIONS = 64,
MAXIMUM_MESSAGE_LENGTH = 512,
@@ -431,12 +436,18 @@ static boolean string_valid(
/* (the name a nameless tag or reference is given) */
if (string == tag_validate_empty_name)
return TRUE;
/* (and no longer than a tag's path is, MAXIMUM_TAG_NAME_LENGTH: the
game copies and formats names into buffers of that size) */
if (region_contains(validation, string, 1))
return memchr(string, 0, validation->region_size - offset) != NULL;
{
return memchr(string, 0, MIN(validation->region_size - offset, (unsigned long)MAXIMUM_TAG_NAME_LENGTH)) !=
NULL;
}
if (tags && (unsigned long)string >= (unsigned long)tags &&
tags_offset < (unsigned long)tag_validate_globals.tag_data_size)
{
return memchr(string, 0, tag_validate_globals.tag_data_size - tags_offset) != NULL;
return memchr(string, 0, MIN(tag_validate_globals.tag_data_size - tags_offset,
(unsigned long)MAXIMUM_TAG_NAME_LENGTH)) != NULL;
}
return FALSE;
+24
View File
@@ -0,0 +1,24 @@
/*
TIFF_FILE.C
port: the builds leave out source/bitmaps/tiff_file.c and the libtiff it
writes with (port/linux/port.json). Its one function, tiff_export, is
called by main.c's screenshot_record: for the Xbox's movie recorder
(main_movie_start, which nothing starts) and for the screenshot_count and
screenshot_size globals (screenshot_render). Those read the back buffer's
memory, which the port draws into with OpenGL, never the CPU, so they never
had the picture; the port's screenshots are its own (the Screenshot key,
controls.screenshot, and debug.screenshot_every: d3d8_gl.c).
*/
#include "cseries/cseries.h"
#include "bitmaps/tiff_file.h"
char const *tiff_export(
struct file_reference *file,
struct bitmap_data *bitmap)
{
(void)file;
(void)bitmap;
return "this build writes no TIFF screenshots: use the Screenshot key (controls.screenshot)";
}
+59
View File
@@ -0,0 +1,59 @@
/*
TOUCH_GAME.C
What the touch controls (port/linux/src/touch_input.c) need to know of the
game. The platform layer cannot see the game's types, so it asks here
(this file is compiled as the game's own sources are).
*/
#include "cseries.h"
#include "game/game.h"
#include "cutscene/cinematics.h"
#include "input/input.h"
#include "input/input_abstraction.h"
#include "interface/ui_widget.h"
/* asks whether a cinematic is playing that A would skip
(player_control.c, player_control_action_test_check_reset_input_blob); tests
cinematic_globals first because the controller is read before the game exists
during input_initialize and shell_initialize, and game_initialize makes it
after the game time globals that game_in_progress reads (game.c) */
int touch_game_cinematic_skippable(void)
{
return cinematic_globals && game_in_progress() && cinematic_in_progress() &&
cinematic_can_be_skipped();
}
/* asks whether a cinematic is playing, skippable or not: the player has no
controls, so the on-screen touch controls hide (touch_input.c) */
int touch_game_cinematic_playing(void)
{
return cinematic_globals && game_in_progress() && cinematic_in_progress();
}
/* asks whether a game is being played: not while the game starts, nor
between maps, nor at the main menu (whose ui.map is a game too), where the
on-screen touch controls do not show (touch_input.c); cinematic_globals
first, as touch_game_cinematic_skippable has it */
int touch_game_playing(void)
{
return cinematic_globals && game_in_progress() && !main_menu_is_active();
}
/* (touch_input.c's TOUCH_BUTTONS and the overlay count 16 buttons) */
typedef char verify_touch_buttons[NUMBER_OF_GAMEPAD_BUTTONS == 16 ? 1 : -1];
/* the game control on each of player 1's 16 controller buttons, as the
gamepad's buttons are numbered (input.h), -1 for none: the touch controls
name their buttons by them, so a profile's own mapping shows */
void touch_game_button_controls(int *controls)
{
short buttons[NUMBER_OF_GAMEPAD_BUTTONS];
short index;
input_abstraction_port_button_controls(0, buttons);
for (index = 0; index < NUMBER_OF_GAMEPAD_BUTTONS; index++)
{
controls[index] = buttons[index];
}
}
+286
View File
@@ -0,0 +1,286 @@
/*
VIEW_FOV.C
The first-person view's field of view (display.fov, display.viewmodel_fov)
and whether the weapon is drawn (display.viewmodel_visible), the local
render view only: the observer, its transitions and what the network sends
keep the game's own camera. Each angle is horizontal at 16:9 (the vertical
angle is kept, so other shapes of window widen or narrow as the stock view
does), and 0 keeps the stock view. Called from the game's camera
(main.c's set_window_camera_values), its HUD (hud_draw.c) and the passes
that draw the first-person weapon (rasterizer and render sources).
*/
#include "cseries.h"
#include "view_fov.h"
#include "camera/director.h"
#include "cutscene/cinematics.h"
#include "game/players.h"
#include "items/weapons.h"
#include "objects/objects.h"
#include "rasterizer/rasterizer.h"
#include "rasterizer/xbox/rasterizer_xbox_internal.h"
#include "render/render.h"
#include "render/render_cameras.h"
#include "render/render_cameras_internal.h"
#include "units/unit_definitions.h"
#include "units/units.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
#include <math.h>
/* (the window's rasterizer target, as the rasterizer's sources number it) */
enum
{
_rasterizer_target_render_primary = 0
};
double config_real(const char *name);
int config_boolean(const char *name);
unsigned long config_changes(void);
/* port: in VR the headset gives each eye's view (vr.c) and the weapon is
held in the hand: display.fov, display.viewmodel_fov and
display.viewmodel_visible do nothing */
static boolean view_fov_vr(void)
{
#ifdef HALO_ANDROID
return halo_vr_active() != 0;
#else
return FALSE;
#endif
}
/* ---------- the world's field of view */
static real reticle_scales[MAXIMUM_LOCAL_PLAYERS];
static real authored_vertical[MAXIMUM_LOCAL_PLAYERS];
/* the view's vertical angle for display.fov: the game's own in a vehicle,
dead, in a cinematic, a scripted camera or a view the director holds, and
at a scope's full zoom; between, the zoom's own transition */
static real render_fov_adjust(short local_player_index, real native_vertical_field_of_view)
{
static boolean initialized;
static unsigned long read_at;
static real requested_tangent;
unsigned long changes = config_changes();
long unit_index, weapon_index;
struct unit_datum *unit;
real unzoomed_angle, unzoomed_tangent, native_tangent, adjusted_tangent;
if (!initialized || read_at != changes)
{
double degrees = config_real("display.fov");
initialized = TRUE;
read_at = changes;
requested_tangent = degrees >= 20.0 && degrees <= 150.0 ?
(real)(tan(degrees * _pi / 360.0) * (9.0 / 16.0)) : 0.0f;
}
if (!requested_tangent || view_fov_vr() || local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
director_get_perspective(local_player_index) != _director_perspective_first_person ||
director_inhibited_facing(local_player_index))
{
return native_vertical_field_of_view;
}
unit_index = player_control_get_unit_index(local_player_index);
if (unit_index == NONE)
return native_vertical_field_of_view;
unit = unit_try_and_get(unit_index);
if (!unit || unit->object.parent_object_index != NONE || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
return native_vertical_field_of_view;
unzoomed_angle = unit_definition_get(unit->definition_index)->unit.camera_field_of_view;
if (!(unzoomed_angle >= 0.001f && unzoomed_angle <= _pi / 2.0f) ||
!(native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi))
{
return native_vertical_field_of_view;
}
unzoomed_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(unzoomed_angle);
native_tangent = tanf(native_vertical_field_of_view * 0.5f);
if (requested_tangent < unzoomed_tangent)
{
/* (a view narrower than the stock one: the zoom narrows it further) */
adjusted_tangent = native_tangent * (requested_tangent / unzoomed_tangent);
}
else
{
real blend = 1.0f;
weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index);
if (weapon_index != NONE)
{
real scoped_angle = weapon_get_field_of_view(weapon_index, unzoomed_angle, 0);
if (scoped_angle > 0.0f && scoped_angle < unzoomed_angle)
{
real scoped_tangent = 0.75f * render_camera_get_adjusted_field_of_view_tangent(scoped_angle);
/* (a scope's full zoom keeps its stock angle exactly: the
observer's arctangent and tangent can land just above it) */
if (native_tangent <= scoped_tangent * 1.000001f)
return native_vertical_field_of_view;
/* (the extra width fades out along the scope's own
transition, so every zoom level reached is the stock view) */
blend = PIN((native_tangent - scoped_tangent) /
(unzoomed_tangent - scoped_tangent), 0.0f, 1.0f);
}
}
if (blend == 0.0f)
return native_vertical_field_of_view;
adjusted_tangent = native_tangent + (requested_tangent - unzoomed_tangent) * blend;
}
return 2.0f * atanf(adjusted_tangent);
}
real render_fov_vertical(short local_player_index, real native_vertical_field_of_view)
{
real adjusted = render_fov_adjust(local_player_index, native_vertical_field_of_view);
if (local_player_index >= 0 && local_player_index < MAXIMUM_LOCAL_PLAYERS)
{
real scale = 1.0f;
if (adjusted != native_vertical_field_of_view &&
native_vertical_field_of_view > 0.0f && native_vertical_field_of_view < _pi &&
adjusted > 0.0f && adjusted < _pi)
{
scale = tanf(native_vertical_field_of_view * 0.5f) / tanf(adjusted * 0.5f);
if (!(scale > 0.0f && scale < 1000.0f))
scale = 1.0f;
}
/* (the view's projection, the zoom's transition too, for the HUD:
the weapon's own projection, viewmodel_projection_begin, is not it) */
reticle_scales[local_player_index] = scale;
/* (a widened view's own angle, which the weapon keeps by default) */
authored_vertical[local_player_index] =
adjusted != native_vertical_field_of_view ? native_vertical_field_of_view : 0.0f;
}
return adjusted;
}
real render_fov_authored_vertical(short local_player_index)
{
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS)
return 0.0f;
return authored_vertical[local_player_index];
}
real render_fov_reticle_scale(short local_player_index)
{
if (local_player_index < 0 || local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
reticle_scales[local_player_index] == 0.0f)
{
return 1.0f;
}
return reticle_scales[local_player_index];
}
/* ---------- the first-person weapon's */
/* how deep the weapon's passes nest, and whether the outermost changed the
view (the cameras and frustums it saved) */
static unsigned long projection_depth;
static boolean projection_applied;
static struct render_camera saved_render_camera, saved_rasterizer_camera;
static struct render_frustum saved_render_frustum, saved_rasterizer_frustum;
static real viewmodel_vertical(void)
{
static boolean initialized;
static unsigned long read_at;
static real angle;
unsigned long changes = config_changes();
if (!initialized || read_at != changes)
{
double degrees = config_real("display.viewmodel_fov");
angle = degrees >= 20.0 && degrees <= 150.0 ?
(real)(2.0 * atan(tan(degrees * _pi / 360.0) * (9.0 / 16.0))) : 0.0f;
initialized = TRUE;
read_at = changes;
}
return angle;
}
void viewmodel_projection_begin(void)
{
real angle;
real_rectangle2d render_bounds, rasterizer_bounds;
if (projection_depth++ != 0)
return;
projection_applied = FALSE;
if (view_fov_vr())
return;
/* (display.viewmodel_fov's angle; by default the view's own, from before
display.fov widened it: arms and a gun right against the camera stretch
at a wide angle) */
angle = viewmodel_vertical();
if (!angle)
angle = render_fov_authored_vertical(render.local_player_index);
if (!angle || render.local_player_index < 0 || render.local_player_index >= MAXIMUM_LOCAL_PLAYERS ||
global_window_parameters.rasterizer_target != _rasterizer_target_render_primary ||
cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
director_get_perspective(render.local_player_index) != _director_perspective_first_person ||
director_inhibited_facing(render.local_player_index))
{
return;
}
saved_render_camera = render.camera;
saved_render_frustum = render.frustum;
saved_rasterizer_camera = global_window_parameters.camera;
saved_rasterizer_frustum = global_window_parameters.frustum;
/* (the frustums' own bounds, the view's crop of a split screen: the
visibility and the projection change together, the weapon's pose and
markers do not; get_projection_bounds' slopes are not bounds) */
render_bounds = saved_render_frustum.frustum_bounds;
rasterizer_bounds = saved_rasterizer_frustum.frustum_bounds;
render.camera.vertical_field_of_view = angle;
global_window_parameters.camera.vertical_field_of_view = angle;
render_camera_build_frustum(&render.camera, &render_bounds, &render.frustum, TRUE);
render_camera_build_frustum(&global_window_parameters.camera, &rasterizer_bounds,
&global_window_parameters.frustum, TRUE);
projection_applied = TRUE;
rasterizer_set_frustum_z(0.0f, 0.0f);
}
void viewmodel_projection_end(void)
{
if (!projection_depth || --projection_depth || !projection_applied)
return;
render.camera = saved_render_camera;
render.frustum = saved_render_frustum;
global_window_parameters.camera = saved_rasterizer_camera;
global_window_parameters.frustum = saved_rasterizer_frustum;
projection_applied = FALSE;
rasterizer_set_frustum_z(0.0f, 0.0f);
}
/* ---------- whether the first-person weapon is drawn */
boolean viewmodel_is_visible(void)
{
static boolean initialized, visible;
static unsigned long read_at;
unsigned long changes = config_changes();
if (!initialized || read_at != changes)
{
visible = config_boolean("display.viewmodel_visible") != 0;
initialized = TRUE;
read_at = changes;
}
return visible || view_fov_vr();
}
boolean viewmodel_draws_geometry(boolean first_person)
{
return !first_person || viewmodel_is_visible();
}
+31
View File
@@ -0,0 +1,31 @@
/*
VIEW_FOV.H
The first-person view's field of view and weapon (view_fov.c).
*/
#ifndef VIEW_FOV_H
#define VIEW_FOV_H
/* the vertical angle the local player's world view is drawn at (display.fov,
after the observer and before the projection and the visibility frustum
are built), from the game's own */
real render_fov_vertical(short local_player_index, real native_vertical_field_of_view);
/* how much the reticle shrinks or grows to stay on its aim at that view (the
last local view's own projection against the game's): 1 for the stock view */
real render_fov_reticle_scale(short local_player_index);
/* the view's own vertical angle when display.fov widened it, else 0 */
real render_fov_authored_vertical(short local_player_index);
/* the first-person weapon's projection (display.viewmodel_fov) around its
passes: balanced and nestable, the current local render view only */
void viewmodel_projection_begin(void);
void viewmodel_projection_end(void);
/* whether the first-person weapon, hands and attached visuals are drawn
(display.viewmodel_visible); its firing, sounds and lights go on either way */
boolean viewmodel_is_visible(void);
/* whether a pass draws: not one of the first-person weapon's that is hidden */
boolean viewmodel_draws_geometry(boolean first_person);
#endif
+7
View File
@@ -34,6 +34,10 @@ enum halo_keyboard_action
read from it) */
HALO_KEYBOARD_SCOREBOARD,
HALO_KEYBOARD_PAUSE,
HALO_KEYBOARD_SCREENSHOT,
/* (voice chat's, which the game itself does not read:
halo_push_to_talk_held) */
HALO_KEYBOARD_PUSH_TO_TALK,
NUMBER_OF_HALO_KEYBOARD_ACTIONS
};
@@ -41,5 +45,8 @@ enum halo_keyboard_action
(a bit for each halo_keyboard_action): theirs is the first's; none while a
menu or the console is up */
unsigned long halo_keyboard_actions(short controller_index);
/* whether voice chat's push to talk is held (controls.push_to_talk), in the
game and in the menus alike */
int halo_push_to_talk_held(void);
#endif
+1 -1
View File
@@ -10,7 +10,7 @@ byte-for-byte identical to what the matching MSVC build compiles.
#ifndef __HALO_LINUX_PREFIX_H
#define __HALO_LINUX_PREFIX_H
#if !defined(__i386__) && !defined(HALO_ANDROID)
#if !defined(__i386__) && !defined(HALO_ARM64_GUEST)
#error the Linux port targets 32-bit x86: game data structures assume 32-bit pointers
#endif
+3 -11
View File
@@ -1,22 +1,12 @@
/*
HALO_LINUX_SOURCE_FIXUPS.H
Game-only workarounds for source that MSVC accepts but clang rejects, where
editing the source itself would change the byte-matched MSVC output (see
port/linux/README.md for how each was checked).
The port's functions that the game's sources call, declared for them.
*/
#ifndef __HALO_LINUX_SOURCE_FIXUPS_H
#define __HALO_LINUX_SOURCE_FIXUPS_H
/* rasterizer.h declares rasterizer_debug_drawing_begin(boolean opaque) while
rasterizer_xbox_debug.h declares a second `long zbias` parameter, and
rasterizer_debug.c includes both and passes two arguments. MSVC tolerates
the mismatch; the definition ignores zbias. Adding the parameter to
rasterizer.h perturbs MSVC's register allocation elsewhere, so instead every
declaration and call collapses to the one-parameter form here. */
#define rasterizer_debug_drawing_begin(opaque, ...) (rasterizer_debug_drawing_begin)(opaque)
/* frames between the 30 Hz ticks (port/linux/game/render_interpolation.c);
the platform layer reads the display.interpolation setting */
struct observer_result;
@@ -57,6 +47,8 @@ unsigned char custom_edition_vehicle_placement_allowed(struct scenario_object_da
/* the width of the screen the game draws, 480 lines tall: the device's or
the display's shape, or 640 (port/linux/src/d3d8_gl.c) */
long halo_screen_width(void);
/* the screen's pixels to the Xbox's one (port/linux/src/d3d8_gl.c) */
float halo_screen_scale(void);
/* takes up a new width between frames (F11); returns the width */
long halo_screen_commit(void);
/* the shadow maps' pixels for each of their 128 texels each way, a power of
+2 -2
View File
@@ -46,7 +46,7 @@ measurement). The desktop builds' cache is 256 MB, half their 512 MB memory
window (port/linux/src/platform.h), whose pages are backed as they are used.
Android's window is 128 MB, and its cache the Xbox's. */
#ifdef HALO_ANDROID
#ifdef HALO_ARM64_GUEST
#define HALO_PORT_TEXTURE_CACHE_PAGE_COUNT 0x580 /* (0x580) */
#else
#define HALO_PORT_TEXTURE_CACHE_PAGE_COUNT 0x4000 /* (0x580) */
@@ -62,7 +62,7 @@ over a map's (loose_sounds.c), are longer than the Xbox maps' and fill it (a
campaign map's dialogue and music did, "SOUND CACHE BLOWN" in debug.txt).
The desktop builds' cache is 16 MB; Android's window keeps the Xbox's. */
#ifdef HALO_ANDROID
#ifdef HALO_ARM64_GUEST
#define HALO_PORT_SOUND_CACHE_SIZE 0x400000 /* (0x400000) */
#else
#define HALO_PORT_SOUND_CACHE_SIZE 0x1000000 /* (0x400000) */
+1 -1
View File
@@ -66,7 +66,7 @@ is newer (network_client_manager.c). A host advertises it, with its netcode,
in its game's advertisement's reserved bytes (network_server_message_handler.c),
which hosts built before there was a version send as zeros: version 0.
Raise it with any change to what the machines send each other. */
#define HALO_PORT_NETWORK_VERSION 24
#define HALO_PORT_NETWORK_VERSION 25
/* ... the advertisement's reserved bytes: the version (a little-endian word),
then flags */
#define HALO_PORT_ADVERTISED_VERSION_OFFSET 0
+23 -6
View File
@@ -1,9 +1,11 @@
/*
HALO_UI_POINTER.H
The mouse in the menus of the desktop builds. While a menu is up, the mouse
is released and its pointer shows (port/linux/src/sdl_platform.c); each frame
the menus (source/interface/ui_widget.c) ask where it is, in their own 640x480
The mouse in the menus of the desktop builds, and the touchscreen in those
of the Android build. While a menu is up, the mouse is released and its
pointer shows (port/linux/src/sdl_platform.c), or taps and drags go to the
menus (port/linux/src/touch_input.c); each frame the menus
(source/interface/ui_widget.c) ask where the pointer is, in their own 640x480
coordinates (port/linux/src/d3d8_gl.c undoes the letterbox, the scale and the
widescreen centering), and what it did since.
*/
@@ -21,11 +23,26 @@ struct halo_ui_pointer
unsigned char right_clicks;
/* whole wheel notches, away from the user positive */
signed char wheel_steps;
/* nonzero when the pointer is the touchscreen, not a mouse: a drag and a
wheel notch are the same steps, but the menus treat them differently */
unsigned char touch;
/* fingers down since the last read and where the latest went down; only
the touchscreen reports them (debug.touch_targets shows them) */
unsigned char downs;
short down_x, down_y;
};
/* frees the mouse for the menus while menus_active, and captures it again
for aiming when not; while menus are active returns nonzero and what the
pointer did since the last call. Always 0 on Android. */
/* gives the menus the pointer; frees the mouse for the menus while
menus_active and captures it for aiming when not (touchscreen: taps and
drags go to the menus while menus_active); returns nonzero while menus are
active and the pointer could be read */
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer);
/* the open scoreboard's pointer (game_engine.c), offered (a network game's)
or not: 1 while a right click has freed it, with where it is and what it
did since the last call, in the screen's coordinates (the game's drawing,
not the menus' centered 640); 0 while it is not; -1 where there is none
(Android) */
int halo_scoreboard_pointer_update(int offered, struct halo_ui_pointer *pointer);
#endif
@@ -0,0 +1,30 @@
/*
PROFILE_CONSOLE_GAMETYPE.H
Name the current scenario and running game engine for recordings.
*/
#ifndef __PROFILE_CONSOLE_GAMETYPE_H
#define __PROFILE_CONSOLE_GAMETYPE_H
static inline const char *profile_console_scenario_map(const char *pending, const char *current)
{
return pending && pending[0] ? pending : current;
}
/* (engine indices none, ctf, slayer, oddball, king, race; the check program
cannot include game_engine.h. The variant outlives the engine into campaign
and co-op, so it is read only while an engine runs) */
static inline const char *profile_console_engine_gametype(int running, long engine)
{
static const char *const names[] = { "none", "ctf", "slayer", "oddball", "king", "race" };
if (!running)
return "campaign";
if (engine == 0)
return "none";
if (engine >= 1 && engine <= 5)
return names[engine];
return "none";
}
#endif
+33
View File
@@ -0,0 +1,33 @@
/*
PROFILE_SECTIONS.H
The profiling build's CPU sections around the game's steps (configure.py
--profile, port/linux/src/profile_trace.c): in a normal build each wraps
its statement and leaves nothing else.
*/
#ifndef __PROFILE_SECTIONS_H
#define __PROFILE_SECTIONS_H
#ifdef HALO_PROFILE
#include "cseries/profile.h"
/* these macros leave no tokens in a normal build: call sites have no trailing
semicolon. a call site may be an unbraced if-branch: in a normal build
profile_scope is the statement alone. */
#define PROFILE_SECTION(variable, name) static struct profile_section variable = { name, NONE, TRUE };
#define profile_scope_enter(variable) profile_enter(variable)
#define profile_scope_exit(variable) profile_exit(variable)
#define profile_scope(variable, statement) { profile_enter(variable) statement profile_exit(variable) }
#else
#define PROFILE_SECTION(variable, name)
#define profile_scope_enter(variable)
#define profile_scope_exit(variable)
#define profile_scope(variable, statement) statement
#endif
#endif
+144
View File
@@ -0,0 +1,144 @@
/*
PROFILE_TRACE.H
Expose bounded CPU trace recording and session controls.
*/
#ifndef __PROFILE_TRACE_H
#define __PROFILE_TRACE_H
#ifdef HALO_PROFILE
/* included by C89 game units: keep declarations compatible with them. */
/* ---------- constants */
enum
{
_profile_track_game = 0,
_profile_track_p2p,
NUMBER_OF_PROFILE_TRACKS,
MAXIMUM_PROFILE_TRACE_NAMES = 512,
MAXIMUM_PROFILE_TRACE_DEPTH = 64,
MAXIMUM_PROFILE_TRACE_AGGREGATES = 32,
/* debug.profile_memory: both arenas together, megabytes */
DEFAULT_PROFILE_TRACE_MEMORY = 256,
MINIMUM_PROFILE_TRACE_MEMORY = 4,
MAXIMUM_PROFILE_TRACE_MEMORY = 1024,
};
enum profile_trace_state
{
_profile_trace_idle = 0,
_profile_trace_armed,
_profile_trace_recording,
_profile_trace_finishing,
};
/* when an armed recording starts: at the next frame, or once a game that is
not the main menu is in progress (debug.profile_record_when = "game") */
enum profile_trace_when
{
_profile_trace_when_now = 0,
_profile_trace_when_game,
};
enum profile_trace_stop_reason
{
_profile_trace_stop_none = 0,
_profile_trace_stop_command,
_profile_trace_stop_seconds,
_profile_trace_stop_map_load,
_profile_trace_stop_exit,
};
enum profile_trace_answer
{
_profile_trace_answer_armed = 0,
_profile_trace_answer_already_recording,
_profile_trace_answer_still_writing,
_profile_trace_answer_stopping,
_profile_trace_answer_disarmed,
_profile_trace_answer_not_recording,
};
/* ---------- structures */
/* (the recording's identity at start; players_most grows while it runs) */
struct profile_trace_session
{
char folder[260];
char platform[16];
char build[48];
/* host, client or local */
char role[8];
/* a client's own machine index, -1 otherwise */
long own_machine;
char map[64];
char map_name[64];
char gametype[16];
long players;
long players_most;
};
struct profile_trace_status
{
int state;
char name[64];
long part;
};
/* ---------- prototypes/PROFILE_TRACE.C */
/* the seams: set once at start-up by the game (profile_console.c), and by
tools/profile_check.c with fakes */
void profile_trace_set_clock(unsigned long long (*now_ns)(void));
void profile_trace_set_track_lock(void (*lock)(void), void (*unlock)(void));
void profile_trace_set_allocator(void *(*allocate)(unsigned long size), void (*release)(void *block));
void profile_trace_set_log(void (*log)(const char *text));
void profile_trace_set_notify(void (*notify)(const char *text));
/* starts the writer of a new recording (profile_json_writer_start): 0 when
it cannot */
void profile_trace_set_writer(int (*start)(void));
/* whether a game that is not the main menu is in progress */
void profile_trace_set_ready(int (*ready)(void));
void profile_trace_set_session(void (*describe)(struct profile_trace_session *session));
void profile_trace_set_session_sampler(void (*sample)(struct profile_trace_session *session));
/* which track the calling thread records on */
void profile_trace_thread_register(int track);
/* a name's id (the same name, the same id, for the whole run); an
aggregate name's scopes are summed per frame rather than recorded */
int profile_trace_name(const char *name);
void profile_trace_name_aggregate(const char *name);
void profile_trace_begin(int name);
void profile_trace_end(int name);
/* the game thread, with no scope open: where a recording starts, is cut
and stops */
void profile_trace_frame_boundary(void);
/* the game thread, at each game tick's start */
void profile_trace_tick(void);
/* the console's requests (the game thread), applied at the next frame
boundary; seconds 0 records until stopped */
int profile_trace_request_start(double seconds, int when, long memory_megabytes);
int profile_trace_request_stop(void);
/* a map starts loading: a recording stops, but for the first load after
launch (the main menu, or init.txt's map) */
void profile_trace_map_loaded(void);
/* the process ends: stops a recording, cuts its last part and waits for
the writer (main_exit, and atexit for the other ways out) */
void profile_trace_shutdown(void);
/* whether the tracks record now: changed under the track lock, so a p2p
pass sees one value */
int profile_trace_recording(void);
void profile_trace_status(struct profile_trace_status *status);
#endif
#endif
+3 -3
View File
@@ -1,11 +1,11 @@
{
"comment": "Native build inputs, shared by the Windows and Android builds. See port/linux/README.md.",
"game": {
"comment": "The game: every C file under root but those excluded (zlib's and libtiff's own programs), built with these defines and include directories.",
"comment": "The game: every C file under root but those excluded (a folder ends in /: libtiff and the TIFF writer that uses it, port/linux/game/tiff_file.c has why; zlib's own programs), built with these defines and include directories.",
"root": "source",
"exclude": [
"source/bitmaps/libtiff/mkg3states.c",
"source/bitmaps/libtiff/mkspans.c",
"source/bitmaps/libtiff/",
"source/bitmaps/tiff_file.c",
"source/memory/zlib/contrib/asm386/gvmat32c.c",
"source/memory/zlib/contrib/minizip/miniunz.c",
"source/memory/zlib/contrib/minizip/minizip.c",
+491 -136
View File
File diff suppressed because it is too large. Load diff
+237 -32
View File
@@ -37,9 +37,11 @@ turns the reverb off; audio.enabled = false skips opening a device
#include "platform.h"
#include "sdl_platform.h"
#include "port_config.h"
#include "voice_audio.h"
#include <SDL3/SDL.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -60,6 +62,11 @@ turns the reverb off; audio.enabled = false skips opening a device
#define RESAMPLER_KAISER_BETA 6.5
#define RESAMPLER_MAXIMUM_STRETCH 2
#define RESAMPLER_HISTORY 128
#if defined(__GNUC__) || defined(__clang__)
#define RESAMPLER_ALIGNED __attribute__((aligned(16)))
#else
#define RESAMPLER_ALIGNED
#endif
/* ---------- voices */
@@ -119,9 +126,12 @@ struct sdl_stream
/* the next frame to take from the first packet not finished */
unsigned long cursor;
/* the resampler's (resampler_reset): the last RESAMPLER_HISTORY frames
taken, how many were taken, the frame the output is at and how far past
it, and the frames of silence taken since the packets ran out */
float history[RESAMPLER_HISTORY][2];
taken, each channel's apart and each frame twice, RESAMPLER_HISTORY apart,
so that the frames a low pass reaches are side by side however the ring
wraps (resampler_dot); how many were taken, the frame the output is at and
how far past it, and the frames of silence taken since the packets ran
out */
float history[2][2 * RESAMPLER_HISTORY];
unsigned long history_count;
unsigned long center;
double phase;
@@ -467,8 +477,11 @@ static float resampler_table[RESAMPLER_ZERO_CROSSINGS * RESAMPLER_TABLE_STEPS +
to RESAMPLER_ZERO_CROSSINGS, at each of RESAMPLER_TABLE_STEPS phases between two
source frames (and one row more, for the last's blend): a voice at the output
rate or slower blends two rows by its phase, which is what the table gave tap
by tap, a third of the work */
static float resampler_phases[RESAMPLER_TABLE_STEPS + 1][2 * RESAMPLER_ZERO_CROSSINGS];
by tap, a third of the work. resampler_deltas holds each row's step to the
next, so the blend is a multiply and an add a tap */
#define RESAMPLER_TAPS (2 * RESAMPLER_ZERO_CROSSINGS)
static float resampler_phases[RESAMPLER_TABLE_STEPS + 1][RESAMPLER_TAPS] RESAMPLER_ALIGNED;
static float resampler_deltas[RESAMPLER_TABLE_STEPS][RESAMPLER_TAPS] RESAMPLER_ALIGNED;
static double bessel_i0(double x)
{
@@ -527,6 +540,77 @@ static void resampler_phases_initialize(void)
resampler_phases[phase][tap] = resampler_weight(distance * RESAMPLER_TABLE_STEPS);
}
}
for (phase = 0; phase < RESAMPLER_TABLE_STEPS; phase++)
{
for (tap = 0; tap < RESAMPLER_TAPS; tap++)
resampler_deltas[phase][tap] = resampler_phases[phase + 1][tap] - resampler_phases[phase][tap];
}
}
/* The low pass over a voice's frames, the sum of each frame times its weight:
four taps at a time where the compiler has vectors (SSE, NEON), summed in four
lanes, then the lanes together. Only the order of the additions differs from
tap by tap, a difference of the order of float's rounding (-115 dB or less);
RESAMPLER_SCALAR adds them tap by tap (tools/harness/tests/test_mixer.py). */
#if (defined(__GNUC__) || defined(__clang__)) && !defined(RESAMPLER_SCALAR)
typedef float resampler_vector __attribute__((vector_size(16)));
typedef float resampler_unaligned_vector __attribute__((vector_size(16), aligned(4)));
/* (each row of the phase tables whole vectors, aligned) */
typedef char resampler_taps_in_fours[RESAMPLER_TAPS % 4 ? -1 : 1];
#define RESAMPLER_LANES 4
#else
#define RESAMPLER_LANES 1
#endif
/* frames[0, RESAMPLER_TAPS) weighted by the phase table's row blended blend of
the way to the next */
static float resampler_dot(const float *frames, const float *weights, const float *deltas, float blend)
{
#if RESAMPLER_LANES == 4
resampler_vector sum = { 0.0f, 0.0f, 0.0f, 0.0f }, blends = { blend, blend, blend, blend };
int tap;
for (tap = 0; tap < RESAMPLER_TAPS; tap += 4)
{
resampler_vector weight = *(const resampler_vector *)(weights + tap) +
*(const resampler_vector *)(deltas + tap) * blends;
sum += *(const resampler_unaligned_vector *)(frames + tap) * weight;
}
return (sum[0] + sum[1]) + (sum[2] + sum[3]);
#else
float sum = 0.0f;
int tap;
for (tap = 0; tap < RESAMPLER_TAPS; tap++)
sum += frames[tap] * (weights[tap] + deltas[tap] * blend);
return sum;
#endif
}
/* resampler_dot of two channels' frames, with the same weights */
static void resampler_dot2(const float *left_frames, const float *right_frames, const float *weights,
const float *deltas, float blend, float *left, float *right)
{
#if RESAMPLER_LANES == 4
resampler_vector left_sum = { 0.0f, 0.0f, 0.0f, 0.0f }, right_sum = left_sum;
resampler_vector blends = { blend, blend, blend, blend };
int tap;
for (tap = 0; tap < RESAMPLER_TAPS; tap += 4)
{
resampler_vector weight = *(const resampler_vector *)(weights + tap) +
*(const resampler_vector *)(deltas + tap) * blends;
left_sum += *(const resampler_unaligned_vector *)(left_frames + tap) * weight;
right_sum += *(const resampler_unaligned_vector *)(right_frames + tap) * weight;
}
*left = (left_sum[0] + left_sum[1]) + (left_sum[2] + left_sum[3]);
*right = (right_sum[0] + right_sum[1]) + (right_sum[2] + right_sum[3]);
#else
*left = resampler_dot(left_frames, weights, deltas, blend);
*right = resampler_dot(right_frames, weights, deltas, blend);
#endif
}
/* a voice starting (over): silence before its first frame, which the output
@@ -579,6 +663,26 @@ static BOOL take_frame(struct sdl_stream *stream, float *frame)
}
}
/* whether the voice has a frame to take; the packets with none left are
marked finished on the way, as take_frame marks them */
static BOOL voice_has_frames(struct sdl_stream *stream)
{
unsigned long position;
for (position = 0; position < stream->packet_count; position++)
{
struct voice_packet *packet = &stream->packets[(stream->packet_head + position) % MAXIMUM_STREAM_PACKETS];
if (packet->finished)
continue;
if (stream->cursor < packet->frames)
return TRUE;
stream->cursor = 0;
packet->finished = TRUE;
}
return FALSE;
}
/* ---------- mixing */
/* mixes one voice into output (frames of stereo float), and into the reverb's
@@ -589,6 +693,7 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
float target_left, target_right, target_room, left, right, room, ramp_left, ramp_right, ramp_room, scale;
float target_direct_lowpass, target_room_lowpass, direct_lowpass, room_lowpass;
float ramp_direct_lowpass, ramp_room_lowpass;
float direct_state[2], room_state;
long width;
unsigned long frame;
/* (a stereo voice panned towards its sound: voice_gains) */
@@ -601,6 +706,16 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
rate, and the frames it reaches on each side */
scale = step > 1.0 ? (float)(1.0 / (step < RESAMPLER_MAXIMUM_STRETCH ? step : RESAMPLER_MAXIMUM_STRETCH)) : 1.0f;
width = (long)ceilf(RESAMPLER_ZERO_CROSSINGS / scale);
/* a voice that ran dry and stopped (below) starts over once it has
frames again: stream_process starts over only a stream with no packets,
and the next can come before the finished ones are completed */
if (stream->silence > (unsigned long)(2 * width))
{
if (!voice_has_frames(stream))
return;
resampler_reset(stream);
stream->gains_valid = FALSE;
}
voice_gains(stream, &target_left, &target_right, &target_room, &target_direct_lowpass, &target_room_lowpass);
if (!stream->gains_valid)
{
@@ -618,6 +733,11 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
room = stream->current_room;
direct_lowpass = stream->current_direct_lowpass;
room_lowpass = stream->current_room_lowpass;
/* the low passes' states, here while the mix writes the output (which
the compiler cannot tell from them) */
direct_state[0] = stream->direct_lowpass[0];
direct_state[1] = stream->direct_lowpass[1];
room_state = stream->room_lowpass;
ramp_left = (target_left - left) / (float)frames;
ramp_right = (target_right - right) / (float)frames;
ramp_room = (target_room - room) / (float)frames;
@@ -633,17 +753,20 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
silence, until what was taken has played out */
while ((long)(stream->history_count - stream->center) <= width)
{
float *slot = stream->history[stream->history_count % RESAMPLER_HISTORY];
unsigned long slot = stream->history_count % RESAMPLER_HISTORY;
float taken[2];
if (take_frame(stream, slot))
if (take_frame(stream, taken))
{
stream->silence = 0;
}
else
{
slot[0] = slot[1] = 0.0f;
taken[0] = taken[1] = 0.0f;
stream->silence++;
}
stream->history[0][slot] = stream->history[0][slot + RESAMPLER_HISTORY] = taken[0];
stream->history[1][slot] = stream->history[1][slot + RESAMPLER_HISTORY] = taken[1];
stream->history_count++;
}
if (stream->silence > (unsigned long)(2 * width))
@@ -659,46 +782,53 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
double position = stream->phase * RESAMPLER_TABLE_STEPS;
unsigned long row = (unsigned long)position;
float blend = (float)(position - (double)row);
const float *weights = resampler_phases[row], *next_weights = resampler_phases[row + 1];
unsigned long first = stream->center + 1 - RESAMPLER_ZERO_CROSSINGS;
unsigned long first = (stream->center + 1 - RESAMPLER_ZERO_CROSSINGS) % RESAMPLER_HISTORY;
for (tap = 0; tap < 2 * RESAMPLER_ZERO_CROSSINGS; tap++)
/* (a mono voice's two channels are the same: take_frame) */
if (stream->channels == 1)
{
const float *source = stream->history[(first + (unsigned long)tap) % RESAMPLER_HISTORY];
float weight = weights[tap] + (next_weights[tap] - weights[tap]) * blend;
sample_left += source[0] * weight;
sample_right += source[1] * weight;
sample_left = sample_right = resampler_dot(stream->history[0] + first, resampler_phases[row],
resampler_deltas[row], blend);
}
else
{
resampler_dot2(stream->history[0] + first, stream->history[1] + first, resampler_phases[row],
resampler_deltas[row], blend, &sample_left, &sample_right);
}
}
else
{
unsigned long first = (stream->center + 1 - (unsigned long)width) % RESAMPLER_HISTORY;
const float *left_frames = stream->history[0] + first, *right_frames = stream->history[1] + first;
for (tap = 1 - width; tap <= width; tap++)
{
const float *source = stream->history[(stream->center + (unsigned long)tap) % RESAMPLER_HISTORY];
float weight = scale * resampler_weight(fabsf((float)tap - (float)stream->phase) * scale * RESAMPLER_TABLE_STEPS);
sample_left += source[0] * weight;
sample_right += source[1] * weight;
sample_left += left_frames[tap + width - 1] * weight;
if (stream->channels == 2)
sample_right += right_frames[tap + width - 1] * weight;
}
if (stream->channels == 1)
sample_right = sample_left;
}
/* the room send, with its own low pass */
if (room || ramp_room)
{
float mono = stream->channels == 1 ? sample_left : 0.5f * (sample_left + sample_right);
stream->room_lowpass = flush_denormal(mono + room_lowpass * (stream->room_lowpass - mono));
send[frame] += stream->room_lowpass * room;
room_state = flush_denormal(mono + room_lowpass * (room_state - mono));
send[frame] += room_state * room;
}
/* the direct path, muffled, or as it is; a mono voice's mix bins or pan
split it across the speakers */
if (direct_lowpass || ramp_direct_lowpass)
{
sample_left = flush_denormal(sample_left + direct_lowpass * (stream->direct_lowpass[0] - sample_left));
sample_right = flush_denormal(sample_right + direct_lowpass * (stream->direct_lowpass[1] - sample_right));
sample_left = flush_denormal(sample_left + direct_lowpass * (direct_state[0] - sample_left));
sample_right = flush_denormal(sample_right + direct_lowpass * (direct_state[1] - sample_right));
}
stream->direct_lowpass[0] = sample_left;
stream->direct_lowpass[1] = sample_right;
direct_state[0] = sample_left;
direct_state[1] = sample_right;
if (stream->channels == 1)
{
output[frame * 2] += sample_left * left;
@@ -733,6 +863,9 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns
stream->center++;
}
}
stream->direct_lowpass[0] = direct_state[0];
stream->direct_lowpass[1] = direct_state[1];
stream->room_lowpass = room_state;
stream->current_left = target_left;
stream->current_right = target_right;
stream->current_room = target_room;
@@ -1053,6 +1186,9 @@ static struct
/* the frames the output is delayed by, and the gain each needs */
float delay[LIMITER_LOOKAHEAD][OUTPUT_CHANNELS];
float needed[LIMITER_LOOKAHEAD];
/* how many of them are under 1: none while nothing is too loud, when the
smallest is 1 without looking */
unsigned long limiting;
/* the gain held down to what the frames ahead need, coming back up */
float held;
/* its last LIMITER_LOOKAHEAD values, and their sum */
@@ -1092,12 +1228,18 @@ static void limit(float *output, unsigned long frames)
peak = fabsf(sample[channel]);
limiter.delay[position][channel] = sample[channel];
}
limiter.limiting -= limiter.needed[position] < 1.0f;
limiter.needed[position] = peak > LIMITER_CEILING ? LIMITER_CEILING / peak : 1.0f;
lowest = limiter.needed[0];
for (index = 1; index < LIMITER_LOOKAHEAD; index++)
limiter.limiting += limiter.needed[position] < 1.0f;
lowest = 1.0f;
if (limiter.limiting)
{
if (limiter.needed[index] < lowest)
lowest = limiter.needed[index];
lowest = limiter.needed[0];
for (index = 1; index < LIMITER_LOOKAHEAD; index++)
{
if (limiter.needed[index] < lowest)
lowest = limiter.needed[index];
}
}
if (lowest < limiter.held)
limiter.held = lowest;
@@ -1143,6 +1285,8 @@ static void mix(float *output, unsigned long frames)
if ((!enabled && reverb.level <= 0.0f) || reverb.quiet > REVERB_QUIET_FRAMES)
reverb_clear();
}
/* the players' voices (voice_audio.c), dry, under the limiter */
voice_audio_mix(output, frames);
limit(output, frames);
}
@@ -1178,6 +1322,18 @@ static void audio_log_level(const float *samples, unsigned long count)
}
}
/* the device it plays on (audio.output_device), and when it was looked at */
static char audio_device_name[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
static unsigned long audio_device_read_at = (unsigned long)-1;
/* audio.output_device ("default": the system's; none on Android) */
static const char *audio_device_setting(void)
{
const char *name = config_string("audio.output_device");
return name && name[0] ? name : "default";
}
static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
float buffer[MIX_CHUNK_FRAMES * OUTPUT_CHANNELS];
@@ -1241,7 +1397,7 @@ static void audio_start(void)
spec.format = SDL_AUDIO_F32;
spec.channels = OUTPUT_CHANNELS;
spec.freq = OUTPUT_RATE;
#ifdef HALO_ANDROID
#ifdef HALO_ARM64_GUEST
/* frames per callback: on Android each callback is handed to a thread
that can run the guest (host_sdl.c): 512 left it too little time and
the menus' music broke up, which 1024 does not (about 21 ms at 48 kHz,
@@ -1250,7 +1406,12 @@ static void audio_start(void)
#else
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, "512");
#endif
audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting());
audio_device_read_at = config_changes();
audio_stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec,
audio_callback, NULL);
if (!audio_stream && strcmp(audio_device_name, "default"))
audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
if (audio_stream)
{
SDL_ResumeAudioStreamDevice(audio_stream);
@@ -1268,6 +1429,44 @@ static void audio_start(void)
}
}
/* (the event thread, each frame: sdl_platform.c) audio.output_device
changed (Settings > Audio): the sound goes on on the new device, else the
system's default */
void dsound_sdl_output_device_check(void)
{
SDL_AudioSpec spec;
SDL_AudioStream *stream;
if (!audio_stream || audio_device_read_at == config_changes())
return;
audio_device_read_at = config_changes();
if (!strcmp(audio_device_name, audio_device_setting()))
return;
snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting());
spec.format = SDL_AUDIO_F32;
spec.channels = OUTPUT_CHANNELS;
spec.freq = OUTPUT_RATE;
SDL_DestroyAudioStream(audio_stream);
stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec, audio_callback, NULL);
if (!stream)
stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
audio_stream = stream;
if (audio_stream)
{
SDL_ResumeAudioStreamDevice(audio_stream);
platform_log("audio: playing on %s", audio_device_name);
}
else
{
pthread_t thread;
/* (none at all: the voices drained in real time, as at the start) */
platform_log("audio: cannot open an audio device (%s); sound is silent", SDL_GetError());
pthread_create(&thread, NULL, silent_clock_thread, NULL);
pthread_detach(thread);
}
}
/* ---------- completion */
static void packet_release(struct voice_packet *entry)
@@ -1283,6 +1482,10 @@ static void stream_complete_head(struct sdl_stream *stream, DWORD status, DWORD
struct voice_packet *entry = &stream->packets[stream->packet_head];
XMEDIAPACKET packet = entry->packet;
/* (one not played to its end, flushed: the cursor was its place, and the
mixer may take the next while the lock is let go below) */
if (!entry->finished)
stream->cursor = 0;
packet_release(entry);
entry->finished = FALSE;
stream->packet_head = (stream->packet_head + 1) % MAXIMUM_STREAM_PACKETS;
@@ -1440,7 +1643,9 @@ static HRESULT STDMETHODCALLTYPE stream_flush(IDirectSoundStream *object)
{
struct voice_packet *head = &stream->packets[stream->packet_head];
stream_complete_head(stream, head->finished ? XMEDIAPACKET_STATUS_SUCCESS : XMEDIAPACKET_STATUS_FLUSHED,
/* Cancellation is synchronous. SUCCESS would let the game's
completion callback refill the stream we are stopping. */
stream_complete_head(stream, XMEDIAPACKET_STATUS_FLUSHED,
head->finished ? head->packet.dwMaxSize : 0);
}
stream->cursor = 0;
+7 -3
View File
@@ -16,7 +16,7 @@ Windows it is __stdcall too, and SDL would include windows.h without it) */
#ifndef _WIN32
#undef APIENTRY
#endif
#ifdef HALO_ANDROID
#ifdef HALO_GLES
#include <GLES3/gl32.h>
#include <GLES2/gl2ext.h>
#define GLAPIENTRY GL_APIENTRY
@@ -25,7 +25,7 @@ Windows it is __stdcall too, and SDL would include windows.h without it) */
#endif
#pragma pop_macro("APIENTRY")
#ifdef HALO_ANDROID
#ifdef HALO_GLES
/* OpenGL ES 3.2 (port/android/README.md); tools/android_gl_stubs.py reads
this list to generate the guest's entry points */
/* ANDROID_GL_FUNCTIONS_BEGIN */
@@ -87,6 +87,7 @@ this list to generate the guest's entry points */
X(glBlitFramebuffer) \
X(glDrawBuffers) \
X(glGenRenderbuffers) \
X(glDeleteRenderbuffers) \
X(glBindRenderbuffer) \
X(glRenderbufferStorageMultisample) \
X(glFramebufferRenderbuffer) \
@@ -202,6 +203,7 @@ this list to generate the guest's entry points */
X(glBlitFramebuffer) \
X(glDrawBuffers) \
X(glGenRenderbuffers) \
X(glDeleteRenderbuffers) \
X(glBindRenderbuffer) \
X(glRenderbufferStorageMultisample) \
X(glFramebufferRenderbuffer) \
@@ -265,7 +267,7 @@ GL_FUNCTIONS(GL_DECLARE_FUNCTION)
/* call sites use the ordinary names; gl_functions.c, which defines the
pointers, sees the declarations without these aliases */
#ifndef GL_FUNCTIONS_DEFINE
#ifdef HALO_ANDROID
#ifdef HALO_GLES
#define glGetString halo_glGetString
#define glGetIntegerv halo_glGetIntegerv
#define glCopyImageSubData halo_glCopyImageSubData
@@ -323,6 +325,7 @@ pointers, sees the declarations without these aliases */
#define glBlitFramebuffer halo_glBlitFramebuffer
#define glDrawBuffers halo_glDrawBuffers
#define glGenRenderbuffers halo_glGenRenderbuffers
#define glDeleteRenderbuffers halo_glDeleteRenderbuffers
#define glBindRenderbuffer halo_glBindRenderbuffer
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
@@ -436,6 +439,7 @@ pointers, sees the declarations without these aliases */
#define glBlitFramebuffer halo_glBlitFramebuffer
#define glDrawBuffers halo_glDrawBuffers
#define glGenRenderbuffers halo_glGenRenderbuffers
#define glDeleteRenderbuffers halo_glDeleteRenderbuffers
#define glBindRenderbuffer halo_glBindRenderbuffer
#define glRenderbufferStorageMultisample halo_glRenderbufferStorageMultisample
#define glFramebufferRenderbuffer halo_glFramebufferRenderbuffer
+33 -15
View File
@@ -185,6 +185,8 @@ static unsigned char *png_decode(const unsigned char *data, unsigned long size,
ended when the output is exactly full: all of it is enough) */
if ((result != Z_OK && result != Z_BUF_ERROR) || inflated_size != filtered_size)
goto failed;
/* (each row by its filter, the first pixel's 4 bytes, which have none to
their left, apart; the first row has none above: zeroes) */
for (row = 0; row < height; row++)
{
const unsigned char *line = filtered + row * (stride + 1) + 1;
@@ -194,22 +196,38 @@ static unsigned char *png_decode(const unsigned char *data, unsigned long size,
if (filter > 4)
goto failed;
for (column = 0; column < stride; column++)
if (!above && filter == 2)
filter = 0; /* (up: zero) */
else if (!above && filter == 4)
filter = 1; /* (Paeth of left, zero and zero: left) */
switch (filter)
{
unsigned char left = column >= 4 ? out[column - 4] : 0;
unsigned char up = above ? above[column] : 0;
unsigned char up_left = above && column >= 4 ? above[column - 4] : 0;
unsigned char predicted;
switch (filter)
{
case 1: predicted = left; break;
case 2: predicted = up; break;
case 3: predicted = (unsigned char)(((unsigned)left + up) / 2); break;
case 4: predicted = paeth(left, up, up_left); break;
default: predicted = 0; break;
}
out[column] = (unsigned char)(line[column] + predicted);
case 0:
memcpy(out, line, stride);
break;
case 1:
memcpy(out, line, 4);
for (column = 4; column < stride; column++)
out[column] = (unsigned char)(line[column] + out[column - 4]);
break;
case 2:
for (column = 0; column < stride; column++)
out[column] = (unsigned char)(line[column] + above[column]);
break;
case 3:
for (column = 0; column < 4; column++)
out[column] = (unsigned char)(line[column] + (above ? above[column] : 0) / 2);
for (column = 4; column < stride; column++)
out[column] = (unsigned char)(line[column] +
((unsigned)out[column - 4] + (above ? above[column] : 0)) / 2);
break;
default:
/* (Paeth of zero, up and zero: up) */
for (column = 0; column < 4; column++)
out[column] = (unsigned char)(line[column] + above[column]);
for (column = 4; column < stride; column++)
out[column] = (unsigned char)(line[column] + paeth(out[column - 4], above[column], above[column - 4]));
break;
}
}
free(compressed);
+5
View File
@@ -223,3 +223,8 @@ void memory_watch_forget(void *address, unsigned long size)
page_generation[page] = __sync_add_and_fetch(&current_generation, 1);
}
}
/* page protection sees each write at once: nothing to do per frame */
void memory_watch_begin_frame(void)
{
}
+1 -1
View File
@@ -99,7 +99,7 @@ static long file_add(const char *path, const unsigned char *data, unsigned long
static unsigned char *file_read(const char *path, unsigned long *size)
{
#ifdef HALO_ANDROID
#ifdef HALO_ARM64_GUEST
FILE *file = fopen(path, "rb");
unsigned char *data = NULL;
long length;
+5 -4
View File
@@ -1,8 +1,8 @@
/*
MSVC_CRT.C
Microsoft C runtime functions the game (and its copies of libtiff and zlib)
call that glibc does not provide under the same names, plus the Xbox-path
Microsoft C runtime functions the game (and its copy of zlib) call that
glibc does not provide under the same names, plus the Xbox-path
aware fopen/open family that the game's headers redirect to (see
port/linux/include/stdio.h).
*/
@@ -295,7 +295,7 @@ static unsigned short msvc_to_control_word(unsigned int value, unsigned short wo
return word;
}
#ifdef HALO_ANDROID
#ifdef HALO_ARM64_GUEST
/* AArch64: the rounding mode lives in FPCR.RMode, the sticky exception
flags in FPSR. Precision control and exception unmasking have no
equivalent; the rest of the MSVC control word is only remembered. */
@@ -678,7 +678,8 @@ int _fileno(FILE *stream)
return fileno(stream);
}
/* libtiff maps its POSIX calls onto the MSVC underscore names itself */
/* the MSVC underscore names of the POSIX file calls (the game's libtiff
used them; the builds leave it out: port.json) */
int _open(const char *path, int flags, ...)
{
+63 -50
View File
@@ -520,6 +520,10 @@ long msvc_wtol(const wchar_t *string)
/* ---------- formatted output */
/* the widest field and the longest precision a conversion is given (a
format's own, or an argument's) */
#define WIDE_FORMAT_MAXIMUM_WIDTH 4096L
struct wide_output
{
wchar_t *buffer;
@@ -599,54 +603,32 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis
continue;
}
specification[specification_length++] = '%';
/* flags */
while (*format == '-' || *format == '+' || *format == ' ' || *format == '#' || *format == '0')
/* (the specification handed to snprintf below is built from what
was parsed, bounded: a format may be a map's text, ui strings and
HUD messages among them, so its flags and digits may be any number,
and its width and precision are capped at WIDE_FORMAT_MAXIMUM_WIDTH) */
{
if (*format == '-')
left = TRUE;
specification[specification_length++] = (char)*format++;
}
/* width */
if (*format == '*')
{
width = va_arg(arguments, int);
if (width < 0)
char flags[8];
size_t flag_count = 0;
while (*format == '-' || *format == '+' || *format == ' ' || *format == '#' || *format == '0')
{
left = TRUE;
width = -width;
specification[specification_length++] = '-';
if (*format == '-')
left = TRUE;
if (flag_count < sizeof(flags) - 1)
flags[flag_count++] = (char)*format;
format++;
}
specification_length += (size_t)snprintf(specification + specification_length,
sizeof(specification) - specification_length, "%ld", width);
format++;
}
else
{
while (*format >= '0' && *format <= '9')
{
width = width * 10 + (*format - '0');
specification[specification_length++] = (char)*format++;
}
}
/* precision */
if (*format == '.')
{
precision = 0;
specification[specification_length++] = '.';
format++;
/* width */
if (*format == '*')
{
precision = va_arg(arguments, int);
if (precision < 0)
width = va_arg(arguments, int);
if (width < 0)
{
precision = -1;
specification_length--;
}
else
{
specification_length += (size_t)snprintf(specification + specification_length,
sizeof(specification) - specification_length, "%ld", precision);
left = TRUE;
width = width < -WIDE_FORMAT_MAXIMUM_WIDTH ? WIDE_FORMAT_MAXIMUM_WIDTH : -width;
if (flag_count < sizeof(flags) - 1)
flags[flag_count++] = '-';
}
format++;
}
@@ -654,10 +636,44 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis
{
while (*format >= '0' && *format <= '9')
{
precision = precision * 10 + (*format - '0');
specification[specification_length++] = (char)*format++;
if (width <= WIDE_FORMAT_MAXIMUM_WIDTH)
width = width * 10 + (*format - '0');
format++;
}
}
if (width > WIDE_FORMAT_MAXIMUM_WIDTH)
width = WIDE_FORMAT_MAXIMUM_WIDTH;
flags[flag_count] = '\0';
/* precision */
if (*format == '.')
{
precision = 0;
format++;
if (*format == '*')
{
precision = va_arg(arguments, int);
if (precision < 0)
precision = -1;
format++;
}
else
{
while (*format >= '0' && *format <= '9')
{
if (precision <= WIDE_FORMAT_MAXIMUM_WIDTH)
precision = precision * 10 + (*format - '0');
format++;
}
}
if (precision > WIDE_FORMAT_MAXIMUM_WIDTH)
precision = WIDE_FORMAT_MAXIMUM_WIDTH;
}
if (precision >= 0)
specification_length = (size_t)snprintf(specification, sizeof(specification), "%%%s%ld.%ld", flags,
width, precision);
else
specification_length = (size_t)snprintf(specification, sizeof(specification), "%%%s%ld", flags,
width);
}
/* size */
if (*format == 'h')
@@ -716,13 +732,10 @@ static int wide_format(struct wide_output *output, const wchar_t *format, va_lis
break;
}
case 'n':
{
int *count = va_arg(arguments, int *);
if (count)
*count = (int)output->length;
/* (never written: a format may be a map's text. Its argument
is passed over, as UCRT does with %n disabled) */
(void)va_arg(arguments, int *);
break;
}
case 'd': case 'i': case 'u': case 'x': case 'X': case 'o':
case 'e': case 'E': case 'f': case 'g': case 'G': case 'p':
{
+6 -6
View File
@@ -314,7 +314,7 @@ static void dot_input(struct xgpu_text *text, const DWORD *state, int stage)
}
}
#ifdef HALO_ANDROID
#ifdef HALO_GLES
/* ES samplers have no LOD bias: pass D3DTSS_MIPMAPLODBIAS to the lookup */
#define SAMPLE_BIAS ", texture_lod_bias[%d]"
#define SHADER_VERSION \
@@ -334,21 +334,21 @@ static void sample(struct xgpu_text *text, const struct nv2a_pixel_shader_key *k
{
case _xgpu_sampler_3d:
xgpu_text_append(text, "texture(tex%d, (%s).xyz" SAMPLE_BIAS ")", stage, coordinates
#ifdef HALO_ANDROID
#ifdef HALO_GLES
, stage
#endif
);
break;
case _xgpu_sampler_cube:
xgpu_text_append(text, "texture(tex%d, (%s).xyz" SAMPLE_BIAS ")", stage, coordinates
#ifdef HALO_ANDROID
#ifdef HALO_GLES
, stage
#endif
);
break;
default:
xgpu_text_append(text, "texture(tex%d, (%s).xy * texture_scale[%d].xy" SAMPLE_BIAS ")", stage, coordinates, stage
#ifdef HALO_ANDROID
#ifdef HALO_GLES
, stage
#endif
);
@@ -594,7 +594,7 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
if (combiner_count > 8)
combiner_count = 8;
#ifdef HALO_ANDROID
#ifdef HALO_GLES
xgpu_text_append(&text, "#version %s\n", xgpu_capabilities.shading_language);
if (key->count_samples)
{
@@ -741,7 +741,7 @@ char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key)
xgpu_text_append(&text, "\tresult = vec4(t0.rgb, 1.0);\n");
if (config_boolean("debug.gpu_debug_flat"))
xgpu_text_append(&text, "\tresult = xD0.a > 0.0 ? vec4(xD0.rgb, 1.0) : vec4(1.0, 0.0, 1.0, 1.0);\n");
#ifdef HALO_ANDROID
#ifdef HALO_GLES
if (key->count_samples)
xgpu_text_append(&text, "\tatomicCounterIncrement(visible_samples);\n");
#endif
Loaded 100 of 510 files, more files were not shown because too many files have changed in this diff. Show more