extends Node # Manual two/three-process smoke test for MatchNet (task 1.4 acceptance: # "a mismatched client is rejected with a readable reason", plus the happy # path: hello/welcome, roster sees player_joined on both sides). Not part of # tests/test_runner.tscn — needs real ENet peers. Run: # # godot --headless --path Game res://tests/match_net_smoke.tscn -- --role=host # godot --headless --path Game res://tests/match_net_smoke.tscn -- --role=client --name=Alice # godot --headless --path Game res://tests/match_net_smoke.tscn -- --role=client-badversion # godot --headless --path Game res://tests/match_net_smoke.tscn -- --role=host_recycle # godot --headless --path Game res://tests/match_net_smoke.tscn -- --role=client --name=Bob (run once against host_recycle, then let it disconnect) const PORT := 7800 const TIMEOUT_SECONDS := 5.0 var _role := "" var _player_name := "TestPlayer" var _finished := false func _ready() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--role="): _role = arg.substr("--role=".length()) elif arg.begins_with("--name="): _player_name = arg.substr("--name=".length()) match _role: "host": MatchNet.player_joined.connect(_on_player_joined) var err := NetworkManager.host(PORT) if err != OK: _finish(false, "host() failed: %s" % error_string(err)) return print("SMOKE: hosting on port %d" % PORT) "host_recycle": _run_host_recycle() return "client": MatchNet.local_player_name = _player_name MatchNet.welcomed.connect(_on_welcomed) MatchNet.rejected.connect(_on_rejected) var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return print("SMOKE: joining as '%s' ..." % _player_name) "client-badversion": MatchNet._auto_hello = false MatchNet.rejected.connect(_on_rejected) MatchNet.welcomed.connect(_on_welcomed) var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return NetworkManager.connected_to_server.connect(func(): var NetCodec = load("res://scripts/net_codec.gd") MatchNet._hello.rpc_id(1, NetCodec.PROTOCOL_VERSION + 99, 60, "BadVersion") ) print("SMOKE: joining with a deliberately wrong protocol version ...") "client-longname": # Adversarial-review regression: a client sending an oversized # player_name used to be broadcast verbatim to every peer, # head-of-line-blocking the reliable channel. Confirm it's # rejected outright before ever reaching a broadcast. MatchNet._auto_hello = false MatchNet.rejected.connect(_on_rejected) MatchNet.welcomed.connect(_on_welcomed) var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: _finish(false, "join() failed: %s" % error_string(err)) return NetworkManager.connected_to_server.connect(func(): var NetCodec = load("res://scripts/net_codec.gd") var SimConstants = load("res://scripts/sim_constants.gd") var huge_name := "X".repeat(500000) # 500 KB, well past MAX_INPUT_LENGTH MatchNet._hello.rpc_id(1, NetCodec.PROTOCOL_VERSION, SimConstants.TICK_HZ, huge_name) ) print("SMOKE: joining with a deliberately oversized player name ...") _: _finish(false, "missing or unrecognised --role=") 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_player_joined(peer_id: int, player_name: String) -> void: _finish(true, "host saw player_joined (peer_id=%d, name=%s)" % [peer_id, player_name]) # Adversarial-review regression (multiplayer-next.md §9): MatchNet.roster # used to have no path that cleared it when a HOST itself called # NetworkManager.shutdown() — only the client-side disconnect signal did. # Host -> client joins -> host leaves (shutdown) -> host again used to # leave the first client permanently in roster. Run this role, then run # `--role=client` once against it while it's up. var _host_recycle_joined := false func _on_host_recycle_player_joined(_peer_id: int, _name: String) -> void: _host_recycle_joined = true func _run_host_recycle() -> void: MatchNet.player_joined.connect(_on_host_recycle_player_joined) var err := NetworkManager.host(PORT) if err != OK: _finish(false, "host() failed: %s" % error_string(err)) return print("SMOKE: host_recycle hosting on port %d, waiting for a client..." % PORT) var deadline := Time.get_ticks_msec() + int(TIMEOUT_SECONDS * 1000.0) while not _host_recycle_joined and Time.get_ticks_msec() < deadline: await get_tree().process_frame if not _host_recycle_joined: _finish(false, "no client joined within %.1fs" % TIMEOUT_SECONDS) return print("SMOKE: host_recycle got a joiner (roster size=%d), now leaving and re-hosting..." % MatchNet.roster.size()) NetworkManager.shutdown() err = NetworkManager.host(PORT) if err != OK: _finish(false, "re-host() failed: %s" % error_string(err)) return await get_tree().process_frame await get_tree().process_frame var ok := MatchNet.roster.is_empty() _finish(ok, "roster after re-host: size=%d (expected 0)" % MatchNet.roster.size()) func _on_welcomed() -> void: if _role == "client-badversion" or _role == "client-longname": _finish(false, "%s was welcomed, expected rejection" % _role) else: _finish(true, "client was welcomed") func _on_rejected(reason: String) -> void: if _role == "client-badversion" or _role == "client-longname": _finish(true, "%s correctly rejected: %s" % [_role, reason]) else: _finish(false, "client was rejected unexpectedly: %s" % reason) func _on_timeout() -> void: if not _finished: _finish(false, "timed out") 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.5).timeout NetworkManager.shutdown() get_tree().quit(0 if success else 1)