mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): resolve composition regression from second adversarial review
A second adversarial review of the previous fix commit found two of its nine fixes silently defeated each other: the seq-range guard (fix for a MEDIUM epoch-mismatch finding) capped the exact variable the ring-overflow resync (fix for the original CRITICAL finding) depends on, making the resync unreachable in production and recreating permanent input death at a lower failure threshold, reachable via ordinary server tick loss alone. - CRITICAL: rebind the seq-range guard to InputJitterBuffer's own highest_ingested_seq (now public) instead of the consumer-side last_applied_seq, so it tracks the client's send epoch rather than a value that can lag arbitrarily far behind during a stall. - HIGH: InputLeadController's release logic still ANDed the old `lead > LEAD_MIN` gate onto the new depth-driven condition, so a backlog the controller never caused still couldn't drain. Split into two independent decisions: the seq-duplicate action follows real depth alone; lead's own bookkeeping separately never drops below its floor. - MEDIUM: widen the CI driver's movement/stalled sampling margin (run_seconds - 2.0, was - 0.5) and assert the peer is still in multiplayer.get_peers() at sample time, since the old margin let the check pass on residual starvation grace after a bot had already disconnected. - LOW: measure horizontal-only displacement in the human smoke test's movement check — the old 3D-distance bar was beatable by pure gravity settling with fully dead input. - LOW: fix a real "clean stderr" violation (match_net.gd broadcasting a departure notice to a peer whose ENet channels are already torn down, including a second peer disconnecting in the same poll batch) by deferring the notification to the next idle frame. - Wire the server's per-slot stalled bit into the client debug overlay for real — a prior commit message claimed this already reached the overlay when only the CI gate actually read it. Re-verified end-to-end against the real production RPC path (not just unit tests in isolation, which is how the composition bug got past the first round): a 2-bot CI match with a 1.5s host SIGSTOP freeze injected mid-run, well past the 0.6s threshold the review reproduced the bug at, now recovers cleanly on repeated runs with zero stderr noise.
This commit is contained in:
@@ -67,12 +67,18 @@ func test_release_requires_both_clean_surplus_and_its_own_interval() -> void:
|
||||
|
||||
func test_release_stops_at_minimum() -> void:
|
||||
var c := InputLeadController.new()
|
||||
# Sustained surplus depth, but lead is already at LEAD_MIN — must never
|
||||
# push it below the floor regardless of how much surplus is reported.
|
||||
# Sustained surplus depth with lead already at LEAD_MIN: `lead` itself
|
||||
# must never drop below the floor, but release must still fire
|
||||
# (duplicate a seq) once its own timing conditions are met, since a
|
||||
# real reported surplus at floor lead is exactly the "backlog this
|
||||
# controller never caused" case — capping `lead` is cosmetic, it must
|
||||
# not also block the seq-duplicate action that drains real depth.
|
||||
var released := false
|
||||
for i in InputLeadController.CLEAN_SURPLUS_TICKS * 3:
|
||||
var delta := c.update(InputLeadController.TARGET_DEPTH + 1)
|
||||
assert_true(delta == 1, "lead already at minimum, never duplicates a seq trying to release further, tick %d" % i)
|
||||
assert_eq(c.lead, InputLeadController.LEAD_MIN, "stays at minimum")
|
||||
if c.update(InputLeadController.TARGET_DEPTH + 1) == 0:
|
||||
released = true
|
||||
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead never drops below the floor, tick %d" % i)
|
||||
assert_true(released, "release still fires (duplicates a seq) even though lead itself is pinned at minimum")
|
||||
|
||||
|
||||
func test_starve_resets_clean_surplus_counter() -> void:
|
||||
@@ -99,14 +105,18 @@ func test_starve_resets_clean_surplus_counter() -> void:
|
||||
assert_eq(c.lead, lead_after_attack - 1, "release finally fires once a full fresh clean window has elapsed since the interruption")
|
||||
|
||||
|
||||
# An adversarial review found the original release gate was `lead >
|
||||
# A first attempt at fixing this gated the whole release branch on `lead >
|
||||
# LEAD_MIN` — this controller's own memory of past attacks — so a backlog
|
||||
# it did NOT itself create (a server hitch, persistent client/server clock
|
||||
# drift, a burst re-delivery) was never drained: lead stayed at 1 forever
|
||||
# even while the server kept reporting a deep, real backlog. This
|
||||
# reproduces that scenario directly: lead never attacks (depth is never
|
||||
# reported as a starve, <= 0), yet release must still fire from sustained
|
||||
# real surplus alone.
|
||||
# even while the server kept reporting a deep, real backlog, and — because
|
||||
# that gate blocked the seq-duplicate action too, not just lead's own
|
||||
# bookkeeping — the actual buffered depth was never drained either. A
|
||||
# second adversarial review caught that the depth check added alongside
|
||||
# it didn't remove the old gate, just sat next to it. This reproduces the
|
||||
# scenario directly: lead never attacks (depth is never reported as a
|
||||
# starve, <= 0), yet release must still fire from sustained real surplus
|
||||
# alone, even while lead itself stays pinned at its floor throughout.
|
||||
func test_release_drains_a_backlog_it_never_caused_itself() -> void:
|
||||
var c := InputLeadController.new()
|
||||
assert_eq(c.lead, InputLeadController.LEAD_MIN, "starts at minimum, never attacked")
|
||||
@@ -114,9 +124,12 @@ func test_release_drains_a_backlog_it_never_caused_itself() -> void:
|
||||
# A large, externally-caused surplus (e.g. right after the server's own
|
||||
# ring-overflow resync) reported for well over 2s — lead never moves
|
||||
# via attack since depth is never <= 0.
|
||||
var released_at_floor := false
|
||||
for i in InputLeadController.CLEAN_SURPLUS_TICKS + InputLeadController.RELEASE_INTERVAL_TICKS:
|
||||
c.update(10)
|
||||
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead cannot release below its own floor even under large surplus")
|
||||
if c.update(10) == 0:
|
||||
released_at_floor = true
|
||||
assert_eq(c.lead, InputLeadController.LEAD_MIN, "lead's own bookkeeping never drops below its floor")
|
||||
assert_true(released_at_floor, "release still fires (duplicates a seq, actually draining real depth) even while lead is pinned at the floor")
|
||||
|
||||
# Raise it above the floor via one real attack, then confirm sustained
|
||||
# external surplus (not self-caused) still drains it back down.
|
||||
|
||||
Reference in New Issue
Block a user