extends Node # Manual two-process smoke test for NetworkManager's clock (task 1.8 # acceptance: "offset converges within 2s and stays within ±1 tick on a # clean link"). Not part of tests/test_runner.tscn — needs real ENet peers # and real wall-clock ping/pong cadence. Run: # # godot --headless --path Game res://tests/clock_smoke.tscn -- --role=host # godot --headless --path Game res://tests/clock_smoke.tscn -- --role=client # # Adversarial-review regression: the original version only checked that # later samples agreed with the first one (self-consistency) — a # consistently-wrong offset (e.g. a missing /2 on RTT, or a sign flip) # would converge just as cleanly and still pass. Both roles now also write/ # read an independent ground truth: each process's own OS wall-clock # (Time.get_unix_time_from_system(), shared hardware clock, same machine) # lets it compute "my Time.get_ticks_msec() minus real epoch time" — the # TRUE required offset is just the difference of those two numbers between # host and client, computed via a shared temp file since the two processes # can't otherwise see each other's local variables. This is independent of # NetworkManager's own ping/pong math entirely. const PORT := 7801 const RUN_SECONDS := 6.0 const CONVERGE_BY_SEC := 2.0 const TICK_MS := 1000.0 / 60.0 # SimConstants.TICK_HZ, kept literal to avoid pulling in the whole project for one constant in a throwaway diagnostic const EPOCH_FILE := "/tmp/cosmicclash_clock_smoke_epoch_offset.txt" # Ground-truth tolerance is looser than the ±1-tick self-consistency check: # Time.get_unix_time_from_system() itself is only second-resolution on some # platforms and the two processes sample it at slightly different instants, # so this bounds "is the offset even the right ballpark and sign" rather # than chasing sub-tick precision the way the self-consistency check does. const GROUND_TRUTH_TOLERANCE_MS := 250.0 var _role := "" var _start_ms := 0 var _samples: Array[Dictionary] = [] # {t_sec, offset} var _finished := false func _epoch_offset_ms() -> float: return Time.get_unix_time_from_system() * 1000.0 - float(Time.get_ticks_msec()) func _ready() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--role="): _role = arg.substr("--role=".length()) match _role: "host": var err := NetworkManager.host(PORT) if err != OK: _finish(false, "host() failed: %s" % error_string(err)) return var f := FileAccess.open(EPOCH_FILE, FileAccess.WRITE) if f: f.store_string(str(_epoch_offset_ms())) f.close() print("SMOKE: hosting on port %d" % PORT) "client": NetworkManager.clock_updated.connect(_on_clock_updated) var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return print("SMOKE: joining ...") _: _finish(false, "missing or unrecognised --role=") return _start_ms = Time.get_ticks_msec() get_tree().create_timer(RUN_SECONDS).timeout.connect(_on_run_complete) func _process(_delta: float) -> void: NetworkManager.poll() func _physics_process(_delta: float) -> void: NetworkManager.poll() func _on_clock_updated(rtt_ms: float, offset_ms: float) -> void: var t_sec := float(Time.get_ticks_msec() - _start_ms) / 1000.0 _samples.append({"t_sec": t_sec, "offset": offset_ms}) print("SMOKE clock sample t=%.2fs rtt=%.2fms offset=%.2fms" % [t_sec, rtt_ms, offset_ms]) func _on_run_complete() -> void: if _role != "client": _finish(true, "host ran for %.1fs" % RUN_SECONDS) return if _samples.is_empty(): _finish(false, "no clock samples received at all") return var converged_sample: Dictionary = {} for s: Dictionary in _samples: if s["t_sec"] <= CONVERGE_BY_SEC: converged_sample = s if converged_sample.is_empty(): _finish(false, "no sample landed by t=%.1fs (first sample at t=%.2fs)" % [CONVERGE_BY_SEC, _samples[0]["t_sec"]]) return var reference: float = converged_sample["offset"] var max_drift := 0.0 for s: Dictionary in _samples: if s["t_sec"] < CONVERGE_BY_SEC: continue max_drift = maxf(max_drift, absf(s["offset"] - reference)) if max_drift > TICK_MS: _finish(false, "offset drifted %.2fms after t=%.1fs (> 1 tick = %.2fms)" % [max_drift, CONVERGE_BY_SEC, TICK_MS]) return # Self-consistency alone can't catch a systematically-wrong-but-stable # offset (§9 gotcha, adversarial review) — cross-check against the OS # wall clock, independent of NetworkManager's own math entirely. if not FileAccess.file_exists(EPOCH_FILE): _finish(false, "converged (%.2fms, drift %.2fms) but host's epoch-offset file was never found — ground truth unavailable" % [reference, max_drift]) return var f := FileAccess.open(EPOCH_FILE, FileAccess.READ) var server_epoch_offset := f.get_as_text().to_float() f.close() var client_epoch_offset := _epoch_offset_ms() var true_offset := client_epoch_offset - server_epoch_offset var ground_truth_error := absf(reference - true_offset) if ground_truth_error > GROUND_TRUTH_TOLERANCE_MS: _finish(false, "converged (%.2fms) but disagrees with OS-clock ground truth (%.2fms) by %.2fms (> %.1fms tolerance) — the offset math itself may be wrong, not just noisy" % [ reference, true_offset, ground_truth_error, GROUND_TRUTH_TOLERANCE_MS ]) else: _finish(true, "offset converged by t=%.1fs (%.2fms), stayed within %.2fms (<= 1 tick = %.2fms) for the rest of the run across %d samples, AND agrees with independent OS-clock ground truth (%.2fms, error %.2fms <= %.1fms tolerance)" % [ CONVERGE_BY_SEC, reference, max_drift, TICK_MS, _samples.size(), true_offset, ground_truth_error, GROUND_TRUTH_TOLERANCE_MS ]) func _finish(success: bool, message: String) -> void: if _finished: return _finished = true print("SMOKE %s: %s" % ["PASS" if success else "FAIL", message]) await get_tree().create_timer(0.3).timeout NetworkManager.shutdown() get_tree().quit(0 if success else 1)