extends SceneTree ## 鬼木林 / 赤鬼木林树怪的离线资源和 CPU/GPU 动作验收。 ## 这不是服务器刷新证明;真实地图中的选中、战斗和掉落由 playable_live_test 承担。 const MobView = preload("res://ui/mob_view.gd") const RACES := [2301, 2302, 2303, 2304, 2305, 2306, 2307, 2311, 2312, 2313, 2314, 2315] const STATES := ["wait", "run", "attack", "damage", "dead"] const SIZE := 640 var _output := "" var _failures: Array[String] = [] var _pose_bounds := {} var _results: Array[Dictionary] = [] func _initialize() -> void: call_deferred("run") func _fail(message: String) -> void: _failures.append(message) printerr("FOREST MOB FAIL: " + message) func run() -> void: _output = OS.get_environment("MT_RENDER_OUTPUT") if _output.is_empty(): _output = ProjectSettings.globalize_path("res://../build/rendering/forest-mobs-%d" % Time.get_unix_time_from_system()) DirAccess.make_dir_recursive_absolute(_output) root.size = Vector2i(SIZE, SIZE) _build_lighting() var cam := Camera3D.new() cam.fov = 38.0 cam.near = 0.02 root.add_child(cam) cam.make_current() for gpu in ["0", "1"]: OS.set_environment("MTGODOT_GPUSKIN", gpu) for race in RACES: await _check_race(race, gpu, cam) OS.set_environment("MTGODOT_GPUSKIN", "0") var report := {"passed": _failures.is_empty(), "failures": _failures, "races": RACES, "states": STATES, "gpu_modes": ["0", "1"], "results": _results, "assets": AssetRoot.path(), "viewport": str(root.size), "headless": DisplayServer.get_name() == "headless", "scope": "offline tree-monster assembly; no server spawn or 40250 pixel parity"} var file := FileAccess.open(_output.path_join("report.json"), FileAccess.WRITE) if file: file.store_string(JSON.stringify(report, "\t")) file.close() print("FOREST MOB RENDER: ", JSON.stringify({"passed": _failures.is_empty(), "failures": _failures.size(), "results": _results.size(), "output": _output})) quit(0 if _failures.is_empty() else 1) func _check_race(race: int, gpu: String, cam: Camera3D) -> void: var view := MobView.new() view.name = "ForestMob_%d_%s" % [race, gpu] root.add_child(view) if not view.build(AssetRoot.path(), null, race): _fail("race=%d GPU=%s build returned false" % [race, gpu]) view.queue_free() await process_frame return var row := {"race": race, "gpu_skin": gpu == "1", "states": {}} for state in STATES: var requested := view.get_motion_path(state) if requested.is_empty() or not FileAccess.file_exists(requested): _fail("race=%d GPU=%s state=%s has no resolved .msa" % [race, gpu, state]) continue view.set_anim_state(state) if view.anim == null: _fail("race=%d GPU=%s state=%s has no animation player" % [race, gpu, state]) continue view.anim.set_process(true) await process_frame var actual := String(view.anim.get("anim_path")) if actual != requested: _fail("race=%d GPU=%s state=%s resolved %s but played %s" % [race, gpu, state, requested, actual]) var duration := float(view.anim.call("get_duration")) if view.anim.has_method("get_duration") else 1.0 var fraction := 0.5 if state == "dead": fraction = 0.95 view.anim.call("set_time", maxf(0.0, duration * fraction)) view.anim.set_process(false) await process_frame var bounds: AABB = view.model.call("get_visual_aabb") if not bounds.position.is_finite() or not bounds.size.is_finite() or bounds.size.length() <= 0.0: _fail("race=%d GPU=%s state=%s has invalid visual AABB" % [race, gpu, state]) _pose_bounds["%d/%s" % [race, state]] = bounds if gpu == "0" else _pose_bounds.get("%d/%s" % [race, state], AABB()) if gpu == "1": var cpu_bounds: AABB = _pose_bounds.get("%d/%s" % [race, state], AABB()) var tolerance := maxf(0.01, maxf(cpu_bounds.size.length(), bounds.size.length()) * 0.005) if cpu_bounds.size.length() <= 0.0 or cpu_bounds.position.distance_to(bounds.position) > tolerance or cpu_bounds.size.distance_to(bounds.size) > tolerance: _fail("race=%d state=%s CPU/GPU AABB differs beyond %.4fm" % [race, state, tolerance]) var image_name := "" if DisplayServer.get_name() != "headless": var centre: Vector3 = view.model.global_transform * bounds.get_center() var visual_size: float = bounds.size.length() * view.model.scale.length() / sqrt(3.0) var distance: float = maxf(3.0, visual_size * 1.8) cam.position = centre + Vector3(distance * 0.35, distance * 0.22, distance) cam.look_at(centre, Vector3.UP) await RenderingServer.frame_post_draw image_name = "mob-%d-%s.png" % [race, state] if gpu == "1" else "mob-%d-%s-cpu.png" % [race, state] if root.get_texture().get_image().save_png(_output.path_join(image_name)) != OK: _fail("race=%d state=%s screenshot save failed" % [race, state]) row.states[state] = {"requested_motion": requested, "resolved_motion": actual, "duration": duration, "sample_fraction": fraction, "aabb_position": [bounds.position.x, bounds.position.y, bounds.position.z], "aabb_size": [bounds.size.x, bounds.size.y, bounds.size.z], "screenshot": image_name} _results.append(row) view.queue_free() await process_frame func _build_lighting() -> void: var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-42, 155, 0) sun.light_energy = 1.1 root.add_child(sun) var env := WorldEnvironment.new() var settings := Environment.new() settings.background_mode = Environment.BG_COLOR settings.background_color = Color(0.08, 0.1, 0.14) settings.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR settings.ambient_light_color = Color(0.55, 0.58, 0.65) settings.ambient_light_energy = 0.8 env.environment = settings root.add_child(env)