extends SceneTree ## Client process for the multiplayer avatar replication test ## (scripts/run_multiplayer_avatar_test.sh). ## ## Role "b": Uncustomized character (no hair, no attachments). ## Observes Client A. Verifies Client A's remote model HAS hair/attachments ## and does NOT have a solid material_override tint. ## ## Role "a": Customized character (with hair + chest). ## Observes Client B. Verifies Client B's remote model has NO hair/attachments ## and does NOT have a solid material_override tint. var role := "b" var port := 7789 func _initialize() -> void: var args := OS.get_cmdline_user_args() for i in args.size(): if args[i] == "--role" and i + 1 < args.size(): role = args[i + 1] elif args[i] == "--port" and i + 1 < args.size(): port = int(args[i + 1]) _run() func _run() -> void: await process_frame var avatars = root.get_node_or_null("AvatarSystem") if avatars == null: print("RESULT %s avatar_system_missing" % role) quit(1) return await avatars.ensure_catalog() var bases: Array = avatars.get_assets_for_slot("base_body") if bases.is_empty(): print("RESULT %s no_base_bodies" % role) quit(1) return if role == "a": # Machine A: customized character (hair + chest) avatars.set_loadout_from_dict({ "base_body": bases[0], "slots": { "hair": "hair_ponytail_01", "chest": "chest_bandit_01", } }) else: # Machine B: uncustomized character (no attachments) avatars.set_loadout_from_dict({ "base_body": bases[0], "slots": {} }) var session = root.get_node("Session") session.begin(1, "127.0.0.1", port) change_scene_to_file("res://scenes/game.tscn") for i in 120: await process_frame var game := current_scene var netc = game.get_node_or_null("NetworkClient") if game else null if netc == null or not netc.is_connected_to_server(): print("RESULT %s not_connected" % role) quit(1) return print("RESULT %s in_world" % role) # Search for remote player var remote_node: Node3D = null for i in 1200: await process_frame for c in game.get_children(): if c is Node3D and c.name.begins_with("Remote_"): var vis = c.get_node_or_null("Visual") if vis != null: remote_node = c as Node3D break if remote_node != null: # Give it 10 frames to ensure loadout replication snapshot has arrived and applied for j in 10: await process_frame break if remote_node == null: print("RESULT %s remote_not_found" % role) quit(1) return var visual: Node = remote_node.get_node_or_null("Visual") if visual == null: print("RESULT %s visual_not_found" % role) quit(1) return var skel: Skeleton3D = visual.find_child("Skeleton3D", true, false) as Skeleton3D if skel == null: print("RESULT %s skeleton_not_found" % role) quit(1) return var mount_count := 0 for c in skel.get_children(): if c is BoneAttachment3D: mount_count += 1 var has_override := false var meshes: Array[MeshInstance3D] = [] _gather_meshes(visual, meshes) for m in meshes: if m.material_override != null: has_override = true break if role == "b": # Observer B observing A: A must have customizations (mounts >= 2) and no tint var a_customized := mount_count >= 2 var tinted := has_override print("RESULT b mounts=%d tinted=%s ok=%s" % [mount_count, str(tinted), str(a_customized and not tinted)]) # Stay connected long enough for A to finish its verification for i in 300: await process_frame quit(0 if (a_customized and not tinted) else 1) else: # Observer A observing B: B must NOT have customizations (mounts == 0) and no tint var b_uncustomized := mount_count == 0 var tinted := has_override print("RESULT a mounts=%d tinted=%s ok=%s" % [mount_count, str(tinted), str(b_uncustomized and not tinted)]) quit(0 if (b_uncustomized and not tinted) else 1) func _gather_meshes(node: Node, out: Array[MeshInstance3D]) -> void: if node is MeshInstance3D: out.append(node as MeshInstance3D) for c in node.get_children(): _gather_meshes(c, out)