mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
7b150ef72e
New NetSim autoload: seeded, CLI-driven (--net-sim-latency/-jitter/-loss/-dup) latency/jitter/loss/duplicate decorator, a true no-op passthrough unless a flag is set. Wraps MatchSim.send_input/send_snapshot per the design doc's scope, plus NetworkManager's ping/pong so the already-tested RTT/clock measurement becomes the acceptance signal for "raises observed RTT" without waiting on Phase 3's per-peer snapshot echo. Two real bugs found while building and verifying this against Phase 2's own milestone gate (a real match under --net-sim-latency 80 --net-sim-jitter 20, not just LAN): a timestamp captured inside a delayed RPC closure silently ate that side's own added delay out of the round-trip measurement instead of adding to it; and a delayed send whose target disconnected (or whose own process had already shut down) during the hold threw RPC errors, since the existing get_peers() filtering only checked validity at schedule time. Fixed by capturing timestamps before handing off to NetSim, and by having NetSim re-validate the target at fire time. Phase 2's milestone gate now passes for real: a full 1v1 under simulated 80ms latency / 20ms jitter still shows clean server-authoritative movement and zero RPC errors. Full Phase 1 + Phase 2 regression suite re-verified clean with NetSim present but inactive.
112 lines
4.3 KiB
GDScript
112 lines
4.3 KiB
GDScript
extends Node
|
|
|
|
# Manual two-process smoke test for NetSim (task 2.8 acceptance:
|
|
# "--net-sim-latency 80 measurably raises observed RTT"). Deliberately not
|
|
# part of the pure-function suite — needs two real processes and real wall
|
|
# time to observe a delayed pong.
|
|
#
|
|
# NetSim reads its own --net-sim-* flags directly from OS.get_cmdline_user_args()
|
|
# (see net_sim.gd) — this driver only needs --role= and passes any
|
|
# --net-sim-* flags straight through untouched. The host is where the
|
|
# _pong reply gets delayed, so --net-sim-latency=/--net-sim-loss= belong on
|
|
# the HOST invocation; the client just observes NetworkManager.rtt_ms.
|
|
#
|
|
# Usage:
|
|
# godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=host --net-sim-latency=80
|
|
# godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=client --min-rtt=70
|
|
#
|
|
# For the loss scenario:
|
|
# godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=host --net-sim-loss=1.0
|
|
# godot --headless --path Game res://tests/net_sim_smoke.tscn -- --role=client-loss
|
|
|
|
const DEFAULT_PORT := 7810
|
|
const TIMEOUT_SECONDS := 12.0
|
|
const HOST_LIFETIME_SECONDS := 8.0
|
|
# Loose ceiling, not a tight bound: real localhost jitter plus one full
|
|
# PING_INTERVAL_SEC of scheduling slack is possible before the first sample
|
|
# lands, so this only needs to catch a badly broken (e.g. no-op) NetSim.
|
|
const MAX_RTT_SLACK_MS := 400.0
|
|
|
|
var _role := ""
|
|
var _port := DEFAULT_PORT
|
|
var _min_rtt_ms := 0.0
|
|
var _finished := false
|
|
|
|
|
|
func _ready() -> void:
|
|
for arg: String in OS.get_cmdline_user_args():
|
|
if arg.begins_with("--role="):
|
|
_role = arg.substr("--role=".length())
|
|
elif arg.begins_with("--port="):
|
|
_port = int(arg.substr("--port=".length()))
|
|
elif arg.begins_with("--min-rtt="):
|
|
_min_rtt_ms = arg.substr("--min-rtt=".length()).to_float()
|
|
|
|
if _role == "host":
|
|
var err := NetworkManager.host(_port)
|
|
if err != OK:
|
|
_finish(false, "host() failed: %s" % error_string(err))
|
|
return
|
|
print("SMOKE: hosting on port %d (net-sim latency=%.1fms jitter=%.1fms loss=%.2f)" % [
|
|
_port, NetSim.latency_ms, NetSim.jitter_ms, NetSim.loss_fraction
|
|
])
|
|
get_tree().create_timer(HOST_LIFETIME_SECONDS).timeout.connect(func() -> void:
|
|
_finish(true, "host ran for %.1fs" % HOST_LIFETIME_SECONDS))
|
|
elif _role == "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 127.0.0.1:%d, expecting rtt >= %.1fms ..." % [_port, _min_rtt_ms])
|
|
elif _role == "client-loss":
|
|
NetworkManager.clock_updated.connect(_on_unexpected_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 127.0.0.1:%d, expecting NO rtt sample (100%% loss) ..." % _port)
|
|
get_tree().create_timer(HOST_LIFETIME_SECONDS - 1.0).timeout.connect(func() -> void:
|
|
_finish(NetworkManager.rtt_ms < 0.0, "rtt_ms=%.1f after %.1fs (expected -1, no pong ever arrived)" % [
|
|
NetworkManager.rtt_ms, HOST_LIFETIME_SECONDS - 1.0
|
|
]))
|
|
else:
|
|
_finish(false, "missing or unrecognised --role= (expected host|client|client-loss)")
|
|
return
|
|
|
|
get_tree().create_timer(TIMEOUT_SECONDS).timeout.connect(_on_timeout)
|
|
|
|
|
|
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 ceiling := _min_rtt_ms * 4.0 + MAX_RTT_SLACK_MS
|
|
var ok := rtt_ms >= _min_rtt_ms and rtt_ms <= ceiling
|
|
print("SMOKE INFO: observed rtt_ms=%.2f (want >= %.1f, <= %.1f)" % [rtt_ms, _min_rtt_ms, ceiling])
|
|
_finish(ok, "client observed rtt_ms=%.2f against min=%.1f" % [rtt_ms, _min_rtt_ms])
|
|
|
|
|
|
func _on_unexpected_clock_updated(rtt_ms: float, _offset_ms: float) -> void:
|
|
_finish(false, "client received a pong (rtt_ms=%.2f) despite --net-sim-loss=1.0 on the host" % rtt_ms)
|
|
|
|
|
|
func _on_timeout() -> void:
|
|
if not _finished:
|
|
_finish(false, "timed out waiting for a clock sample")
|
|
|
|
|
|
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)
|