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>
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
@@ -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`).
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = ¤t_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(¤t_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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 411 KiB |
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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 |
@@ -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 |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 930 B |
@@ -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">
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -223,3 +223,8 @@ void memory_watch_forget(void *address, unsigned long size)
|
||||
page_generation[page] = __sync_add_and_fetch(¤t_generation, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* page protection sees each write at once: nothing to do per frame */
|
||||
void memory_watch_begin_frame(void)
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ...)
|
||||
{
|
||||
|
||||
@@ -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':
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||