Repository navigation
Testing - #562
Merged
Merged
Testing#562
Conversation
conftest.py's sessions() plays the scenarios side by side from a thread pool, and launch() puts a copy of the game in each scenario's folder. On CI dist/ and /tmp are on different file systems, so os.link fails and shutil.copy writes the copy. If another thread forks while that copy is open for writing, its child holds the descriptor until it runs its own program, and starting the copied game in that time fails with OSError: [Errno 26] Text file busy. That failed all five obstacle tests in CI run 40916137686 (PR #521). launch() now copies the game and starts it under one lock, so no other launch forks while a copy is open (Popen returns only once the child has run the game). The forks of generate()'s tools in other threads are not under the lock, so the start is tried up to five times, a tenth of a second apart, when it fails with ETXTBSY; any other error is raised at once. Checked on Windows, where the scenarios skip without a Linux game: the tests still collect and skip, and a stand-in for Popen showed the retry (two busy failures, then started), giving up after five, other errors raised at once, and eight threads each getting a whole copy. Not yet run on Linux; CI's Linux job runs the scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The main-menu UI map has a live player but its root widgets belong to NONE, so the per-player widget check incorrectly treated it as gameplay. Keep physical navigation there while retaining per-player pause ownership. Give custom layouts a separate Cycle Camera action in every gameplay camera context. Keep timing and release guards in the remapper and pass the result through the director's existing selection and client-permission checks. Save v3 layouts with 26 actions; accept older layouts without overwriting existing debug bindings. Default layouts and network contracts are unchanged. Add production-code regression coverage for menu ownership, camera entry and exit, rebinding, clearing, migration, timing, per-port state and lifecycle. Keep current keyboard/mouse camera fixes and update their harness stub. Register the integration suite in CI and update the controls documentation. Reviewed the combined implementation, interfaces, state lifetime and tests. On OpenCE testing: all 25 controller/HUD pytest cases and all 12 startup/ camera harness cases and negative controls passed with 32-bit clang. Diff whitespace checks passed. The unchanged upstream web suite has Windows path and POSIX-socket limitations; full platform validation remains for CI. No physical-controller test. Player-Note: Remapped controllers keep D-pad and Back working in the main menu. Player-Note: Bind Cycle Camera to enter and leave flying-camera mode with custom controls. (cherry picked from commit f1e6ec0)
A worn stick that drifts needs a larger dead zone than the game's own (9000 of 32767, about 27%), and a player who wants finer aim a smaller one. Each stick's dead zone is now a setting, input.left_stick_dead_zone and input.right_stick_dead_zone: -1 (DEFAULT, the game's own) or 0% to 50% in steps of 5. A new STICK DEAD ZONES screen under Gamepad Setup sets them, after BUTTON REMAPPING and BUTTON APPEARANCE. - source/input/input_xbox.c reads each stick's dead range from halo_stick_dead_range (port/linux/src/xinput_sdl.c) instead of GAMEPAD_STICK_DEAD_RANGE, which stays the default. - The check that the right stick, not the mouse, aims last uses the right stick's dead zone when it is set below that check's deflection. - On Android and the web the touch controls' stick goes through the same path, so the left setting applies to it too. - Gamepad Setup's screen is written again by tools/port_settings.py: eleven rows at its 27px step would put the last in the help, so it takes a 25px step and Video Setup's help place. Nothing the machines send each other changes: the dead zone only shapes a player's own input before it is predicted and sent. Checked: the settings screens' generator and the menu files are the same on testing as where this was written, so the generated XML is unchanged; the Windows build; the harness against testing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A row being timed showed the same dash as one never probed, and a closed or live game showed CLOSED or LIVE for the whole timing. p2p_listing gains a probing flag, taken from the game's probing state in game_listed, and the ping cell shows ".", ".." and "..." stepping every 300 ms for a game with no ping whose host is being timed. A known ping stays shown while it is timed again, FAILED still comes first, and a master server's region, CLOSED, LIVE and the dash show when the host is not being timed. The dots have no colour. The cell's text moves into browser_ping_text so that the harness test tools/harness/tests/test_browser_ping_text.py can take it from the source. Checked: ninja linux builds and links; test_browser_ping_text.py passes (8 tests, four of them negative controls each failing its own case).
update_probes walked the games in slot order, which has nothing to do with the screen, so pings filled in scattered over the list. The browser now tells the lobby the games it shows, in its order (p2p_lobby_shown, each frame, after its sort, tab and Filters), and update_probes starts the first of those that want a probe, then the next, with the same cap, refusals, PROBE_REFRESH, token and master server checks as before. Games the list hides (Filters, the other tab) are still timed, after the games shown. A column title chosen mid-probe orders the hosts timed next; probes under way finish where their rows moved to. There is no ping column to sort by. The order comes from the menu rather than from network.browser_sort because the list is filtered before it is sorted: sorting the lobby's games the same way put games the list hides ahead of the rows on screen: on macOS against the live list, sorted by name, hidden "[C] ..." games held all four probe slots for the first 15 s. Each game is started at most once a pass: a refused game stays wanted, so the top game would otherwise be picked until the refusals ran out (always on the web, where p2p_probe_start refuses, and while joining a listed game). No probe starts while listings wait to be verified, so the first wave after the browser opens follows the list, not the order the listings were verified in; a listing verified in the pass the probes start is not shown yet and goes last. tools/p2p_lobby_check.c checks the order, a re-sort, a hidden game last, a refused host passed over, and the wait. Checked: ninja linux builds and links; p2p_lobby_check passes (0 failures), and each new check fails when the code it covers is removed.
REFRESH asked the hosts for their listings again and timed every ping again, but with games listed nothing on screen changed: a known ping stayed shown through its re-timing, came back the same on a steady link, and the line below the games kept the chosen game's details. The button looked dead. REFRESH now marks each game it times again (refreshing, taken into p2p_listing as probing is), and the mark comes off when that game's probe ends or the game goes. A master server's game is not timed, so it is not marked. A marked row shows the moving dots, uncoloured, until its new ping lands, and the line below the games says "Refreshing..." while a listed game is marked, for 10 s at most from the press. The re-timing every two minutes is not marked, so its numbers stay shown. "Refreshing..." is a browser word, added last to port_settings.py's BROWSER_WORDS, and the join screen's menu file is written again with it. Checked: ninja linux builds with no warnings in the files changed; test_browser_ping_text.py passes (10 tests), its negative controls failing their cases (dots for every probe fails "number", dots without the mark fails "refresh"); p2p_lobby_check passes, and fails when REFRESH marks nothing, marks a master server's or a password's game, or the mark outlives its probe; test_linux_port.py with test_browser_ping_text.py passes (25 passed, 4 skipped: they need the maps).
A rocket launcher's melee was drawn at 30 Hz at any frame rate. The first-person pose was blended between ticks only while no node moved more than 0.25 world units relative to the camera in a tick, a limit meant to tell another weapon's skeleton from this one moving; the rocket launcher's melee (and other wide, fast animations) moves further, so the whole pose snapped every tick of the swing. The pose now remembers the weapon and the animation graph it was of, and blends only from the same weapon's and graph's pose of the tick just before, so a weapon switch, even between two frames of a tick, still snaps while the weapon's own animations blend however far they move. A position that is not a number still snaps the pose. A node whose two rotations are about half a turn apart (the rocket launcher's barrels set back after a shot) snaps alone, since which way it turned cannot be told; the other nodes still blend. Only what is drawn changes, nothing the machines send or agree on: no network version change. Checked: ninja windows; tools/harness/tests/test_first_person_interpolation.py (8 cases, and 6 negative controls, the old 0.25 limit among them, that fail); the whole harness on Windows fails the same 632 tests as testing does (the Windows environment's) and passes the 14 new ones. Not built for Android or the web here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Let each controller stick's dead zone be set, in percent
On a screen wider than 640 columns, cinematic_render moved each title's box by its middle, scaled to the screen's width. Most chapter titles are right-justified in a box that starts near the left edge, so on 16:9 the text ended about 81% of the way across the screen, where the Xbox's 4:3 picture has it at about 93% (The Silent Cartographer's title, compared with an Xbox capture). The box now moves by the edge its text is justified to: the right edge for right-justified titles, the left edge for left-justified ones, and the middle for centered ones, as before. On 640 columns nothing moves. Only the drawing changes, nothing the machines send or agree on, so HALO_PORT_NETWORK_VERSION stays. Checked in the game on Windows at 16:9: The Silent Cartographer's title ends near the right side, as on the Xbox. test_cinematic_titles.py gains cases for each justification on a 960-column screen and on 640 columns, and negative controls that move the box by its middle again (not run locally, on Windows; CI runs them). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shader translator moves every vertex half a pixel right and down to turn Direct3D's pixel centers into OpenGL's, half a pixel of the game's 480 lines (nv2a_vsh.c). On a screen drawn at more pixels than that, a screen effect pass's quad then started that far inside its target's left and top edges, and the pixels there were never drawn. Each pass of a scope's zoom warp read their stale contents and drew them farther in: a dark band down the left of the blur and along its top, which the right and bottom edges, clamped to drawn pixels, did not have. rasterizer_screen_effect_cover_target moves each pass's quad back by that half pixel, a pixel of the viewport it draws into (the whole surface for the second render-primary surface, the window for the primary), and its texture coordinates with it, now floats: every pixel samples where it did, and the quad covers the whole viewport. Only the drawing changes, nothing the machines send or agree on, so HALO_PORT_NETWORK_VERSION stays. Checked: the new harness test screen_effect_cover (a 16:9 screen, the Xbox's 640x480 and a four-player split screen, with four negative controls) and the existing screen_effect_passes pass, built with clang on Windows; the Windows release build compiles without warnings; in the game on Windows, the scope's blur reaches the left and top edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hud_corrections.py resized the supplied 2x and 10x pictures to fill their sprite cells ([0,0,31,24] and [0,24,43,48]), but the Xbox's glyphs sit inside those with empty texels around them: 2x covers 21x14 texels and 10x 32x14 (alpha >= 128, measured in the Xbox maps), where the high-res labels covered 30x18 and 42x18, a third larger than on the Xbox. The pictures are now placed on the stock glyph bounds, still resized proportionally: 10x within one high-res pixel of them, 2x within three (its supplied glyphs are a little wider for their height than the stock's). Only sniper__hud_reticles_scope__0.png and its layout entry change; the rocket reticle and the Custom Edition scope rebuild unchanged. Custom Edition's own 2x is left as it is: its stock size was not measured. Checked: python tools/hud_corrections.py regenerates the picture; tools/test_hud_corrections.py passes (5), with a new test of the labels' visible bounds that the previous picture fails by 35 and 42 high-res pixels; in the game on Windows, the pistol's and the sniper rifle's labels are the Xbox's size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…size The Pelican's side light (vehicles\pelican\shaders\lights, an additive decal) sank into the hull in slivers, where on the Xbox it is a whole glow. A z bias of 8 is a polygon offset of 8 depth units and 2 times the depth's slope across a pixel (D3DDevice_SetRenderState_ZBias), and the native builds weakened both: - With display.reversed_depth (the default on the desktop) the depth buffer holds floats, whose polygon offset units are a part of the depth itself, which reversed is small far away: at ten world units the 8 units moved a decal a hundredth of what the Xbox's 24-bit units did. The vertex programs now add the units in the Xbox's measure (depth_bias, each 1 / viewport_scale.z), and the polygon offset keeps only the slope. - The slope term is the depth's change across a pixel, and a screen drawn at several pixels to the game's one has that many times less depth across each: at 1080 lines it was 2.25 times weaker, and on a surface seen at an angle it is most of the bias. The factor is now multiplied by the bound target's scale, as the Xbox took it across its own pixels. At 640x480 with display.reversed_depth off nothing changes, and nothing the machines send changes. Checked: the Windows release build compiles without warnings; in the game on Windows (RTX 5070 Ti, 1080 lines), the Pelicans' red lights in a30's opening cutscene are whole glows, as in an Xbox capture (xemu), where they were cut into slivers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Pelicans' thrusters and the lights of a corridor blinked out for a moment and came back, where on the Xbox they stay lit. The game tests each flare at its place in the frame's list and reads the test back by that place at the start of the next frame. The Xbox waited for the tests there, so the place's test was the flare's; the native builds read the counts two or more frames late without waiting (d3d8_gl.c, visibility_read_batch), and the list changes as flares come into view and go out of it, so a flare was read the count of the one at its place frames before. A hidden flare's count puts a flare's visibility to none at once, and it comes back over some ticks. Each flare now keeps a slot, found by its own visibility (lens_flare_occlusion_test_results_get), for as long as it goes on being tested, and is read from there; a slot just taken is not read for four frames, until the flare's own counts have come. A flare that finds no slot is tested at its place in the list, past the slots, as before. Only the drawing changes, nothing the machines send. Checked: the new harness test test_lens_flare_occlusion.py (the real code over a fake GPU that reads counts two frames late, the flares' order changing each frame) passes, built with clang on Windows; the code before this change fails it (a visible flare put out to 0), as do three other faults; in the game on Windows (RTX 5070 Ti), the thrusters in a30's opening cutscene and a corridor's lights stay lit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two faults met running a dedicated server on Linux with Custom Edition
maps copied from Windows:
- A custom map whose file has capitals (cmt_Snow_Grove.map) could not be
played. The maps' list names each map in lower case, and the rotation's
check then looked for custom_maps/cmt_snow_grove.map exactly, which a
case-sensitive file system does not have ("there is no Custom Edition
map cmt_snow_grove"). map_file now falls back to the folder's entry of
that name in any case (posix_find_entry_case_insensitive), as the game's
own platform_translate_path already does when it loads the map. This
covers custom_maps, paths.custom_maps and Custom Edition's own folder.
- PLAY NOW on the page refused a rotation entry with many rules, such as
custom_maps\tsce_multiplayerv1:team_slayer+ff=off+team_vehicles=custom+
...+vehicle_respawn_time=60 (about 170 characters), with "map is needed
(no spaces)". The page sends such an entry whole as the map, and
POST /api/rotation/play read it into 128 bytes, though an entry may be
255 characters. The check is now rotation_play_line, which takes a
rotation entry's length and says so when a map is longer. The protocol
simulator and docs/dedicated_server.md say the same.
Nothing the machines send each other changes, so the network version
stays.
Checked: new harness cases resolve-case (test_server_rotation.py) and
rotation-play (test_server_http.py), each with a negative control
(custom-maps-case-sensitive, play-entries-short). On Windows the two
files' 161 tests passed before; now 164 pass and one is skipped:
custom-maps-case-sensitive, as NTFS finds a file in any case (it runs on
Linux). server_http.c and server_rotation.c compile
clean in the Windows build. The other tests that read these files
(test_server_toml.py, test_deploy_gametypes.py, test_server_sim.py) fail
the same way here before and after the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Driving a vehicle, its shadow's trailing side flickered all the time, most of all a Ghost's. With Smooth Motion (display.interpolation) each frame draws the world between the last two ticks, and the model drawn into an object's shadow map is drawn so (object_get_node_matrices), but the map was centered at the object's bounding sphere as the latest tick left it (object_get_bounding_sphere). A fast vehicle stood up to a tick's travel off its map's center, by as much as each frame's blend, and the map's edge and corner fade cut its shadow's trailing side by that much, frame by frame. render_object_shadow_begin now moves the center to where the frame draws the object (render_interpolation_object_point, as the nav points do). With Smooth Motion off, or on a tick, nothing changes; only the drawing changes, nothing the machines send. Checked: the new harness test test_shadow_center.py passes, built with clang on Windows, and fails with the center left at the tick; in the game on Windows, a Ghost's shadow no longer flickers while driving. A point of the shadow that now and then goes missing at speed does so with Smooth Motion off too, and on the Xbox (checked in xemu): the original's corner fade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firing shook the view less at high frame rates, and the assault rifle hardly at all. A shake or impulse just started is drawn at its full scale once (its just-started flag is cleared by the draw), and each draw counts its time left down by the ticks since the last; the Xbox drew a frame a tick, so the full scale showed for a tick and each later scale for a tick. With Smooth Motion the frames between ticks drew the scale after the tick's, and the full scale only on the tick's first frame: an eighth of the tick at 240 frames a second, and the rifle's short kick was over before it could be seen. player_effect_get_camera_effect_matrix now keeps each local player's effect as the first frame after a tick made it, and the frames after it until the next tick draw it again; the continuous shake looping sounds add each frame is still used up, as those frames would. Its direction was already the tick's (the seed made from the tick). With one frame a tick nothing changes; only the drawing changes, nothing the machines send. Checked: the Windows release build compiles without warnings; in the game on Windows (RTX 5070 Ti, Smooth Motion, high frame rate), firing the assault rifle and the sniper rifle shakes the view as with Smooth Motion off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Shade's and the Spirit's plasma contrails were shorter and weaker at high frame rates. A contrail point steps through its states, and a state that ends drops the rest of the step's time (contrail_update_ points), so on the Xbox, a frame a tick, each state lasted at least a tick. The Shade's gun (the Spirit's fires the same projectile, vehicles\ c gun turret\c gun turret) has points of states of 0.01, 0.05 and 0.005 seconds: they lived four ticks, 133 ms, there, and aged every frame at 240 frames a second about half that, the trail half as long. contrails_update ages the points on the first frame after each tick, for all the time since, as it already emitted new points there; the texture animation stays every frame. With one frame a tick nothing changes; only the drawing changes, nothing the machines send. Checked: the Windows release build compiles without warnings; in the game on Windows (RTX 5070 Ti, high frame rate), the Shade's and the Spirit's contrails are as long as the Xbox's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture the flag flag did not behave as on the Xbox at high frame rates. Each step of its cloth (flag_update) moves every vertex to where its neighbours hold it, a relaxation whose stiffness is in the number of steps, and the native builds stepped it every frame: eight steps a tick at 240 frames a second held it eight times as stiff, and it hung and flapped unlike the Xbox's, which stepped a frame a tick. And the cloth hangs from its object's markers where the tick left them, while the frame draws the object between the ticks, so a running carrier's flag rode ahead of its pole. flags_update steps the cloth on the first frame after each tick, for all the time since the last step, and flag_render_proper draws it (its vertices and centroid) as far from there as its object is drawn (render_interpolation_object_point). With one frame a tick nothing changes; only the drawing changes, nothing the machines send. Checked: the Windows release build compiles without warnings; in the game on Windows (RTX 5070 Ti, high frame rate), a capture the flag flag hangs and flaps as on the Xbox, standing and carried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no weapon in hand, your flashlight stepped at 30 a second while the view moved every frame. A flashlight on a first-person weapon is placed at the weapon's flashlight marker, posed against the drawn camera each frame (first_person_weapon_center_flashlight); with none, the light stayed where the tick left it. The port already moved another player's beam, in a match, to the marker on the pose the frame draws, but only in a network game. Any flashlight not on a first-person weapon now shines from that marker, in a local game too. Only the drawing changes, nothing the machines send. Checked: the Windows release build compiles without warnings; in the game on Windows (RTX 5070 Ti, high frame rate), the flashlight with no weapon in hand (The Pillar of Autumn's start) moves with the view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aging the points on the first frame after each tick, for all the time since, kept them as the Xbox's at 30 frames a second and above, where a frame has one tick at most. Below 30 a frame can have two ticks, and the points aged both in one step: a state that ends drops the rest of the step's time, so they lived longer, the trail brighter, than the Xbox's. They now age a step for each tick since the last aging, each a tick's share of the time, as the Xbox's frames aged them, at any frame rate. Checked: the Windows release build compiles without warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cloth is stepped on the first frame after a tick, and game_frame, which steps it, reads the pose that frame draws (render_interpolation. c): it hangs from its first attachment marker as that frame drew it. The frames after it were drawn as far from there as the flag's object is drawn from where the tick left it, which is the other way, and a running carrier's cloth trailed its pole by about a tick's travel. It is now drawn as far from there as that marker has moved since. Checked: the Windows release build compiles without warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… come The review of the slots found two gaps. A slot just taken was left unread for four frames, but the renderer keeps a slot's earlier count for as long as the GPU is behind, and through the tests it drops with every batch waiting to be read (d3d8_gl.c): after four frames a flare could still be read the count of the flare that had the slot before it. And a flare that found no slot was tested at its place in the list past the slots, which brought back the mismatch the slots are for. The renderer now numbers the tests made at each index as they end, dropped ones too, and keeps with each index's count the number of the test it was read from (xgpu_visibility_tests_ended, xgpu_visibility_test_counted): in the desktop's batches, without queries, and with OpenGL ES's atomic counters and its queries. A flare taking a slot notes the number its first test there will have, and the slot is read only once a count of that test or a later one has come. A flare that finds no slot is tested at an index whose counts are never read, so that it is still drawn while on screen, and keeps its visibility. With the occlusion tests off (rasterizer_lens_flares_occlusion) no test is made, and every flare is read 1 as before. Checked: test_lens_flare_occlusion.py has new cases for a slot reused while the GPU is 12 frames behind and while it drops the new flare's first 20 tests, for every slot taken, for split screen, for a map change with the old map's counts still to come, and for the occlusion tests off. The previous revision fails the four for reuse, dropped tests, no slot and the map change (a hidden flare shown at 64 five frames after taking a visible one's slot; a visible flare put out to 0); eleven faults are caught. test_visibility_readback.py's new case checks the numbering through the real Begin, End and Get functions with a GPU three frames behind and a dropped test; two more faults are caught. Built with clang on Windows without warnings; d3d8_gl.c also compiled as the web and Android builds see it (HALO_GLES, with Khronos's headers) with no diagnostics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each shot of a weapon fired faster than its sound lasts takes the last shot's channel (a sound class allows one weapon_fire sound an object), and Flush dropped the waveform from wherever it was to nothing: a click, and a plasma rifle crackled. The packets still go back to the game at once (it refills the channel, and may reuse their memory), but the next 5 ms of the voice are mixed first, at its gains, faded out along half a cosine, and played over the next mixes beside the new sound. What is left of a cut still playing is kept under the next. Tests: mixer.c "cut" (a tone cut at the top of its wave fades with no step larger than its own, and is silent after), "cuts" (voices of every kind cut at random, once and during a cut, with NaNs where no cut is left: nothing but finite samples), with three negative controls. Checked by ear with the plasma pistol: the clicks are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game works out every 3D sound's velocity relative to the listener each tick and gives it to DirectSound (SetVelocity), with the distance factor its Doppler model takes (3.048 m a unit) and the Doppler factor left at its default, 1; but the SDL mixer ignored the velocities: SetVelocity was a stub. Each 3D voice's resampling step is moved by DirectSound's ratio, (c - listener speed away) / (c - source speed towards), the velocities in meters a second. The ratio glides across each mix as the game updates it every tick. Nothing holds it but the frequencies an Xbox voice plays at, the XDK's 188 to 191983 Hz, so that something coming very fast sounds as it should: as high as a voice plays from the speed of sound on, and as low as one plays for a listener outrunning the sound. The resampler's low pass now narrows for steps up to 4, the fastest a voice can play at 48 kHz. A head relative voice has no shift. It works with either resampler, and on 5.1 as on stereo: only the step moves. A stereo sound in the world, which the port pans towards where it is on a 2D stream, is shifted too: the game gives the mixer how fast it comes towards the listener, worked out as a 3D channel's velocity is (dsound_port_set_channel_stereo_position). Without it a Pelican's wind held its pitch while its engines' rose and fell around it. The velocities the game gives are only as good as the listener's, which on the native ports is wrong; the next commit fixes it. Tests: mixer.c "doppler" (approaching, receding, the listener moving, across the line, a 2D voice, a positioned stereo voice, more than an octave either way, at and past the speed of sound, the glide across a mix), with seven negative controls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game gives DirectSound each 3D sound's velocity less the observer's, which on the native ports is a tick's motion divided by a frame's time (the observer runs every frame): riding a Pelican at 144 frames a second, the camera claimed nearly five times its speed, and while a cinematic forces the camera it reads zero. A sound moving with the camera, the Pelican being ridden or the Warthog being driven, then seemed to come at it, and rose in pitch. Where the camera follows a unit (first person, behind it, a dead player watching a teammate: director_get_followed_unit), the listener now takes the game's velocity for the camera's position as if it were fixed to that unit: its ultimate parent's velocity and spin, v + w x r, as physics.c works out its mass points' velocities (object_get_point_velocity). A sound attached to an object gets the same for the point it is attached to, not only the object's centre's. The Doppler effect takes velocities along the line from a sound to the camera, and two points on one turning body move alike along the line between them, so a vehicle's own sounds hold their pitch for whoever rides it, in any seat and from a chase camera, through a network client's corrections of the vehicle and a turret's swing, and swinging the camera round adds nothing. A scripted or flying camera has no unit: its velocity is measured from how far its focus moves from one tick to the next (observer_get_focus_position), with none across a cut or a teleport. Checked in game with a build that silenced any sound shifted by more than 0.5% and logged it: on The Silent Cartographer's opening (the cinematic, the Pelican ride, the landing), and in a Warthog in tight turns; only sounds moving relative to the listener were shifted (footsteps left behind, the other Pelican, enemies firing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#551) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The light bridges' lightning in Assault on the Control Room and 343 Guilty Spark's tendrils flicker several times as fast at high frame rates as at 30 frames a second. lightning_submit draws a bolt's random offsets (its markers', its points' jitter, its texture's) from the local random seed each time it draws it. The Xbox drew a frame a tick, so a bolt took a new shape 30 times a second; the native builds draw every frame, and at 144 frames a second it took a new shape 144 times a second. The first frame of a tick to draw a lightning in a window keeps the local random seed it started from, and the frames after it until the next tick draw the bolt from that seed: the same shape, between its markers where the frame draws them. They leave the seed as they found it, so the frame's other effects get the numbers they would have. Each window of a split screen keeps its own, and a lightning made where another was starts afresh. Checked: the new harness test test_lightning_tick_shape.py (the real lightning_submit, lightning_new and lightnings_initialize_for_new_map over a fake renderer that keeps each bolt drawn) passes, built with clang on Windows: a bolt keeps its shape through eight frames of a tick whatever the other effects take from the seed, follows its markers when they move, takes a new shape the next tick, and each window's is its own; four faults are caught (a new shape each frame, the seed moved on, one seed for every window, a new lightning drawing the old one's bolt). The game builds without warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A far Covenant plasma turret's trail flickers at high frame rates and does not on the Xbox. contrails_update makes and ages a contrail's points on the first frame after each tick, for the frames' time since the last tick: at 144 frames a second four or five frames', 28 or 35 ms, where the Xbox's frames were a tick each, a 29.97th of a second. The turret's points (a point a tick, states of 0.01, 0.05 and 0.005 seconds, both ends of the trail drawn faded) were made and aged by those uneven steps, so the fades along its short trail changed from tick to tick, several times a second. The points are made and aged by the ticks' own time, a 30th of a second each. A game time gone back or jumping ahead (a new map, a checkpoint) counts as a tick. Checked: the new harness test test_contrail_tick_points.py (the real contrails_update, contrail_add_points, contrail_update_points and contrail_compute_new_point_count over a projectile moving a tick at a time, with the turret's contrail) passes, built with clang on Windows: at 144 frames a second, with uneven frames (5, 9 and 3 ms) and with the game's time going back and jumping ahead, each tick shows the trail as a frame a tick does. The code before this change fails it (0.164 of the trail shown where a frame a tick shows 0.167, changing from tick to tick), and four faults are caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ashing A dedicated server playing CTF on the Custom Edition map coldsnap_20th_xl_rc5 crashed in hs_thread_main: the map's script ran volume_test_object on a Warthog that had been deleted. A script keeps an object's index after the object is gone, in a global or an object list, and nothing between the script and object_get checked it. In a release build datum_get reports the stale index and returns NULL, and the read through that NULL faults. Halo PC plays this map. The script runtime now turns such a value into none. hs_cast checks every global read and every function's or script's result, before casting, so no list is made of a deleted object. hs_arguments_evaluate checks the arguments again before the call, because a later argument's static script can sleep after an earlier one was evaluated. Lists collected in the same way count as gone too. Walking an object list passes over deleted objects; list_count still counts every entry, as it always has. A script already gets none for a named object that doesn't exist, and nearly every script function takes it. The functions with object parameters were searched for a check of none, and those without one were read: unit_kill, unit_kill_silent and scenery_get_animation_time crashed on none, so they now do nothing for it. HALO_PORT_NETWORK_VERSION stays the same: nothing sent changes, and scripts need not agree between machines. Only the host runs them in co-op, and in other games the host decides what they do to the game. Checked with tools/harness/tests/test_script_stale_objects.py (the real runtime and data arrays, with negative controls): 42 pass. On Windows the full harness has the same 631 failures as testing and no new ones. The four changed files compile for Windows; CI builds the other platforms. Not run on the real map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-index Give a script none for an object that no longer exists, instead of crashing
…y-now Find custom maps in any case and play long entries from the server page
…interpolation Blend the first-person weapon's own fast animations between ticks
…ile-busy Start StageKit's games one at a time, and again when the binary is busy
Fix remapped menu navigation and add a camera cycle binding
Keep a lightning bolt's shape through the frames of a tick
Shine an empty-handed player's flashlight from the drawn pose
The flag cloth a tick at a time and draw it with its pole
Draw a tick's camera shake on every frame until the next tick
…nter Center an object's shadow where the frame draws it between ticks
Size the Xbox scope's 2x and 10x to the stock glyphs, not their sprites
Keep chapter titles at the side for wide screens
Draw a scope's zoom blur to the left and top edges of the screen
Keep a z bias's depth units and slope as the Xbox's at any depth and size
…lots Test each lens flare's occlusion in a slot of its own
Sound: fade out a voice the game cuts off instead of stopping it dead
Sound: the Doppler effect
Age contrail points a tick at a time
Time server browser hosts in the order shown, with dots
…r physics steps a tick at a time (#546) game_frame halves the frame's time while the players are double speed, and the ticks' time the points now take should be halved with it. The comment and the README row now say that the points' physics and fades step once a tick, not every frame. test_contrail_tick_points.py passes (7).
…ost (#441) The probes waited while any listing was queued for its signature check, and anyone can send listings: a queue kept full held every ping back for good. They now wait at most PROBE_HOLD (1 s) after the browser opens or REFRESH, so the first wave still starts in the order shown. p2p_lobby_check.c checks the hold both ways and fails with the old gate; test_p2p_signatures_and_listings passes.
stream_flush now calls voice_cut, and the stream holds the cut's buffers (CUT_FADE_FRAMES), which test_sound_stream_flush.py's extract of dsound_sdl.c lacked: it no longer compiled. The fade itself is test_mixer.py's; here it is a stub. The audio harness tests pass (76).
cursor Bot
added a commit
to damiantw/halo-ce-web-multiplayer
that referenced
this pull request
Nov 6, 2026
…62-d02a Port upstream rendering fixes from OpenCE OpenCommunityEdition#562
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.