extends SceneTree const MainScene: PackedScene = preload("res://main/main.tscn") const TEST_PORT: int = 18194 const LATE_MESSAGE: String = "late join visibility check" const CLEAR_MESSAGE: String = "late join animation clear check" const ACTIVE_ACTION: StringName = &"draw" func _initialize() -> void: call_deferred("_run") func _run() -> void: var arguments: PackedStringArray = OS.get_cmdline_user_args() if arguments.has("host"): await _run_host() elif arguments.has("first"): await _run_first_client() elif arguments.has("late"): await _run_late_client() else: push_error("Late-join validation needs host, first, or late mode.") quit(1) func _run_host() -> void: var main: Node = await _create_initialized_main() var session := main.get_node("%NetworkSession") as NetworkSession var save_manager := main.get("_save_manager") as PlayerSaveManager assert(bool(main.call( "_apply_world", WorldLayout.STARTER_ISLAND, NetworkProtocol.DEFAULT_WORLD_SEED, true, ))) assert(session.set_host_world( WorldLayout.STARTER_ISLAND, NetworkProtocol.DEFAULT_WORLD_SEED, )) assert(session.start_private_host(TEST_PORT)) assert(save_manager.initialize_new_game( NetworkProtocol.DEFAULT_WORLD_SEED, WorldLayout.STARTER_ISLAND, )) main.call("_enter_gameplay") for _frame: int in 4: await physics_frame assert(session.set_host_open(true)) assert(await _wait_for_registry_size(session, 3)) var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService assert(spawn.get_peer_ids().size() == 3) var action_peer_id: int = await _wait_for_remote_action( spawn, ACTIVE_ACTION, ) assert(action_peer_id > 1) assert(await _wait_for_avatar_bool( spawn, action_peer_id, &"_active_item_is_net", true, )) assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true)) assert(await _wait_for_avatar_action( spawn, action_peer_id, &"", )) assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false)) var chat := main.get_node("%NetworkChatService") as NetworkChatService assert(await _wait_for_history_message(chat, CLEAR_MESSAGE)) print("Late-join multiplayer host validation: PASS") await _cleanup(main, session) func _run_first_client() -> void: var main: Node = await _create_initialized_main() var session := await _join(main) var profile := main.get_node("%NetworkProfileService") as NetworkProfileService var preview: Dictionary = profile.get_persisted_appearance().duplicate(true) preview["scale"] = 0.9 assert(profile.preview_appearance(preview)) var player := main.get("_player") as Player assert(player != null) var floor_deadline: int = Time.get_ticks_msec() + 5000 while Time.get_ticks_msec() < floor_deadline and not player.is_on_floor(): await physics_frame assert(player.is_on_floor()) player.toggle_sitting() assert(player.is_sitting()) assert(player.bag.add_item(&"crab_net")) # Slot zero begins selected with the starter rod. Replacing that slot does # not emit a selection change, so use the next slot to exercise the same # equipped-item path a player uses in normal gameplay. assert(player.hotbar.assign_item(1, &"crab_net")) assert(player.hotbar.select_slot(1)) assert(await _wait_for_local_net(main, player)) assert(player.begin_animation_action(ACTIVE_ACTION)) await create_timer(0.5).timeout assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION) var chat := main.get_node("%NetworkChatService") as NetworkChatService var deadline: int = Time.get_ticks_msec() + 15000 while Time.get_ticks_msec() < deadline: await process_frame if ( session.get_authenticated_peer_ids().size() == 3 and _history_contains(chat, LATE_MESSAGE) ): break assert(session.get_authenticated_peer_ids().size() == 3) assert(_history_contains(chat, LATE_MESSAGE)) var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService assert(spawn.get_peer_ids().size() == 3) # Keep the action active until the late peer has joined and announced that # it has received the lifecycle snapshot. This exercises client -> host -> # existing client/late client replication, including the join-in-progress # state carried by the spawn envelope. await create_timer(1.0).timeout player.end_animation_action() player.toggle_sitting() assert(not player.is_sitting()) assert(await _wait_for_history_message(chat, CLEAR_MESSAGE)) print("Late-join first-client validation: PASS") await _cleanup(main, session) func _run_late_client() -> void: var main: Node = await _create_initialized_main() var session := await _join(main) var deadline: int = Time.get_ticks_msec() + 8000 while ( Time.get_ticks_msec() < deadline and session.get_authenticated_peer_ids().size() < 3 ): await process_frame assert(session.get_authenticated_peer_ids().size() == 3) var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService assert(spawn.get_peer_ids().size() == 3) var action_peer_id: int = await _wait_for_remote_action( spawn, ACTIVE_ACTION, ) assert(action_peer_id > 1) assert(await _wait_for_avatar_bool( spawn, action_peer_id, &"_active_item_is_net", true, )) assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true)) var chat := main.get_node("%NetworkChatService") as NetworkChatService assert(chat.send_local_message(LATE_MESSAGE)) assert(await _wait_for_avatar_action( spawn, action_peer_id, &"", )) assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false)) assert(chat.send_local_message(CLEAR_MESSAGE)) await create_timer(0.5).timeout print("Late-join late-client validation: PASS") await _cleanup(main, session) func _join(main: Node) -> NetworkSession: main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT) var session := main.get_node("%NetworkSession") as NetworkSession var deadline: int = Time.get_ticks_msec() + 20000 while Time.get_ticks_msec() < deadline: await process_frame if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY: main.call("_confirm_server_trust") if session.is_joined_client() and bool(main.get("_gameplay_started")): return session assert(false, "Client did not join in time.") return session func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool: var deadline: int = Time.get_ticks_msec() + 25000 while Time.get_ticks_msec() < deadline: await process_frame if session.get_authenticated_peer_ids().size() == expected: return true return false func _wait_for_local_net(main: Node, player: Player) -> bool: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: # A joined client can finish its world transition one frame after the # hotbar selection signal. Refresh through the production path until the # ready fishing state accepts the equipped item. main.call("_refresh_active_hotbar_item") await process_frame if bool(player.get("_active_item_is_net")): return true var fishing_spot: Node = main.get_node("%FishingSpot") print( "net equip wait timed out: ", { "fishing_state": int(fishing_spot.get("state")), "selected_slot": player.hotbar.get_selected_slot(), "selected_item": String(player.hotbar.get_selected_item_id()), "owns_net": player.bag.owns_item(&"crab_net"), }, ) return false func _history_contains(service: NetworkChatService, body: String) -> bool: return service.get_history().any( func(message: Dictionary) -> bool: return str(message.get("body", "")) == body ) func _wait_for_history_message( service: NetworkChatService, body: String, ) -> bool: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: await process_frame if _history_contains(service, body): return true return false func _wait_for_remote_action( spawn_service: PlayerSpawnService, action_id: StringName, ) -> int: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: await process_frame for peer_id: int in spawn_service.get_peer_ids(): var avatar: Player = spawn_service.get_avatar(peer_id) if ( avatar != null and _observed_action_id(avatar) == action_id ): return peer_id print( "animation wait timed out: ", _action_debug_snapshot(spawn_service), ) return 0 func _wait_for_avatar_action( spawn_service: PlayerSpawnService, peer_id: int, action_id: StringName, ) -> bool: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: await process_frame var avatar: Player = spawn_service.get_avatar(peer_id) if ( avatar != null and _observed_action_id(avatar) == action_id ): return true print( "animation clear wait timed out: ", _action_debug_snapshot(spawn_service), ) return false func _wait_for_avatar_bool( spawn_service: PlayerSpawnService, peer_id: int, property_name: StringName, expected: bool, ) -> bool: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: await process_frame var avatar: Player = spawn_service.get_avatar(peer_id) if avatar != null and bool(avatar.get(property_name)) == expected: return true return false func _wait_for_avatar_sitting( spawn_service: PlayerSpawnService, peer_id: int, expected: bool, ) -> bool: var deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < deadline: await process_frame var avatar: Player = spawn_service.get_avatar(peer_id) if avatar != null and avatar.is_sitting() == expected: return true var avatar: Player = spawn_service.get_avatar(peer_id) if avatar != null: print( "sitting wait timed out: ", { "peer_id": peer_id, "expected": expected, "sitting": avatar.is_sitting(), "sit_after_landing": bool(avatar.get("_sit_after_landing")), "on_floor": avatar.is_on_floor(), "position": avatar.global_position, "velocity": avatar.velocity, "authoritative": bool(avatar.get( "_network_authoritative_simulation" )), "last_input": int(avatar.get( "_last_network_input_sequence" )), }, ) return false func _observed_action_id(avatar: Player) -> StringName: return StringName(str(avatar.get( "_animation_action_id" if bool(avatar.get("_network_authoritative_simulation")) else "_network_target_animation_action_id" ))) func _action_debug_snapshot(spawn_service: PlayerSpawnService) -> Dictionary: var result: Dictionary = {} for peer_id: int in spawn_service.get_peer_ids(): var avatar: Player = spawn_service.get_avatar(peer_id) if avatar == null: continue result[peer_id] = { "authoritative": bool(avatar.get( "_network_authoritative_simulation" )), "source": str(avatar.get("_animation_action_id")), "target": str(avatar.get( "_network_target_animation_action_id" )), "last_input": int(avatar.get("_last_network_input_sequence")), } return result func _create_initialized_main() -> Node: root.size = Vector2i(1280, 720) var main: Node = MainScene.instantiate() root.add_child(main) for _frame: int in 4: await process_frame if not bool(main.get("_application_initialized")): main.call("_activate_selected_data_path", "", true) for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) return main func _cleanup(main: Node, session: NetworkSession) -> void: session.disconnect_session("") main.queue_free() for _frame: int in 4: await process_frame await create_timer(0.1).timeout quit()