# Offline real-resource stress/viewport report; NOT 40250 visual parity or # server reconnect acceptance. Run with a renderer, --quit-after 3600. extends SceneTree const GameScene = preload("res://game_scene.gd") const Fixtures = preload("res://gamescene_test.gd") const PlayerView = preload("res://ui/player_view.gd") const MobView = preload("res://ui/mob_view.gd") class ScenarioClient extends Fixtures.FakeClient: signal warp(pos: Vector3, same_server: bool) signal world_reset() var failures: Array[String] = [] var output := "" func check(ok: bool, label: String) -> void: if not ok: failures.append(label) printerr("FAIL: " + label) func _init() -> void: call_deferred("run") func run() -> void: var run_start := Time.get_ticks_msec() var sample_frames := 90 if OS.has_environment("MT_RENDER_SAMPLE_FRAMES"): sample_frames = clampi(int(OS.get_environment("MT_RENDER_SAMPLE_FRAMES")), 90, 36000) output = OS.get_environment("MT_RENDER_OUTPUT") if output == "": output = ProjectSettings.globalize_path("res://../build/rendering/scenarios-followup") DirAccess.make_dir_recursive_absolute(output) root.size = Vector2i(1280, 720) var report := {"scope": "offline real maps, CPU/GPU animated crowd and viewport layout; no server writes or 40250 parity", "engine": Engine.get_version_info(), "backend": DisplayServer.get_name(), "samples": [], "sample_frames": sample_frames, "model_builds": []} var client := ScenarioClient.new() root.add_child(client) var scene := GameScene.new() root.add_child(scene) await scene.setup(client, AssetRoot.path(), "OutdoorA1/metin2_map_a1") client.main = 1000 client.ents[1000] = {"vid": 1000, "name": "Scenario", "race": 0, "pos_cm": Vector2(454600, 934000), "pos": Vector3(4546, 0, -9340), "is_main": true, "func": 0, "moving": false, "angle_deg": 0.0, "hp": 100, "max_hp": 100, "dead": false} for i in 30: await process_frame check(scene._model_built and scene._map_loaded(), "A1 main character and world loaded") var centre: Vector3 = scene.player.position for mode in ["0", "1"]: OS.set_environment("MTGODOT_GPUSKIN", mode) var crowd := Node3D.new() scene.add_child(crowd) for count in [1, 8, 16]: while crowd.get_child_count() < count: var index := crowd.get_child_count() var view := PlayerView.new() var build_start := Time.get_ticks_usec() check(view.build(AssetRoot.path(), index % 8), "crowd race %d build" % (index % 8)) report.model_builds.append({"gpu_skin": mode == "1", "race": index % 8, "build_ms": (Time.get_ticks_usec() - build_start) / 1000.0, "note": "synchronous build only; OS/shader caches may already be warm"}) crowd.add_child(view) view.position = centre + Vector3((index % 4 - 1.5) * 2, 0, (index / 4 + 1) * 2) view.position.y = scene.world.sample_height(view.position.x, view.position.z) view.set_anim_state("run") for i in 12: await process_frame var times: Array[float] = [] var switches: Array[float] = [] var memory_start := Performance.get_monitor(Performance.MEMORY_STATIC) var previous := Time.get_ticks_usec() for i in sample_frames: if i > 0 and i % 30 == 0: var switch_start := Time.get_ticks_usec() var state: String = ["run", "attack", "wait"][(i / 30) % 3] for view in crowd.get_children(): view.set_anim_state(state) switches.append((Time.get_ticks_usec() - switch_start) / 1000.0) await process_frame var now := Time.get_ticks_usec() times.append((now - previous) / 1000.0) previous = now for view in crowd.get_children(): var bounds: AABB = view.model.get_visual_aabb() check(bounds.position.is_finite() and bounds.size.is_finite() and bounds.size.length() > 0, "finite posed bounds after run/attack/wait blend") times.sort() switches.sort() report.samples.append({"gpu_skin": mode == "1", "extra_players": count, "median_ms": times[sample_frames / 2], "p95_ms": times[int((sample_frames - 1) * 0.95)], "max_ms": times[-1], "motion_switches": switches.size(), "motion_switch_max_ms": switches[-1], "memory_static_start": memory_start, "memory_static_end": Performance.get_monitor(Performance.MEMORY_STATIC), "clip_cache": Metin2AnimPlayer.get_clip_cache_stats(), "note": "%d window-frame intervals with repeated motion transitions; not GPU timestamp, RSS or FPS guarantee" % sample_frames}) print("SCENARIO_SAMPLE gpu=%s count=%d frames=%d max_ms=%.3f" % [mode, count, sample_frames, times[-1]]) crowd.free() await process_frame OS.set_environment("MTGODOT_GPUSKIN", "0") var mobs := Node3D.new() scene.add_child(mobs) for race in [101, 110, 20001]: var view := MobView.new() if not view.build(AssetRoot.path(), null, race): check(false, "NPC/monster build race=%d" % race) view.free() continue mobs.add_child(view) view.position = centre + Vector3(mobs.get_child_count() * 2, 0, 3) view.position.y = scene.world.sample_height(view.position.x, view.position.z) view.set_anim_state("wait") check(not view.model.get_visual_aabb().size.is_zero_approx(), "NPC/monster real geometry") for size in [Vector2i(1280, 720), Vector2i(1920, 1080), Vector2i(2560, 1440)]: root.size = size for i in 5: await process_frame if scene.quickbar and scene.quickbar._root: var rect: Rect2 = scene.quickbar._root.get_global_rect() check(Rect2(Vector2.ZERO, root.get_visible_rect().size).encloses(rect), "quickbar inside viewport %s" % size) if DisplayServer.get_name() != "headless": await RenderingServer.frame_post_draw check(root.get_texture().get_image().save_png(output.path_join("scene-%dx%d.png" % [size.x, size.y])) == OK, "screenshot saved") # Late server location update exercises the production map-correction path. client.ents[1000].pos_cm = Vector2(959109, 269267) client.ents[1000].pos = Vector3(9591.09, 0, -2692.67) client.world_reset.emit() client.warp.emit(client.ents[1000].pos, false) for i in 60: await process_frame check(scene.map_path == "OutdoorC1/metin2_map_c1" and scene._map_loaded(), "A1 to C1 map correction") report["final_map"] = scene.map_path scene.free() client.free() await process_frame report["failures"] = failures report["duration_seconds"] = (Time.get_ticks_msec() - run_start) / 1000.0 FileAccess.open(output.path_join("report.json"), FileAccess.WRITE).store_string(JSON.stringify(report, "\t")) print("rendering_scenario_test: failures=%d report=%s" % [failures.size(), output]) quit(1 if not failures.is_empty() else 0)