class_name M2CachedAnimationResourceObserver extends RefCounted ## Observes or requests the cached-GLB animated phase of one M2 build step. const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload( "res://src/render/m2/m2_build_resource_snapshot.gd" ) ## Returns a snapshot containing the exact cached prototype or pending state. ## Invalid composition and terminal failures produce a non-pending snapshot. func observe( normalized_relative_path: String, cache_directory: String, maximum_primitive_count: int, allowlist_patterns: PackedStringArray, denylist_patterns: PackedStringArray, prototype_cache_state: RefCounted, load_pipeline_state: RefCounted, debug_logging_enabled: bool = false ) -> RefCounted: var empty_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( normalized_relative_path, null, false ) if ( normalized_relative_path.is_empty() or prototype_cache_state == null or load_pipeline_state == null ): return empty_snapshot var cached_prototype := prototype_cache_state.call( "find_animated_prototype", normalized_relative_path ) as Node3D if cached_prototype != null: return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( normalized_relative_path, cached_prototype, false ) if bool(prototype_cache_state.call( "is_animation_static", normalized_relative_path )): return empty_snapshot if bool(load_pipeline_state.call("has_request", normalized_relative_path)): return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( normalized_relative_path, null, true ) var cache_resource_path := find_eligible_glb_cache_path( normalized_relative_path, cache_directory, maximum_primitive_count, allowlist_patterns, denylist_patterns, debug_logging_enabled ) if cache_resource_path.is_empty(): prototype_cache_state.call( "mark_animation_static", normalized_relative_path ) return empty_snapshot var request_error := ResourceLoader.load_threaded_request( cache_resource_path, "", false, ResourceLoader.CACHE_MODE_REUSE ) if request_error == OK or request_error == ERR_BUSY: load_pipeline_state.call( "remember_request", normalized_relative_path, cache_resource_path ) return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( normalized_relative_path, null, true ) prototype_cache_state.call("mark_animation_static", normalized_relative_path) return empty_snapshot ## Returns the first eligible historical `.glb` cache candidate. func find_eligible_glb_cache_path( normalized_relative_path: String, cache_directory: String, maximum_primitive_count: int, allowlist_patterns: PackedStringArray, denylist_patterns: PackedStringArray, debug_logging_enabled: bool = false ) -> String: if not is_animation_path_allowed( normalized_relative_path, allowlist_patterns, denylist_patterns ): return "" for cache_resource_path in cache_resource_paths( cache_directory, normalized_relative_path ): if not ResourceLoader.exists(cache_resource_path): continue if glb_cache_is_safe_for_runtime_animation( cache_resource_path, maximum_primitive_count, debug_logging_enabled ): return cache_resource_path return "" ## Applies the historical stripped, case-insensitive allow/deny substring policy. func is_animation_path_allowed( normalized_relative_path: String, allowlist_patterns: PackedStringArray, denylist_patterns: PackedStringArray ) -> bool: if normalized_relative_path.is_empty() or allowlist_patterns.is_empty(): return false var lowercase_path := normalized_relative_path.to_lower() var allowed := false for pattern in allowlist_patterns: var needle := String(pattern).strip_edges().to_lower() if not needle.is_empty() and lowercase_path.contains(needle): allowed = true break if not allowed: return false for pattern in denylist_patterns: var needle := String(pattern).strip_edges().to_lower() if not needle.is_empty() and lowercase_path.contains(needle): return false return true ## Returns historical nested/lowercase/basename `.glb` candidates without I/O. func cache_resource_paths( cache_directory: String, relative_path: String ) -> PackedStringArray: var normalized := relative_path.replace("\\", "/") var lowercase := normalized.to_lower() var stems := [ normalized.get_basename(), lowercase.get_basename(), normalized.get_file().get_basename(), lowercase.get_file().get_basename(), ] var result := PackedStringArray() for stem in stems: if stem.is_empty(): continue var cache_resource_path := cache_directory.path_join(stem + ".glb") if not result.has(cache_resource_path): result.append(cache_resource_path) return result ## Checks animation presence, primitive limit and accepted OpenWC schema. func glb_cache_is_safe_for_runtime_animation( cache_resource_path: String, maximum_primitive_count: int, debug_logging_enabled: bool = false ) -> bool: var gltf := read_glb_json(cache_resource_path) if gltf.is_empty(): return false var animations: Array = gltf.get("animations", []) if animations.is_empty(): return false var primitive_count := glb_primitive_count(gltf) if maximum_primitive_count > 0 and primitive_count > maximum_primitive_count: return false var schema := glb_animation_schema(gltf) if schema == "pivot_prefix_v1" or schema.is_empty(): return true if debug_logging_enabled: print( "M2_ANIM_REJECT schema=%s primitives=%d cache=%s" % [schema, primitive_count, cache_resource_path] ) return false ## Reads the JSON chunk of a version-2 GLB or returns an empty Dictionary. func read_glb_json(cache_resource_path: String) -> Dictionary: var absolute_path := ProjectSettings.globalize_path(cache_resource_path) if not FileAccess.file_exists(absolute_path): return {} var file := FileAccess.open(absolute_path, FileAccess.READ) if file == null or file.get_length() < 20: return {} var magic := file.get_32() var version := file.get_32() file.get_32() if magic != 0x46546c67 or version != 2: return {} var json_length := int(file.get_32()) var chunk_type := file.get_32() if json_length <= 0 or chunk_type != 0x4e4f534a: return {} var parsed: Variant = JSON.parse_string( file.get_buffer(json_length).get_string_from_utf8() ) return parsed as Dictionary if parsed is Dictionary else {} ## Counts all glTF Mesh primitives using the historical safety rule. func glb_primitive_count(gltf: Dictionary) -> int: var count := 0 var meshes: Array = gltf.get("meshes", []) for mesh_variant in meshes: if mesh_variant is Dictionary: var primitives: Array = (mesh_variant as Dictionary).get( "primitives", [] ) count += primitives.size() return count ## Returns the OpenWC animation schema marker from parsed glTF metadata. func glb_animation_schema(gltf: Dictionary) -> String: var asset: Dictionary = gltf.get("asset", {}) var extras: Dictionary = asset.get("extras", {}) return String(extras.get("openwc_m2_anim_schema", ""))