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.
115 lines
5.0 KiB
GDScript
115 lines
5.0 KiB
GDScript
extends Node
|
|
|
|
# Test-only helper (tests/main_menu_smoke.gd). Not referenced by production
|
|
# code. Registered as a temporary project autoload only while running this
|
|
# test — see the test's own header for why an autoload (rather than a
|
|
# scene-child driver) is needed: main_menu.tscn IS the real current_scene
|
|
# here (run directly via --path Game res://scenes/main_menu.tscn, exactly
|
|
# like production), so unlike lobby_smoke.gd's driver this doesn't even
|
|
# need to survive a scene swap — it just needs to exist independently of
|
|
# main_menu.gd so main_menu.gd itself stays untouched by test concerns.
|
|
|
|
var _role := ""
|
|
|
|
|
|
func _ready() -> void:
|
|
for arg in OS.get_cmdline_user_args():
|
|
if arg.begins_with("--role="):
|
|
_role = arg.substr("--role=".length())
|
|
if _role.is_empty():
|
|
return
|
|
# main_menu.gd's own _ready() (which builds %-unique-name refs) must run
|
|
# before we touch its nodes; autoloads run first, so wait a frame.
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
match _role:
|
|
"host":
|
|
_run_host()
|
|
"join_ok":
|
|
_run_join_ok()
|
|
"join_refused":
|
|
_run_join_refused()
|
|
"join_cancel":
|
|
_run_join_cancel()
|
|
|
|
|
|
func _run_host() -> void:
|
|
var menu := get_tree().current_scene
|
|
var host_btn: Button = menu.get_node("CenterContainer/VBoxContainer/HostButton")
|
|
host_btn.emit_signal("pressed")
|
|
await get_tree().create_timer(1.0).timeout
|
|
var scene := get_tree().current_scene
|
|
var ok := scene != null and scene.scene_file_path == "res://scenes/lobby.tscn"
|
|
print("SMOKE %s: host -> current_scene=%s" % ["PASS" if ok else "FAIL", scene.scene_file_path if scene else "null"])
|
|
if not ok:
|
|
_finish(false, "host transition check failed")
|
|
return
|
|
# Stay up long enough for a separate join_ok/join_refused/join_cancel
|
|
# process (started after this one) to actually exercise the host —
|
|
# unlike _finish()'s normal 0.3s beat, this test's whole point is being
|
|
# a live target for a while.
|
|
await get_tree().create_timer(6.0).timeout
|
|
_finish(true, "host ran and stayed up for a joiner")
|
|
|
|
|
|
func _run_join_ok() -> void:
|
|
# Give the host process (started first by the shell script) time to be listening.
|
|
await get_tree().create_timer(1.5).timeout
|
|
var menu := get_tree().current_scene
|
|
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
|
|
address_edit.text = "127.0.0.1"
|
|
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
|
|
join_btn.emit_signal("pressed")
|
|
var overlay: Control = menu.get_node("%ConnectingOverlay")
|
|
print("SMOKE INFO: overlay visible right after Join press = %s" % str(overlay.visible))
|
|
await get_tree().create_timer(2.0).timeout
|
|
var scene := get_tree().current_scene
|
|
var ok := scene != null and scene.scene_file_path == "res://scenes/lobby.tscn"
|
|
_finish(ok, "join_ok -> current_scene=%s" % (scene.scene_file_path if scene else "null"))
|
|
|
|
|
|
func _run_join_refused() -> void:
|
|
var menu := get_tree().current_scene
|
|
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
|
|
address_edit.text = "127.0.0.1"
|
|
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
|
|
join_btn.emit_signal("pressed")
|
|
var overlay: Control = menu.get_node("%ConnectingOverlay")
|
|
print("SMOKE INFO: overlay visible right after Join press (no server) = %s" % str(overlay.visible))
|
|
# main_menu.gd's own CONNECT_TIMEOUT_SECONDS (6.0) is what actually
|
|
# bounds this now — ENet's own connection_failed proved unbounded in
|
|
# practice against a genuinely refused loopback connection.
|
|
await get_tree().create_timer(8.0).timeout
|
|
var error_label: Label = menu.get_node("%MultiplayerErrorLabel")
|
|
var still_on_menu := get_tree().current_scene == menu
|
|
var ok := still_on_menu and not overlay.visible and error_label.visible
|
|
_finish(ok, "join_refused -> still_on_menu=%s overlay_visible=%s error_visible=%s error_text='%s'" % [
|
|
str(still_on_menu), str(overlay.visible), str(error_label.visible), error_label.text
|
|
])
|
|
|
|
|
|
func _run_join_cancel() -> void:
|
|
var menu := get_tree().current_scene
|
|
var address_edit: LineEdit = menu.get_node("%JoinAddressEdit")
|
|
address_edit.text = "10.255.255.1" # non-routable; connect attempt just hangs until timeout/cancel
|
|
var join_btn: Button = menu.get_node("CenterContainer/VBoxContainer/JoinRow/JoinButton")
|
|
join_btn.emit_signal("pressed")
|
|
var overlay: Control = menu.get_node("%ConnectingOverlay")
|
|
await get_tree().create_timer(0.5).timeout
|
|
var overlay_shown := overlay.visible
|
|
var cancel_btn: Button = menu.get_node("%ConnectingCancelButton")
|
|
cancel_btn.emit_signal("pressed")
|
|
await get_tree().create_timer(0.5).timeout
|
|
var still_on_menu := get_tree().current_scene == menu
|
|
var ok := overlay_shown and not overlay.visible and still_on_menu and not NetworkManager.is_client
|
|
_finish(ok, "join_cancel -> overlay_shown=%s overlay_now=%s still_on_menu=%s is_client=%s" % [
|
|
str(overlay_shown), str(overlay.visible), str(still_on_menu), str(NetworkManager.is_client)
|
|
])
|
|
|
|
|
|
func _finish(success: bool, message: String) -> void:
|
|
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)
|