mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
4533da34e0
Lands tasks 1.0-1.8 of multiplayer-todo.md: the pure-function test runner, net_codec (wire format quantizers/pack-unpack), NetworkManager (ENet transport, manual polling, min-RTT clock sync), MatchNet (handshake, protocol/tick-rate gating, roster with team+ready state), lobby.tscn (team columns, switch team, ready toggle), server_boot.tscn (headless dedicated server with structured logging and an overrun watchdog), and main_menu.gd's Host/Join-by-IP UI (connecting overlay, cancel, bounded failure path). Followed by an adversarial review (Opus subagent) that found and fixed two real bugs - an unvalidated player_name broadcast that let one client's oversized name head-of-line-block the reliable channel for everyone, and a server-side roster leak across a host/re-host cycle - plus three gaps in the test suite itself where a claim of "verified" wasn't actually backed by what the test checked. All five two-process smoke tests plus the pure-function suite are green with the strengthened assertions in place.
113 lines
4.2 KiB
GDScript
113 lines
4.2 KiB
GDScript
extends Node
|
|
|
|
# Manual two-process smoke test for NetworkManager (task 1.2 acceptance:
|
|
# "two peers connect and disconnect cleanly"; also exercises task 1.3's
|
|
# manual-poll-only regime — NetworkManager disables automatic multiplayer
|
|
# polling, so this script's own _process() polling is what makes the
|
|
# connection progress at all). Deliberately not part of the pure-function
|
|
# suite in tests/test_runner.tscn — an ENet handshake needs two real
|
|
# processes. Run:
|
|
#
|
|
# godot --headless --path Game res://tests/net_smoke.tscn -- --role=host
|
|
# godot --headless --path Game res://tests/net_smoke.tscn -- --role=client
|
|
#
|
|
# (start the host first). Each process prints one "SMOKE PASS/FAIL: ..."
|
|
# line and exits 0/1.
|
|
#
|
|
# Adversarial-review regression: this test used to only confirm each
|
|
# process exits cleanly on its own initiative — it never confirmed the
|
|
# OTHER peer actually observes the disconnect. The host role now waits for
|
|
# BOTH client_connected and client_disconnected before passing; the client
|
|
# explicitly disconnects mid-test (rather than only on process exit) and
|
|
# gives it a beat before quitting, same reasoning as §9 gotcha 26 for
|
|
# connects: a clean disconnect notice still needs a few poll() cycles to
|
|
# reach the wire, or the other side falls back to its ~5s peer timeout
|
|
# (§9 gotcha 11) instead of a prompt, clean disconnect.
|
|
|
|
const DEFAULT_PORT := 7799
|
|
const TIMEOUT_SECONDS := 8.0
|
|
|
|
var _role := ""
|
|
var _port := DEFAULT_PORT
|
|
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("--port="):
|
|
_port = int(arg.substr("--port=".length()))
|
|
|
|
if _role == "host":
|
|
NetworkManager.client_connected.connect(_on_host_client_connected)
|
|
NetworkManager.client_disconnected.connect(_on_host_client_disconnected)
|
|
var err := NetworkManager.host(_port)
|
|
if err != OK:
|
|
_finish(false, "host() failed: %s" % error_string(err))
|
|
return
|
|
print("SMOKE: hosting on port %d, waiting for a client..." % _port)
|
|
elif _role == "client":
|
|
NetworkManager.connected_to_server.connect(_on_client_connected)
|
|
NetworkManager.connection_failed.connect(_on_client_connection_failed)
|
|
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 ..." % _port)
|
|
else:
|
|
_finish(false, "missing or unrecognised --role= (expected host|client)")
|
|
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_host_client_connected(peer_id: int) -> void:
|
|
print("SMOKE INFO: host saw client_connected (peer_id=%d), waiting for client_disconnected too..." % peer_id)
|
|
|
|
|
|
func _on_host_client_disconnected(peer_id: int) -> void:
|
|
_finish(true, "host saw client_connected AND client_disconnected (peer_id=%d)" % peer_id)
|
|
|
|
|
|
func _on_client_connected() -> void:
|
|
print("SMOKE INFO: client connected to host")
|
|
# §9 gotcha 26: give the host a beat to fully settle the connect
|
|
# handshake before we turn around and disconnect again.
|
|
await get_tree().create_timer(0.5).timeout
|
|
NetworkManager.shutdown()
|
|
# Same class of issue as gotcha 26, the disconnect leg: closing the
|
|
# peer queues ENet's own disconnect notice, which still needs a few
|
|
# more poll() cycles to actually reach the wire before this process
|
|
# exits — quit immediately and the host would fall back to its ~5s
|
|
# peer timeout (§9 gotcha 11) instead of a prompt, clean disconnect.
|
|
await get_tree().create_timer(1.0).timeout
|
|
_finish(true, "client connected then disconnected cleanly")
|
|
|
|
|
|
func _on_client_connection_failed() -> void:
|
|
_finish(false, "client connection_failed")
|
|
|
|
|
|
func _on_timeout() -> void:
|
|
if not _finished:
|
|
_finish(false, "timed out waiting for connect+disconnect confirmation")
|
|
|
|
|
|
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)
|