Skip to content

Merge Testing - #524

Merged
cybersecurity merged 129 commits into
mainfrom
testing
Oct 29, 2026
Merged

cybersecurity merged 129 commits into
mainfrom
testing

Conversation

@cybersecurity

Copy link
Copy Markdown
Collaborator

No description provided.

steverice and others added 30 commits October 19, 2026 10:08
Refresh intact and broken surfaces through numbered co-op events so a host checkpoint revert restores client collision and drawing flags as well as vitality. Retain immediate breaks, force a refresh after reverting, retry a full event queue, and periodically repair lost or reordered chunks. Raise `HALO_PORT_NETWORK_VERSION` to 33 because clients must interpret the new surface-event type.

Verified the existing restored-pane failure before the change. All six native-width production-source cases pass, covering rewind state, event ordering, periodic repair, full queues, bounds and host/client roles. These use separate fake host/client state; full engine and transport validation remain pending.
Add `network.coop_team_wipe` and the TEAM WIPE choice in Co-op Options. Automatic mode rewinds private games and preserves world progress in public games; explicit revert and respawn choices override visibility. Passwords and temporary listing outages do not change the selected policy.

Record player identities and machine assignments only after a successful checkpoint save and clear them for a new map. Refuse wipe reverts when membership differs, including a departure before its quit timer expires, and log the fallback to existing position-only respawn. Keep the 92-tick delay and scripted-failure/manual revert paths.

All seven native-width host-policy source cases and six surface source cases pass, as do generated-menu and wire-format checks. These are fake-world source tests, not full engine or transport validation. Updated the local death fixture for the new policy call and its existing stale respawn signature; its regression rerun, upstream 32-bit harness and full builds remain pending.
Retain the next surface chunk until it has been enqueued, and restart the pass after a revert, map load, or BSP change. Verify sustained one-slot capacity, partial delivery, lifecycle resets, and surface mask bytes through the actual batch encoder and client consumer. Native source checks, menu generation, death and checkpoint timing, network-format checks, and compile-time 32-bit ABI assertions pass; 32-bit runtime tests remain covered by the Linux harness.
Exercise attachment and sound reset counts and verify that batch handling clears after consumption. This covers the state transitions and fixes Clang 22 unused-but-set-global failures without suppressing warnings.
Keep the production-source policy, surface repair and packet-byte cases in the normal 32-bit harness. Move the generated menu checks into the existing co-op menu suite and remove duplicate native runners so CI needs no additional command.
NVIDIA's vsync waits about a second for each of two frames it has already
queued once the compositor stops releasing buffers, and that wait is on the
thread that submits sound. Unfocused presents now use swap interval 0 and
pace at the display refresh. While a present sits in the driver, another
thread finishes the packets the mixer has played and queues the next of the
same sounds. A full sound update from there deletes the soundtrack: a
looping sound is dropped unless the game refreshed it since the last update.

Checked with a hidden Hang 'em High host, and by alt-tabbing a focused
game: the music keeps its place and stays continuous.
CMake 4 no longer seeds the find paths from `CMAKE_SYSROOT`, so
`find_library` looked only in `/usr/local/lib` and never in the system
root's `usr/lib/i386-linux-gnu`. Every library the checks look for was
missing and the portable build's SDL configure failed with X11 off.

The toolchain file now sets `CMAKE_FIND_ROOT_PATH`, `CMAKE_SYSTEM_PREFIX_PATH`
and the system's library and include paths, so SDL's checks find X11 and
ALSA in the Debian i386 system root again.
`display.show_fps` (Video Setup's SHOW FRAMES, off by default) turns on
the game's own frame counter, which the Xbox version showed with its
`display_framerate` console variable: the frames a second at the bottom
right of a game, counted over half a second, green from 30 and red below.
`port/linux/game/frame_rate_display.c` reads it when the setting changes
and sets `display_framerate`, and `interface.c` calls it before the
counter draws, so a menu's change shows in the next frame.

Video Setup gains rows of its own on Android, where the console is not at
hand: RESOLUTION (`display.resolution_scaling`) and FRAME RATE LIMIT
(`display.max_fps`), both of which the Android build now honours, and
SHOW FRAMES on every platform. `display.resolution_scaling` defaults to
`"original"` on Android and `display.max_fps` applies there, so
`port_config.c` declares both for every platform and gives the former the
Xbox's 640x480 as Android's default.

The Video Setup XML is written again by `tools/port_settings.py` and
matches its output; the two READMEs document the settings.
…on_scaling = "native"

The Android build always drew the Xbox's 640x480 and let the display blit
scale it up. `display.resolution_scaling = "native"` now draws at the
display's own resolution, as the desktop builds do: the render targets
take the display's size and the scale is the display over the game's
picture, so the game still works in its 480 lines.

The setting stays `"original"` by default on Android: a phone's GPU has
less to spare than a desktop's. The picture keeps its shape when the
display is narrower or wider than the game's width, and the display blit
letterboxes it.
The Android build skipped the frame rate limit entirely, so with vsync off
it drew as fast as the GPU allowed and `display.max_fps` did nothing.
`sdl_platform.c` now paces the frames on Android too: the guest's SDL
bridge has no `SDL_GetTicksNS` nor `SDL_DelayPrecise`, so the swap reads
musl's monotonic clock and sleeps with `nanosleep`. The refresh rate that
`display.max_fps = 0` doubles comes from `HALO_DISPLAY_REFRESH`, which the
app passes (`port/android/host/host_main.c`), since the guest cannot ask
SDL for the display mode.

This gives a phone the same ceiling every other platform has, and less
heat and battery drain in the menus and on a slow GPU.
…e while playing

The touch controls hid only for a controller the app's own device list
knew about, which misses a pad paired after the game started. The game
now says whether SDL sees a controller of its own: `touch_input.c` adds
`_touch_scene_controller` (from `sdl_gamepads`, which `xinput_sdl.c` now
exposes) to the scene, and the overlay hides for it. The overlay also
hides for a physical mouse or keyboard, which an Android device with a
touchscreen can have. The web page's touch.js reads the same scene bit.

A physical mouse is captured while playing and released in the menus, as
on the desktop, so its pointer shows where the menus need it. The SDL
mouse patch reports the buttons held in an ACTION_MOVE, so a held button
(a shot, a throw) no longer ends as the pointer moves.

The Android README describes both.
A graphics bug's report or two builds compared should say which renderer, map
and mode were running and at what frame rate, without reading numbers off a
screen or asking for them. display.stats_log writes one line a second to the
port's log (logcat on Android):

  STATS fps=120.2 mode=Slayer map=bloodgulch gpu="Adreno (TM) 735" res=1068x480

The frame rate is counted over each second, the mode is the game engine's as
the scoreboard names it, the map is the loaded one and the renderer is the
driver's own text (glGetString once the context exists, d3d8_gl.c). Off by
default and read again when the file changes; nothing is drawn and nothing is
sent anywhere.

It does not add a counter setting: the frame rate counter's own setting is
#437's (display.show_fps), and this only reads and reports.
The plasma impact's "plasma electric bolts" particle lives one tick: one sprite, a 1/30 s span, no loop. At 30 Hz its first update is a whole tick, so it dies in the game_frame that created it and render_particles never draws it. With frames between the ticks, particles_update steps it a fraction of a tick at a time, and every frame of its tick drew it flying along its velocity: tall lightning columns at plasma impacts at any rate above 30 fps. "flash c generic", "flash h ar" and "light ring expand" had the same early draws.

particle_drawn now holds a particle back from render_particles until a tick's update has passed: drawn after an update of a whole tick (a fixed timestep, as on the Xbox) or once its age is past a tick. A particle that animates once a tick ("flash c generic med") is drawn from its first animation step instead, since that step, not its age, decides the one tick it shows. Creation, updates and random draws are unchanged; only drawing is gated, and a fixed timestep draws exactly what it drew before.

tools/harness/tests/test_particle_draw_gate.py steps the real animation code at 30 to 1000 Hz.

From VISR (https://github.com/steverice/visr), commit 7f44ddf.
display.hdr kept the light the game adds past white in a half-float back
buffer and tone mapped it, but its curves did not suit what Halo draws.
"neutral" (the default) rolls off by the largest channel to keep the hue,
so an explosion or plasma bright enough that the Xbox's 8 bits clipped it
toward white stayed deep red or blue; "aces" is a fit made for SDR; and an
HDR display's white and peak could only come from Windows.

- display.hdr_tone_mapping "gt7", now the default: Polyphony Digital's
  GT7 tone mapping operator (SIGGRAPH 2025, MIT licensed), written again
  in GLSL in xgpu_post.c's tone mapping pass. The light goes to Rec.2020
  at its brightness in nits; each channel goes along GT7's curve to the
  display's peak, and is blended with that curve's brightness at the
  colour's own hue in ICtCp, whose chroma fades out as the light nears the
  peak, so the brightest light turns to white. Without an HDR display it
  is GT7's SDR (white at 250 nits, 1.0 at it): Halo's white comes to 0.92
  of what it was (gamma encoded), its midtones stay.
- display.hdr_paper_white and display.hdr_peak_brightness (desktop): with
  display.hdr_output on an HDR display, white (the HUD, the menus, the 3D
  view's) and the brightest light in nits; 0, Windows' own, as before.
  Extra Effects has PAPER WHITE and PEAK BRIGHTNESS rows after HDR
  DISPLAY, and its HDR rows' help is said plainly.
- port/third_party/gt7_tone_mapping: Polyphony's sample, unchanged, with
  its licence and its upstream's address and SHA-256; it is not compiled.

Checked: the GLSL, compiled as C++ over stand-ins for GLSL's types,
against Polyphony's C++ for 200,000 colours and peaks from 250 to 10000
nits: within 0.06% of the peak in HDR, 0.03% of white in SDR (float
rounding, largest where the chroma fade starts); their own test colour
the same to five places. glslc compiles the pass as GLSL 4.50 and ES
3.10. Android and web compile the changed files with no warnings; the CI
test suite passes. On the Android emulator (ANGLE, half floats),
Blood Gulch hosted with HDR and "gt7", "neutral" and HDR off: the pass
builds and draws, no errors. Not seen on an HDR display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ITU-R BT.2408 puts SDR content's white at 203 nits in HDR television, as
the BBC's SDR to HDR conversions do; PAPER WHITE now offers it between
200 and 250, and the README says what it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The curve was set only in config.toml. Extra Effects now has a TONE CURVE
row after HDR DISPLAY (every platform): GT7, NEUTRAL, ACES or REINHARD,
taking effect from the next frame, so they can be compared in play. The
screen is written again from tools/port_settings.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
display.hdr_peak_brightness and display.hdr_paper_white could be set only
as numbers, or left to Windows. HDR games calibrate them by eye, with
patterns brighter than white, which the menus cannot draw: they are drawn
at SDR levels, 1.0 being paper white.

- Extra Effects has HDR CALIBRATION (desktop), shown only with
  display.hdr_output's floating-point window on a display whose HDR is on.
  Its screen has PEAK BRIGHTNESS, then PAPER WHITE, with Defaults (Auto),
  OK and Cancel as every settings screen has.
- While the screen is drawn, the display takes the values its rows show
  (the menus' white follows PAPER WHITE at once), and after the back
  buffer is presented, d3d8_gl.c fills the pattern of the row chosen into
  the window in scRGB, over the menus, between the rows and the help. The
  peak's: a square at the value, with a ring in it at 10000 nits, which
  disappears into the square once the square is as bright as the display
  goes. Paper white's: a grey scale up to white, beside a white square with
  a ring at the peak. Cancel goes back to the settings' values.
- xgpu_post.c's xgpu_post_fill draws them: a program of one colour over a
  rectangle, or a ring in it, in a set of its own (a failure leaves HDR on).
- menu_functions.c's "port hdr calibration" (the screen's list, as it is
  drawn) passes the row chosen and the values shown; "port settings help"
  hides Extra Effects' row without an HDR display.
- PAPER WHITE and PEAK BRIGHTNESS, on Extra Effects too, step finely
  enough for the patterns (paper white by 10 nits, then 20, with 203;
  the peak by 25, 50, then 100 nits up to 2000), so both screens show
  the value chosen.

Checked: ninja android, ninja web and ninja windows (cross-compiled with
clang against MSVC) build, with no warnings in the changed files; the fill
program compiles as GLSL 4.50 and ES 3.10 (glslc); the settings screens'
XML is the generator's, and CI's pytest suite passes. Not seen on an HDR
display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Snapdragon (Galaxy S22+, Adreno 730) characters and some level
geometry were drawn as long spikes. Vertices and indices were written with
unsynchronized maps into the frame's one stream and index buffers, which
draws earlier in the same frame still read. glBufferSubData into those
buffers drew correctly but made the driver copy the whole buffer on every
upload.

Android now takes the web build's pooled buffers: each draw's uploads go
into a buffer that no queued draw reads, written with glBufferSubData and
reused only after the ring's frame fence. Tested clean and at speed on the
S22+; the web build is unchanged.

(cherry picked from GulchCE commit 5ec4c8d)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8d9d483)
Android has no mirror of the contiguous window, so every draw streamed its
vertices and indices: a high-poly MCC marine sent its whole body (about
520 KB of vertices and 200 KB of indices) on every pass, for every marine
in view, every frame.

The buffers rasterizer_vertex_buffer_new and rasterizer_triangle_buffer_new
make (MCC, Custom Edition and mod models, MCC BSPs) are written once. On
Android each now gets a GL buffer of its own, filled the first time a draw
reads it and never written again, so no write tracking is needed and no
queued draw can read a buffer while it changes. Releasing the resource
drops its entry at once; the GL buffer is deleted at the next Present, and
the GL state cache forgets it. Everything else still streams through the
pooled per-draw buffers.

(cherry picked from commit 462f09e)
(cherry picked from commit d4ef6a8)
The static GL buffers covered only geometry made through
rasterizer_vertex_buffer_new and rasterizer_triangle_buffer_new (Custom
Edition and mod models, a Custom Edition BSP's vertices). An Xbox map's
model vertex and index buffers are in its tags, and a BSP's vertices and
lightmap vertices in the BSP, registered from their headers with no sizes,
so they still streamed on every draw.

Register them as ranges instead: an Xbox map's model vertices and indices
from the first buffer to the end of its tags, a BSP's vertices from its
first buffer to the BSP's end, and the BSP itself for the indices its
surfaces draw (a Custom Edition BSP's too). The cache files hold that
geometry and nothing writes it while it is registered; each range is
uploaded the first time a draw reads it and dropped when the map or BSP
unloads. The registry keeps vertices and indices in separate lists, so a
range can be both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1a4cd02)
static_buffer_range did not check that glBufferData allocated the
buffer's storage. On GL_OUT_OF_MEMORY it still returned the buffer and kept
it, so later draws skipped both the upload and the streaming fallback and
read a buffer with no storage. The errors queued before the upload are now
taken first, so that the upload's own is read; on an error the buffer is
deleted, the state cache forgets it, the range is marked so it is not tried
again, and it streams as before. A release and a later add try it again.
One line is logged.

Checked with the real function and an injected allocation failure
(tools/test_android_static_geometry.py, next commit), which fails on the
code before this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit af5c165)
tools/test_android_static_geometry.py compiles the code of d3d8_gl.c as it
is, taken from the source as tools/test_android_mirror_uploads.py does,
against fake GL:

- pooled uploads (web_chunk_get, stream_reserve, stream_upload,
  index_upload, web_chunks_begin_frame): over 60 frames of varied draws,
  and uploads past a pool class and past the ring buffer's size, no upload
  writes a buffer that a draw of the last three frames reads, nothing goes
  through an unsynchronized map, and the pools are reused (671 buffers for
  about 5,000 draws);
- the static cache: a range is uploaded once and read with the right
  bytes, released ranges are not drawn and their buffers are deleted at
  Present, reloads and address reuse upload the new bytes under a new
  serial, an allocation failure streams the range without retrying, and an
  Xbox BSP's vertex, lightmap and index ranges register and unload.

Negative controls: the allocation case fails on the code before the
previous commit, and the pooled case fails when a draw may write the
buffer an earlier draw read. Run with clang locally (pytest was not
installed here, so the module was driven directly); added to CI's tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 210a5bd)
Detect the SoC once before the game starts. Snapdragon and unknown devices
keep geometry and CE texture fixes, MediaTek keeps CE texture fixes, and
other identified vendors use their normal paths. Geometry includes the
prerequisite pooled uploads and static buffers from PR #455. Remove the
blanket per-draw state refresh while retaining resource invalidation.

Adapt the change to OpenCE main, preserving its workflow and app identity;
exclude all fork-specific and MCC integration and associated test cases.
Include production-code regressions and Android policy documentation.
Self-review covered startup/host imports, policy gates, resource lifecycle,
platform guards, upstream texture changes and prerequisite geometry hooks.
80 focused tests passed locally, including both policies, cache allocation
failure/reuse/unload, unknown/conflicting identity, and desktop/web controls.
Full platform builds and Android release packaging are delegated to CI.
No network messages or simulation behavior change.

Player-Note: Android selects geometry and texture fixes automatically for the detected chip.

(cherry picked from commit 97fb4ec)
Linux CI's newer clang diagnoses the Android policy variable in the WebGL
fixture as set but unused. Compile that variable and the Android-only test
case only for Android; retain the separate WebGL pooled-upload controls.
Rendering code is unchanged. Self-reviewed the platform guards and reran
231 local regression tests with LLVM 23; four optional checks skipped.
…gh a filter

The renderer asks, for each texture it binds (about 2000 a frame in a
co-op battle), whether it is one of the menus' bitmaps or one of the
high-res HUD's placeholders, and each question searched the whole list
(a hundred bitmaps and more). A bit for each registered texture's Data in
a small filter answers the usual "no" without the search; a texture is
still found exactly when it is registered.

tools/harness/tests/test_texture_lookup_filter.py checks registering,
replacing and forgetting against a model, with three negative controls;
its benchmark (160 bitmaps, 24 placeholders): 41.4 ns a texture before,
3.7 ns after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
collision_get_features_in_sphere marked each object of a cluster before
testing it on the packed copy of the bounding spheres
(object_bounds_cache.c). The mark is in the object itself, so every far
object was still read, and written, once per query: the scattered read
the cache is there to save. The cache's test now comes first. The same
objects are passed over and the same features gathered, in order: an
object out of reach that is met again in another cluster is out of reach
again, and one in reach is marked the first time, as before. Only the
marks of the far objects, which nothing reads outside the query, are no
longer written.

Checked: the harness's object bounds cache test, with a new case
(far-unread) that counts the objects marked: 1240739 without the cache,
265301 with it (1240739 before this change, so the case fails on main).
b30, co-op network test, enemies x32, 5 pairs: fps with 2000 draws a
frame or more 177 -> 183 (+3.4%, every pair), worst 60 frames 112 -> 117.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The periodic and transition functions (shader animations), the objects'
functions and others take fmod(x, 1.0) many times a frame. On Windows
that is the UCRT's x86 fmod, which saves and checks the x87's state and
runs fprem through two dispatchers: in b30's busy scene at 32 times the
actors, _trandisp2 and _ctrandisp2 alone were 1.9% of the game thread.
halo_math.h now gives fmod a fast case for a divisor of 1.0 and x within
a long: x less its truncation, which is exact, with a zero taking x's
sign as fmod's does; anything else goes to the C library's fmod. The
result is the same to the bit, so nothing machines must agree on changes.

Checked with a harness test (tools/harness/tests/test_fmod_fraction.py)
comparing halo_fmod with the C library's fmod to the bit over the edges
(zeros, subnormals, the long's limits, infinities, NaN) and 400000 values,
with three negative controls (the zero's sign, the range, the divisor).
chaos.py on b30 at 32 times the actors, 60 s, 4 alternating pairs against
b1ef7f7: 2000-draw frames 150 -> 156 fps (+3.3%), mean 224 -> 227, the
new build ahead in every pair; the sampler no longer finds the
dispatchers or fmod in the game thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cybersecurity and others added 29 commits October 29, 2026 23:27
xinput_sdl.c asks network_vote_pad_open on every read of the
controllers, the first during input_initialize, before the game's state
exists: cinematic_in_progress and the vote's status read it, and every
build crashed at the start (a dedicated server too). It answers FALSE now
unless a network game is in progress (game_time_initialized,
game_in_progress), as controller_remap_game.c asks.

Also includes limits.h in the object bounds cache's harness test, whose
collision code (#471) uses LONG_MIN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…yle/Auto Button Detection into its own menu inside Gamepad menu

Conflicts: the menus' function names and Network Setup's rows kept both sides; the vote's F1/F2 and D-pad block kept before the chat's new condition (and the vote's D-pad read from the first controller's own pad, halo_chat_pad_buttons_for_port(0)); the vote's D-pad hold-over moved to where the chat's pad is now read for every port; the Graphics menu regenerated with tools/port_settings.py.
….menu_cursor

#473 made the arrow medium (24 of the menus' 480 lines) by default, smaller than Halo PC's 32: large is the default now, as the README says the pointer is sized as Halo PC's was. And display.menu_cursor gets its row in the README's settings table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…peak and paper white (after #464)

Conflicts kept both sides (Network Setup's rows and HDR Calibration's; the menus' data functions; xgpu.h's static buffers and xgpu_post_fill). PAPER WHITE keeps 250 among its choices (the new range dropped it: a saved 250 would match none), and the menus are regenerated.
…nt keyboard player, and pause menus

Conflicts: the lobby's Y is the 4-way join screen's now, so the map vote's screen opens on X (posted as a vote begins, and the lobby's X while it runs); both menus' data functions kept; the menus regenerated.
…he fixups header

#506 declared widget_instance_render_recursive's extra and design_right after a statement (C89 wants them first), and pc_menu_screen_reads_every_controller and pc_menu_screen_routes_by_controller with externs in ui_widget.c itself: they go in halo_linux_source_fixups.h, as the port's other functions source/ calls do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s START NOW

Conflicts: the lobby countdown's harness test keeps #502's dedicated server cases and this PR's START NOW cases, both docstrings' clauses, both function lists (network_game_server_update_countdown once) and both mutation dicts.
The comments of particles_last_update_dt, particle_drawn, particle_age_drawn and its tolerance (a C++ comment before), the include and the prototype get port: markers, as source/ changes are marked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…frame rate, the mode, the map and the renderer

Conflicts: display.shader_cache and display.stats_log kept side by side in port_config.c and the README's table.
stats_log_mode took a NULL variant for the menus, but game_engine_get_variant returns the global variant, never NULL: the menus and the campaign came out as Game or the last gametype. No game in progress is the menus now, and a game with no multiplayer engine running the campaign (or co-op).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
….max_fps

Brings #437 and #438 too (this branch contains both). Conflicts: display.frames_in_flight and display.show_fps kept side by side, and the README's table keeps testing's rows (frames_in_flight, menu_cursor) with show_fps; interface.c keeps display.stats_log's include and tick beside the frame rate display's; the Android README's touch controls row stays testing's until #440. The video settings menu regenerated with tools/port_settings.py (main's generator changed since), and SHOW FRAMES's label loses its trailing spaces.
…ard, and capture the mouse while playing

Conflicts: touch_input.c keeps testing's touch_input_screen_buttons beside this PR's touch_controller_connected, and the scene's menus test keeps testing's chat controller check with this PR's controller bit. touch_controller_connected counted a controller only past the first SDL gamepad, but the touch controls are none, so one late paired pad, the case it is for, went unseen: one counts now, on Android a recognised one only (not the generic gamepads a phone's own keys show as).
#440's isKeyboard took any device with a keyboard source, and most phones list their power and volume keys as one (gpio-keys): on "auto" the touch controls hid on most phones. It asks for an alphabetic keyboard now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#506's debug.virtual_gamepads attaches SDL virtual joysticks, which the Android host does not give its guest (SDL_AttachVirtualJoystick and the rest are not among its imports): ninja android failed to link. They are the desktop's test tool, so the Android build attaches none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntrols

#440's row for input.touch_controls, which the merge of #439 (whose branch carried the older text) left out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s focus

Conflicts: the menus' cursor and platform_swap_interval kept side by side; the swap sound thread and refresh_period_ns guarded for the desktop alone (#439 took frame_interval_ns out of the Android guard this branch closed); platform_video_swap's pacing variables declared for every build (#439 paces Android too) and the focus and vsync ones for the desktop, whose unfocused-window pacing is now guarded off Android as well.
dsound_sdl.c's header said packets complete only from the game's thread. #431 runs DirectSoundDoWork on the swap sound thread too, while the game's thread is blocked in the driver and waits for it: never at once with the game's, as the comment now says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er team wipes

Conflicts: network_coop.c keeps testing's config_integer and halo_server_mode with this PR's p2p_hosting_listed; Co-op Options keeps ALLY FRIENDLY FIRE and adds TEAM WIPE, nine rows now, which Server Setup's screens space 29 apart to clear the help (more than eight rows only; its test counts nine); the co-op menu test keeps enum_with and imports port_settings; the menus regenerated.
…et sv_set change them

#421's "auto" asked p2p_hosting_listed, a player's lobby's PUBLIC choice, which a dedicated server never makes: a public halo --server co-op game reverted the checkpoint as a private one. A dedicated server is public with server.public and internet play now (network_coop_hosting_public). network.coop_team_wipe joins the settings sv_set and PUT /api/settings change (its choices auto, revert, respawn), the simulator's, and docs/dedicated_server.md's lists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#511's controller_remap_game.c and controller_glyphs.c called datum_get with no prototype in scope, an implicit int for a pointer: the build's LTO may miscompile such a call. They include memory/data.h now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host's notices (the MOTD, the votes', the AFK warning, a kick's, the update's) reached a client's console as console_warning lines, an important kind, which game.console_log's default since it was "none" (429f7c8) keeps off the screen: players saw none of them. They are the chat's notice lines now, in its notice colour, broken into as many lines as the chat holds (network_chat_host_notice), and the host's own players see its notices so too; with the chat not shown (display.text_chat false), a red console line whatever game.console_log says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ame its settings

server_director_first_level gave the network game no level for a co-op entry, so it was set up on the game's stock default, carousel: a server whose maps had none (or none this build reads) failed to precache it in the lobby, a fatal error, and a rotation of co-op entries alone restarted for ever. The entry's own level is the first now, a co-op one's campaign map too, which server_apply_entry makes the co-op game.

And a dedicated server's co-op entry applies network.coop_friendly_fire, network.coop_player_collisions and network.coop_ally_friendly_fire, which only the player's Server Setup had (ui_widget_port_cooperative_settings_apply, both now): collisions stayed on, the game's friendly fire was always on, and allies (Keyes) could always be hurt. It sets server.max_players again after choosing the level, which holds a co-op game to 16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main_save_map_private waited, as the Xbox's game did, until game_safe_to_save held of every player at once: none dead, none in the air, none seen, no vehicle moving near any. Among a network game's players one of them nearly always was not, and a level's checkpoint gave up after its 8 seconds: the Maw's before its Warthog run never came, and a failed run took the team back to the bridge. In network co-op the host now saves once one living player on the ground is out of the enemies' sight, as the scripts' own test already took it (coop_scripts_safe_to_save); a dead player has no spot noted and comes back beside the others.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SHOW FRAMES (#437, #439) made the desktop's Video Setup eleven rows at the 30px step, the last of them over the help text (from 364): SHOW FRAMES moves to Graphics, with the HUD's rows, and Video Setup's ten rows step 28px with the help below them at 358 (before, its last row already reached 7px into the help).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
org.opence.network-version, from the image's own halo --version: opence-fleet reads it from the registry before it updates a server into a new build, to wait for the game's end when the network version changes (the players' games cannot join a server of another) and for the server to be empty when it does not, as a lone server's server.auto_update "install" does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_profile_keyboard copies xinput_sdl.c's keyboard_gamepad, which #506 made set keyboard_join_down from the globals of the 4-way ADD PLAYER screen: the test now copies their declaration too. test_coop_wipe copies network_coop_wipe_reverts, which asks network_coop_hosting_public now (#421's fixup): the test copies it too, stubs halo_server_mode, and checks that a dedicated server counts as public with server.public and internet play, and not without the internet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings main's #516 (touch controls), #518 (the level's flashing), #519
(split screen's fog), #520 (the updater's file limit) and #521 (Android's
resolution: original, 75%, 50%, 25% or native).

Conflicts: #521 does what this branch's #438 did (Android drawing at the
display's resolution) and more, so display.resolution_scaling is main's
throughout: d3d8_gl.c's scale, port_config.c's default and choices,
tools/port_settings.py's one row for every platform (in place of #438's
Android row), and the READMEs' rows; the Android README keeps
display.stats_log's row (#457) beside it. The menus are regenerated with
tools/port_settings.py.
@cybersecurity
cybersecurity merged commit bc8784d into main Oct 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.