extends SceneTree const PlayerView = preload("res://ui/player_view.gd") var failures := 0 func check(ok: bool, message: String) -> void: if not ok: failures += 1 printerr("FAIL: " + message) func _init() -> void: call_deferred("run") func run() -> void: Metin2AnimPlayer.clear_clip_cache() var a := PlayerView.new() var b := PlayerView.new() for view in [a, b]: check(view.build(AssetRoot.path(), 0), "warrior builds") root.add_child(view) view.anim.set_process(false) var initial_paths := {} for view in [a, b]: initial_paths[String(view.anim.get("anim_path"))] = true check(Metin2AnimPlayer.get_clip_cache_stats().misses == initial_paths.size(), "two actors decode only the distinct weighted idle resources") a.anim.set_time(0.3) b.anim.set_time(0.9) check(not is_equal_approx(a.anim.get_time(), b.anim.get_time()), "playback clocks remain independent") # 40250 refuses to interrupt an active one-shot with a locomotion motion, so # repeatedly calling set_anim_state() is not a cache stress test. Switch # the same three registered resources explicitly to exercise the hot reload # path while preserving the actor-level interruption gate. # WAIT is a weighted RaceData vector; asking _motion_file("wait") again # would roll a new resource and make the cache expectation nondeterministic. # Reuse the exact wait path already bound by actor A instead. var hot_paths: Array[String] = [a._motion_file("run"), a._motion_file("attack"), String(a.anim.get("anim_path"))] var expected_paths := {} for view in [a, b]: expected_paths[String(view.anim.get("anim_path"))] = true for path in hot_paths: expected_paths[path] = true for i in 3: for path in hot_paths: for view in [a, b]: view.anim.set("anim_path", path) view.anim.set("playing", true) view.anim.set_time(0.2) var stats: Dictionary = Metin2AnimPlayer.get_clip_cache_stats() var expected_misses := expected_paths.size() check(stats.misses == expected_misses and stats.hits >= 20 - expected_misses, "hot motion switches reuse decoded clips across weighted wait variants") check(stats.entries <= 32 and stats.section_bytes <= 64 * 1024 * 1024, "cache has entry and decoded-section budget") check(stats.msa_misses == expected_misses and stats.msa_hits >= 20 - expected_misses, "hot switches reuse parsed MSA metadata across weighted wait variants") check(stats.msa_entries <= 128, "MSA cache has an entry limit") Metin2AnimPlayer.clear_clip_cache() check(Metin2AnimPlayer.get_clip_cache_stats().msa_entries == 0, "MSA cache clears with clips") a.anim.set_time(0.5) check(a.model.get_visual_aabb().size.length() > 0, "clearing cache does not invalidate active clip") a.free() b.free() await process_frame print("animation_cache_test: failures=%d stats=%s" % [failures, stats]) quit(1 if failures else 0)