mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 20:43:43 +00:00
cf73074e27
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.
66 lines
2.7 KiB
GDScript
66 lines
2.7 KiB
GDScript
extends CanvasLayer
|
|
|
|
# Autoload: toggleable network debug overlay (F4 by default — see
|
|
# toggle_net_overlay in project.godot's [input]). Read-only against
|
|
# NetworkManager's clock state (task 1.8). Mirrors perf_overlay.gd's pattern
|
|
# — headless-guarded, hidden by default, no gameplay-state writes.
|
|
|
|
var _label: Label
|
|
|
|
|
|
func _ready() -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
set_process(false)
|
|
return
|
|
layer = 100
|
|
_label = Label.new()
|
|
_label.add_theme_font_size_override("font_size", 14)
|
|
_label.add_theme_color_override("font_color", Color(0.5, 0.8, 1.0))
|
|
_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.85))
|
|
_label.add_theme_constant_override("shadow_offset_x", 1)
|
|
_label.add_theme_constant_override("shadow_offset_y", 1)
|
|
_label.position = Vector2(12, 90)
|
|
_label.visible = false
|
|
add_child(_label)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("toggle_net_overlay") and _label:
|
|
_label.visible = not _label.visible
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if not _label or not _label.visible:
|
|
return
|
|
if NetworkManager.is_server:
|
|
_label.text = "NET: server, %d peer(s) out %s in %s" % [
|
|
MatchNet.roster.size(), _format_kbps(MatchSim.get_bytes_sent_per_sec()), _format_kbps(MatchSim.get_bytes_received_per_sec()),
|
|
]
|
|
elif NetworkManager.is_client:
|
|
if NetworkManager.rtt_ms < 0.0:
|
|
_label.text = "NET: client, connecting (no clock sample yet)"
|
|
else:
|
|
# task 3.7: RTT, jitter, loss, buffer depth, snapshot age,
|
|
# bandwidth all live here now. Prediction error is intentionally
|
|
# absent — there is no client-side prediction until Phase 4, so
|
|
# there is nothing honest to show for it yet. STALLED shows the
|
|
# server's own InputJitterBuffer.stalled bit for this client's
|
|
# slot, round-tripped through the wire.
|
|
var stats := {}
|
|
var game := get_tree().get_first_node_in_group("game")
|
|
if game and game.has_method("get_net_debug_stats"):
|
|
stats = game.get_net_debug_stats()
|
|
var stalled_suffix := " STALLED" if stats.get("server_stalled", false) else ""
|
|
_label.text = "NET: client RTT %.1fms jitter %.1fms offset %.1fms\nbuf depth %s lead %s loss %.1f%% snap age %.1fms%s\nout %s in %s" % [
|
|
NetworkManager.rtt_ms, NetworkManager.jitter_ms, NetworkManager.clock_offset_ms,
|
|
str(stats.get("input_buffer_depth", -1)), str(stats.get("input_lead", "-")),
|
|
stats.get("snapshot_loss_pct", 0.0), stats.get("snapshot_age_ms", 0.0), stalled_suffix,
|
|
_format_kbps(MatchSim.get_bytes_sent_per_sec()), _format_kbps(MatchSim.get_bytes_received_per_sec()),
|
|
]
|
|
else:
|
|
_label.text = "NET: offline"
|
|
|
|
|
|
func _format_kbps(bytes_per_sec: float) -> String:
|
|
return "%.2f KB/s" % (bytes_per_sec / 1000.0)
|