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 return if not _label or not _label.visible or not (event is InputEventKey) or not event.pressed or event.echo: return var game := get_tree().get_first_node_in_group("game") if game == null or not game.has_method("adjust_prediction_tuning"): return # Client-only live tuning: [/] threshold, -/= visual decay, ,/. visual # offset, P present-time A/B. Deliberately no project input actions: these # diagnostics never enter ShipAction or server/controller code. match event.keycode: KEY_BRACKETLEFT: game.adjust_prediction_tuning(-0.1) KEY_BRACKETRIGHT: game.adjust_prediction_tuning(0.1) KEY_MINUS: game.adjust_prediction_tuning(0.0, -0.01) KEY_EQUAL: game.adjust_prediction_tuning(0.0, 0.01) KEY_COMMA: game.adjust_prediction_tuning(0.0, 0.0, -0.05) KEY_PERIOD: game.adjust_prediction_tuning(0.0, 0.0, 0.05) KEY_P: game.adjust_prediction_tuning(0.0, 0.0, 0.0, true) 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 "" var prediction: Dictionary = stats.get("prediction", {}) _label.text = "NET: client RTT %.1fms jitter %.1fms offset %.1fms\nbuf depth %s (target %s) lead %s loss %.1f%% snap age %.1fms%s\npred pos p50/p95/p99 %.3f / %.3f / %.3fm\npred rot p50/p95/p99 %.2f / %.2f / %.2fdeg snaps %.2f/min\nremote residual p99 %.3fm / %.2fdeg A/B present=%s\ntune [/] pos %.2f -/= decay ,/. offset %.2f P toggle\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_target_depth", "-")), str(stats.get("input_lead", "-")), stats.get("snapshot_loss_pct", 0.0), stats.get("snapshot_age_ms", 0.0), stalled_suffix, prediction.get("position_error_p50", 0.0), prediction.get("position_error_p95", 0.0), prediction.get("position_error_p99", 0.0), prediction.get("rotation_error_p50", 0.0), prediction.get("rotation_error_p95", 0.0), prediction.get("rotation_error_p99", 0.0), prediction.get("hard_snap_rate_per_min", 0.0), stats.get("remote_residual_position_p99", 0.0), stats.get("remote_residual_rotation_p99", 0.0), str(game.remote_visual_present_time_enabled if game else false), prediction.get("position_threshold", 0.0), prediction.get("max_visual_offset", 0.0), _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)