mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +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:
@@ -78,21 +78,31 @@ func update(input_buffer_depth: int) -> int:
|
||||
return 1
|
||||
|
||||
# Release must react to the ACTUAL server-reported depth, not to this
|
||||
# controller's own memory of past attacks. An adversarial review found
|
||||
# the original gate here was `lead > LEAD_MIN` — a self-tracked counter
|
||||
# of this controller's own past decisions — so any backlog it did NOT
|
||||
# itself create (a server hitch, persistent client/server clock drift,
|
||||
# a burst re-delivery) was never drained: `lead` stayed at its starting
|
||||
# value the whole time even while `input_buffer_depth` sat well above
|
||||
# target, permanently adding latency with the control loop reporting
|
||||
# itself perfectly healthy. Gate on the real signal instead.
|
||||
# controller's own memory of past attacks. A first pass at this fix
|
||||
# added the depth check above but left the OLD gate, `lead > LEAD_MIN`,
|
||||
# still ANDed onto the final condition below — so a backlog this
|
||||
# controller did NOT itself cause (a server hitch, persistent client/
|
||||
# server clock drift, a ring resync) still could never be drained:
|
||||
# with lead pinned at its starting floor, that clause always failed
|
||||
# even while input_buffer_depth sat well above target. A second
|
||||
# adversarial review caught it, confirmed by this file's own
|
||||
# test_release_drains_a_backlog_it_never_caused_itself, whose original
|
||||
# assertion text literally said "lead cannot release below its own
|
||||
# floor even under large surplus" as if that were correct.
|
||||
#
|
||||
# The fix splits the one gate into two separate decisions: whether to
|
||||
# duplicate this tick's seq (the only thing that actually narrows real
|
||||
# buffered depth) follows the real signal alone, below; whether to
|
||||
# keep decrementing `lead`'s own bookkeeping below its documented
|
||||
# floor is a separate, cosmetic-only choice made inside that branch.
|
||||
if input_buffer_depth > TARGET_DEPTH:
|
||||
_clean_surplus_ticks += 1
|
||||
else:
|
||||
_clean_surplus_ticks = 0
|
||||
|
||||
if _clean_surplus_ticks >= CLEAN_SURPLUS_TICKS and _ticks_since_change >= RELEASE_INTERVAL_TICKS and lead > LEAD_MIN:
|
||||
lead -= 1
|
||||
if _clean_surplus_ticks >= CLEAN_SURPLUS_TICKS and _ticks_since_change >= RELEASE_INTERVAL_TICKS:
|
||||
if lead > LEAD_MIN:
|
||||
lead -= 1
|
||||
_ticks_since_change = 0
|
||||
return 0 # duplicate this tick's seq — one tick of latency recovered
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user