mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 00:42:05 +00:00
feat(multiplayer): Phase 1 transport, connection, and lobby
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.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
extends Node
|
||||
|
||||
# Manual two-process smoke test for lobby.tscn (task 1.5). BOTH roles load
|
||||
# lobby.tscn as their actual current_scene via change_scene_to_file —
|
||||
# matching how main_menu.gd's Host/Join flow (task 1.7) really gets a
|
||||
# player there — rather than instantiating it as a child of this driver.
|
||||
# That distinction matters: change_scene_to_file() operates on
|
||||
# get_tree().current_scene, and calling it from a node that ISN'T an
|
||||
# ancestor-chain match for current_scene (as an earlier draft of this test
|
||||
# did, by add_child()-ing lobby.tscn under this driver) hung completely
|
||||
# on disconnect — see multiplayer-todo.md §9 gotcha 27.
|
||||
#
|
||||
# The host role loading lobby.tscn is deliberate, not an oversight: a
|
||||
# *dedicated* server (server_boot.tscn) never loads it, but a self-hosting
|
||||
# player clicking main_menu.gd's Host button does — NetworkManager.host()
|
||||
# then _leave_to_lobby(), landing them on lobby.gd's is_server branch (a
|
||||
# read-only view of the roster, no team/ready controls). An earlier
|
||||
# version of this test skipped that branch entirely on the mistaken
|
||||
# assumption that "host" here meant "dedicated server"; adversarial review
|
||||
# caught that it left a real, production-reachable code path untested.
|
||||
#
|
||||
# Not part of tests/test_runner.tscn — needs real ENet peers. Run:
|
||||
#
|
||||
# godot --headless --path Game res://tests/lobby_smoke.tscn -- --role=host
|
||||
# godot --headless --path Game res://tests/lobby_smoke.tscn -- --role=client
|
||||
|
||||
const PORT := 7806
|
||||
|
||||
var _role := ""
|
||||
|
||||
|
||||
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:
|
||||
print("SMOKE FAIL: host() failed: %s" % error_string(err))
|
||||
get_tree().quit(1)
|
||||
return
|
||||
print("SMOKE: hosting on port %d" % PORT)
|
||||
get_tree().change_scene_to_file.call_deferred("res://scenes/lobby.tscn")
|
||||
var host_hooks := preload("res://tests/lobby_test_hooks.gd").new()
|
||||
get_tree().root.add_child.call_deferred(host_hooks)
|
||||
host_hooks.run_host_test.call_deferred()
|
||||
"client":
|
||||
MatchNet.local_player_name = "Carol"
|
||||
var err := NetworkManager.join("127.0.0.1", PORT)
|
||||
if err != OK:
|
||||
print("SMOKE FAIL: join() failed: %s" % error_string(err))
|
||||
get_tree().quit(1)
|
||||
return
|
||||
# Real usage (task 1.7): main_menu.gd will call this same
|
||||
# change_scene_to_file after NetworkManager.join() succeeds — but
|
||||
# not from this test's own _ready(), which the tree is still in
|
||||
# the middle of processing (Godot rejects a synchronous
|
||||
# change_scene_to_file mid node-add with "Parent node is busy").
|
||||
# call_deferred sidesteps that; main_menu.gd's real button-press
|
||||
# handler won't have this problem since it isn't called from
|
||||
# inside _ready().
|
||||
get_tree().change_scene_to_file.call_deferred("res://scenes/lobby.tscn")
|
||||
var hooks := preload("res://tests/lobby_test_hooks.gd").new()
|
||||
get_tree().root.add_child.call_deferred(hooks) # sibling of current_scene, not a child of it -- survives the swap above
|
||||
hooks.run_client_test.call_deferred()
|
||||
_:
|
||||
print("SMOKE FAIL: missing or unrecognised --role=")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
NetworkManager.poll()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
NetworkManager.poll()
|
||||
Reference in New Issue
Block a user