class_name M2AnimationResourceFinalizer extends RefCounted ## Polls cached animated M2 ResourceLoader requests and finalizes terminal scenes. ## The caller owns permits, material-source lookup and SceneTree materialization. const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload( "res://src/render/m2/m2_animated_scene_finalizer.gd" ) var _animated_scene_finalizer: RefCounted var _resource_loader_adapter: Object func _init( animated_scene_finalizer: RefCounted = null, resource_loader_adapter: Object = null ) -> void: _animated_scene_finalizer = animated_scene_finalizer if _animated_scene_finalizer == null: _animated_scene_finalizer = M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new() _resource_loader_adapter = resource_loader_adapter ## Moves terminal threaded requests into the pipeline finalize FIFO. ## Empty resource paths retain the historical immediate static-only outcome. func poll_terminal_requests( load_pipeline_state: RefCounted, prototype_cache_state: RefCounted ) -> int: if load_pipeline_state == null or prototype_cache_state == null: return 0 var completed_request_count := 0 var request_records: Array = load_pipeline_state.call( "request_records_snapshot" ) for request_variant in request_records: if not (request_variant is Dictionary): continue var request: Dictionary = request_variant var normalized_relative_path := String(request.get("normalized", "")) var resource_path := String(request.get("path", "")) if resource_path.is_empty(): load_pipeline_state.call( "discard_request", normalized_relative_path ) prototype_cache_state.call( "mark_animation_static", normalized_relative_path ) completed_request_count += 1 continue var load_status := load_threaded_get_status(resource_path) if ( load_status != ResourceLoader.THREAD_LOAD_LOADED and load_status != ResourceLoader.THREAD_LOAD_FAILED ): continue load_pipeline_state.call( "complete_request", normalized_relative_path, load_status ) completed_request_count += 1 return completed_request_count ## Pops one terminal record and prepares a detached animated scene candidate. ## An empty result means the record was skipped or resolved as static-only. func prepare_next_candidate( load_pipeline_state: RefCounted, prototype_cache_state: RefCounted ) -> Dictionary: if load_pipeline_state == null or prototype_cache_state == null: return {} var terminal_record: Dictionary = load_pipeline_state.call( "pop_finalize_record" ) if terminal_record.is_empty(): return {} var normalized_relative_path := String( terminal_record.get("normalized", "") ) if ( normalized_relative_path.is_empty() or bool(prototype_cache_state.call( "has_animated_prototype", normalized_relative_path )) or bool(prototype_cache_state.call( "is_animation_static", normalized_relative_path )) ): return {} if int(terminal_record.get( "status", ResourceLoader.THREAD_LOAD_FAILED )) != ResourceLoader.THREAD_LOAD_LOADED: prototype_cache_state.call( "mark_animation_static", normalized_relative_path ) return {} var resource_path := String(terminal_record.get("path", "")) var loaded_resource := load_threaded_get(resource_path) var candidate := _animated_scene_finalizer.call( "instantiate_candidate", loaded_resource ) as Node3D if candidate == null: prototype_cache_state.call( "mark_animation_static", normalized_relative_path ) return {} return { "normalized": normalized_relative_path, "path": resource_path, "candidate": candidate, } ## Repairs, validates and adopts one prepared candidate. The preparation must be ## completed synchronously so its detached candidate always receives an owner. func finalize_prepared_candidate( preparation: Dictionary, material_source_root: Node3D, prototype_cache_state: RefCounted, debug_logging_enabled: bool = false ) -> Node3D: if prototype_cache_state == null: return null var normalized_relative_path := String(preparation.get("normalized", "")) var candidate := preparation.get("candidate", null) as Node3D if normalized_relative_path.is_empty() or candidate == null: return null _animated_scene_finalizer.call( "repair_materials", candidate, material_source_root ) var finalization: Dictionary = _animated_scene_finalizer.call( "finalize_candidate", candidate ) var prototype := finalization.get("prototype", null) as Node3D if prototype == null: prototype_cache_state.call( "mark_animation_static", normalized_relative_path ) return null var canonical_prototype := prototype_cache_state.call( "adopt_animated_prototype", normalized_relative_path, prototype ) as Node3D if debug_logging_enabled: print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [ normalized_relative_path, String(preparation.get("path", "")), int(finalization.get("animation_player_count", 0)), ]) return canonical_prototype ## Production ResourceLoader status adapter; injectable in synthetic tests. func load_threaded_get_status(resource_path: String) -> int: if _resource_loader_adapter != null: return int(_resource_loader_adapter.call( "load_threaded_get_status", resource_path )) return ResourceLoader.load_threaded_get_status(resource_path) ## Production ResourceLoader terminal-result adapter; injectable in tests. func load_threaded_get(resource_path: String) -> Resource: if _resource_loader_adapter != null: return _resource_loader_adapter.call( "load_threaded_get", resource_path ) as Resource return ResourceLoader.load_threaded_get(resource_path)