# Render the selection screen with deterministic reference data; no login needed. extends SceneTree const Screen = preload("res://ui/char_select_screen.gd") class Client extends Node: func get_empire() -> int: return 3 func get_slot_count() -> int: return 4 func _initialize() -> void: call_deferred("_run") func _run() -> void: root.size = Vector2i(1472, 854) var output := OS.get_environment("MT_RENDER_OUTPUT") if output.is_empty(): output = ProjectSettings.globalize_path("res://../build/rendering/selection-%d-%d" % [Time.get_unix_time_from_system(), OS.get_process_id()]) DirAccess.make_dir_recursive_absolute(output) var hair_part := 0 var job := 0 var main_part := 20009 for arg in OS.get_cmdline_user_args(): if arg.begins_with("--hair="): hair_part = int(arg.trim_prefix("--hair=")) elif arg.begins_with("--job="): job = clampi(int(arg.trim_prefix("--job=")), 0, 7) elif arg.begins_with("--main-part="): main_part = max(0, int(arg.trim_prefix("--main-part="))) var client := Client.new() root.add_child(client) var screen := Screen.new() root.add_child(screen) screen.setup(client, AssetRoot.path(), [{ "index": 0, "name": "[SA]Admin", "job": job, "level": 105, "guild_name": "[GM-TEAM]", "play_minutes": 2180, "ht": 90, "iq": 15, "st": 90, "dx": 90, "main_part": main_part, "hair_part": hair_part, }]) for i in 40: await process_frame if screen._pv == null: printerr("FAIL: selection model did not load") quit(1) return # R2 diagnostics: compare the exact same armour with the configured hair # removed. This distinguishes an absent/incorrect face surface from a hair # depth or alpha problem; normal visual tests keep the real HairIndex 0. if "--no-hair" in OS.get_cmdline_user_args(): screen._pv.model.set("hair_gr2", "") for _i in 2: await process_frame print("Selected armor: ", screen._pv.model.get("gr2_path")) screen._pv.anim.set_process(false) screen._pv.anim.set("blend_time", 0.0) screen._pv.anim.set("time", 0.5) # Sample after freezing the reference pose: GPU deformation lives in the # shader, so its current bounds are requested by the same production framing # path rather than read back from the static ArrayMesh. screen._frame_model() print("Hair skin: ", screen._pv.model.get("hair_skin")) var ua = preload("res://ui/ui_assets.gd") var hair_image: Image = ua.load_dds_image(String(screen._pv.model.get("hair_skin"))) if hair_image != null: hair_image.save_png(output.path_join("hair-texture.png")) var mesh_bounds: Array = [] for mi in screen._pv.find_children("*", "MeshInstance3D", true, false): var local_aabb: AABB = mi.get_aabb() var world_aabb: AABB = mi.get_global_transform() * local_aabb mesh_bounds.append({ "node": String(mi.name), "local_position": [local_aabb.position.x, local_aabb.position.y, local_aabb.position.z], "local_size": [local_aabb.size.x, local_aabb.size.y, local_aabb.size.z], "world_position": [world_aabb.position.x, world_aabb.position.y, world_aabb.position.z], "world_size": [world_aabb.size.x, world_aabb.size.y, world_aabb.size.z], }) for surface in mi.mesh.get_surface_count(): print("SURFACE ", surface, " ", mi.mesh.surface_get_name(surface)) var mat: Material = mi.get_active_material(surface) if mat is ShaderMaterial: print("MATERIAL mode=", mat.get_shader_parameter("mode"), " alpha_write=", mat.shader.code.contains("ALPHA =")) if "--solid-body" in OS.get_cmdline_user_args(): var solid := StandardMaterial3D.new() solid.albedo_texture = mat.get_shader_parameter("albedo_tex") solid.cull_mode = BaseMaterial3D.CULL_DISABLED mi.set_surface_override_material(surface, solid) if main_part == 20009 and not String(screen._pv.model.get("gr2_path")).to_lower().contains("warrior_5_1"): printerr("FAIL: server armor was not applied") quit(1) return if hair_part == 1001 and not String(screen._pv.model.get("hair_gr2")).to_lower().contains("hair_2_1"): printerr("FAIL: standard HairIndex 1001 did not replace the default hair model") quit(1) return if DisplayServer.get_name() != "headless": await RenderingServer.frame_post_draw if root.get_texture().get_image().save_png(output.path_join("selection.png")) != OK: quit(1) return var hashes := {} for path in ["res://ui/char_select_screen.gd", "res://ui/player_view.gd", "res://ui/equip_model.gd", "res://ui/race_spec.gd", "res://char_select_visual_test.gd"]: hashes[path] = FileAccess.get_sha256(path) for key in ["gr2_path", "hair_gr2", "hair_skin"]: var path := String(screen._pv.model.get(key)) if not path.is_empty(): hashes[path] = FileAccess.get_sha256(path) var library := "res://bin/libmtgodot.macos.template_debug.dylib" hashes[library] = FileAccess.get_sha256(library) var report := FileAccess.open(output.path_join("report.json"), FileAccess.WRITE) report.store_string(JSON.stringify({"sha256": hashes, "engine": Engine.get_version_info(), "job": job, "main_part": main_part, "hair_part": hair_part, "sample_time": 0.5, "args": OS.get_cmdline_user_args(), "gpu_skin": OS.get_environment("MTGODOT_GPUSKIN"), "camera_position": [_cam_pos(screen).x, _cam_pos(screen).y, _cam_pos(screen).z], "mesh_bounds": mesh_bounds, "scope": "assembly assertions; screenshot requires visual review, not 40250 pixel parity"}, "\t")) report.close() print("SELECTION_OUTPUT: ", output) print("PASS: selection model, server armor and UI assembly") quit() func _cam_pos(screen: Node) -> Vector3: return screen._cam.position if screen._cam else Vector3.ZERO