extends Node # Task 6.2 / 6.7: black-box client for the exported dedicated-server smoke. # It contains no in-process server hook: two copies run in distinct containers, # join through ENet, and pass only after an authoritative score RPC arrives. const DEFAULT_PORT := 7777 const TIMEOUT_SECONDS := 55.0 var _address := "server" var _port := DEFAULT_PORT var _name := "ExportSmoke" var _expected_goals := 1 var _goals_observed := 0 @onready var _network_manager: Node = get_node("/root/NetworkManager") @onready var _match_net: Node = get_node("/root/MatchNet") @onready var _match_sim: Node = get_node("/root/MatchSim") func _ready() -> void: # The client enters networked_match.tscn after the hello handshake. Keep # this observer outside that scene so the scene transition cannot free it # before the authoritative score RPC arrives. Deferring avoids reparenting # while Godot is still adding the smoke scene to the tree. call_deferred("_move_to_root") for arg in OS.get_cmdline_user_args(): if arg.begins_with("--address="): _address = arg.get_slice("=", 1) elif arg.begins_with("--port="): _port = int(arg.get_slice("=", 1)) elif arg.begins_with("--name="): _name = arg.get_slice("=", 1) elif arg.begins_with("--expected-goals="): _expected_goals = maxi(1, int(arg.get_slice("=", 1))) # Use node paths instead of autoload identifiers: this test deliberately # runs from a clean source tree, before Godot has an editor-generated cache. _match_net.set("local_player_name", _name) _match_net.connect("welcomed", _on_welcomed) _match_sim.connect("score_update_received", _on_score_update) get_tree().create_timer(TIMEOUT_SECONDS).timeout.connect(_on_timeout) # ENet's connection state machine handles a server still booting. Joining # immediately also ensures MatchSim never runs a physics tick on an inactive # MultiplayerPeer while a timer waits to make the first connection attempt. _connect() func _move_to_root() -> void: var parent := get_parent() if parent == null: return var tree := get_tree() parent.remove_child(self) tree.root.add_child(self) func _process(_delta: float) -> void: _network_manager.call("poll") func _physics_process(_delta: float) -> void: _network_manager.call("poll") func _connect() -> void: var err: int = _network_manager.call("join", _address, _port) if err != OK: _fail("join(%s:%d) failed: %s" % [_address, _port, error_string(err)]) func _on_welcomed() -> void: _match_net.disconnect("welcomed", _on_welcomed) get_tree().change_scene_to_file.call_deferred(ScenePaths.NETWORKED_MATCH) func _on_score_update(score: Dictionary) -> void: if int(score.get(0, 0)) + int(score.get(1, 0)) < 1: return _goals_observed += 1 if _goals_observed < _expected_goals: print("EXPORT SMOKE: %s observed match %d/%d score %s" % [_name, _goals_observed, _expected_goals, str(score)]) return print("EXPORT SMOKE PASS: %s observed %d authoritative goals" % [_name, _goals_observed]) # Give the reliable goal/state messages one beat to settle before this peer # leaves, then let the server's zero-reservation test config abort/drain. get_tree().create_timer(0.5).timeout.connect(func() -> void: get_tree().quit(0)) func _on_timeout() -> void: if _goals_observed < _expected_goals: _fail("%s timed out without an authoritative goal" % _name) func _fail(message: String) -> void: printerr("EXPORT SMOKE FAIL: " + message) _network_manager.call("shutdown") get_tree().quit(1)