122 lines
5.4 KiB
GDScript
122 lines
5.4 KiB
GDScript
# 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
|