mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
4fb7ddfecf
multiplayer-todo.md and multiplayer-next.md tracked overlapping information in two places. Fold everything into multiplayer-next.md (architecture decisions, wire format, task breakdown with checkboxes, gotchas list, testing notes) and delete multiplayer-todo.md. Section numbers are unchanged, so existing code comments citing them by section/task number still resolve; update every such reference to point at the new filename.
168 lines
6.0 KiB
GDScript
168 lines
6.0 KiB
GDScript
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)
|