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:
@@ -98,7 +98,35 @@ func _remove_player(peer_id: int) -> void:
|
||||
return
|
||||
roster.erase(peer_id)
|
||||
player_left.emit(peer_id)
|
||||
_player_left.rpc(peer_id)
|
||||
# rpc() broadcasts to every peer in multiplayer.get_peers() — including,
|
||||
# transiently, the very peer that just disconnected: this fires from
|
||||
# NetworkManager's client_disconnected signal, and empirically that
|
||||
# peer's own ENetConnection can still be momentarily present in the
|
||||
# broadcast's target set with its channels already torn down, which
|
||||
# logs "Unable to send packet on channel 0, max channels: 0" on every
|
||||
# single disconnect (found by a second adversarial review — harmless to
|
||||
# the game, since the departing peer obviously doesn't need to hear
|
||||
# about its own departure, but it meant "clean stderr" wasn't actually
|
||||
# clean for any test in this project).
|
||||
#
|
||||
# A first attempt filtered the broadcast down to rpc_id() calls that
|
||||
# explicitly skip `peer_id`. That's necessary but not sufficient: when
|
||||
# two peers disconnect within the same poll() batch (both bots quitting
|
||||
# at the end of a CI run land within the same tick), get_peers() here
|
||||
# can still list the SECOND peer as connected while its own disconnect
|
||||
# event just hasn't been dispatched yet in this same batch — sending to
|
||||
# it hits the identical error, one hop later. Defer the whole
|
||||
# notification to the next idle frame instead of sending synchronously
|
||||
# from inside signal-handling: by then poll() has fully returned, every
|
||||
# disconnect event in this batch has been dispatched, and get_peers()
|
||||
# reflects the settled, genuinely-still-connected set.
|
||||
call_deferred("_broadcast_player_left", peer_id)
|
||||
|
||||
|
||||
func _broadcast_player_left(peer_id: int) -> void:
|
||||
for other_peer_id in multiplayer.get_peers():
|
||||
if other_peer_id != peer_id:
|
||||
_player_left.rpc_id(other_peer_id, peer_id)
|
||||
|
||||
|
||||
# Balances a new joiner onto whichever team currently has fewer players
|
||||
|
||||
Reference in New Issue
Block a user