mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
154 lines
6.1 KiB
GDScript
154 lines
6.1 KiB
GDScript
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
|
|
# The client starts after the host, so its identical run window ends later.
|
|
# Keep the host alive through that tail to avoid polling a deliberately
|
|
# closed transport during a successful clock test.
|
|
const HOST_GRACE_SECONDS := 1.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":
|
|
await get_tree().create_timer(HOST_GRACE_SECONDS).timeout
|
|
_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)
|