render: extract native M2 animation resource observer
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
class_name M2NativeAnimationResourceObserver
|
||||
extends RefCounted
|
||||
|
||||
## Resolves the existing native GryphonRoost animated M2 prototype path.
|
||||
|
||||
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload(
|
||||
"res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd"
|
||||
)
|
||||
|
||||
var _native_animated_builder: Object
|
||||
|
||||
|
||||
func _init(native_animated_builder: Object = M2_NATIVE_ANIMATED_BUILDER_SCRIPT) -> void:
|
||||
_native_animated_builder = native_animated_builder
|
||||
|
||||
|
||||
## Returns whether the normalized path uses the historical native animation path.
|
||||
func is_native_animation_candidate(normalized_relative_path: String) -> bool:
|
||||
return normalized_relative_path.to_lower().contains("gryphonroost")
|
||||
|
||||
|
||||
## Returns the exact cached or newly adopted native animated prototype.
|
||||
## Rejected native candidates are marked static-only for the renderer session.
|
||||
func observe(
|
||||
normalized_relative_path: String,
|
||||
extracted_directory: String,
|
||||
raw_model_repository: RefCounted,
|
||||
prototype_cache_state: RefCounted,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> Node3D:
|
||||
if (
|
||||
normalized_relative_path.is_empty()
|
||||
or not is_native_animation_candidate(normalized_relative_path)
|
||||
or raw_model_repository == null
|
||||
or prototype_cache_state == null
|
||||
):
|
||||
return null
|
||||
|
||||
var cached_prototype := prototype_cache_state.call(
|
||||
"find_animated_prototype",
|
||||
normalized_relative_path
|
||||
) as Node3D
|
||||
if cached_prototype != null:
|
||||
return cached_prototype
|
||||
if bool(prototype_cache_state.call(
|
||||
"is_animation_static",
|
||||
normalized_relative_path
|
||||
)):
|
||||
return null
|
||||
|
||||
var animated_model_data: Dictionary = raw_model_repository.call(
|
||||
"load_animated_model_data",
|
||||
extracted_directory,
|
||||
normalized_relative_path
|
||||
)
|
||||
var animated_surfaces: Array = animated_model_data.get(
|
||||
"animated_surfaces",
|
||||
[]
|
||||
)
|
||||
if animated_model_data.is_empty() or animated_surfaces.is_empty():
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return null
|
||||
|
||||
var prototype := build_animated_prototype(
|
||||
animated_model_data,
|
||||
extracted_directory
|
||||
)
|
||||
if prototype == null or prototype.get_child_count() <= 0:
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return null
|
||||
|
||||
prototype = prototype_cache_state.call(
|
||||
"adopt_animated_prototype",
|
||||
normalized_relative_path,
|
||||
prototype
|
||||
) as Node3D
|
||||
if debug_logging_enabled:
|
||||
print(
|
||||
(
|
||||
"M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d "
|
||||
+ "anim_id=%d seq=%d score=%d length=%.2f"
|
||||
)
|
||||
% [
|
||||
normalized_relative_path,
|
||||
animated_surfaces.size(),
|
||||
(animated_model_data.get("bones", []) as Array).size(),
|
||||
int(animated_model_data.get("animation_id", -1)),
|
||||
int(animated_model_data.get("animation_sequence_index", -1)),
|
||||
int(animated_model_data.get("animation_activity_score", 0)),
|
||||
float(animated_model_data.get("animation_length", 0.0)),
|
||||
]
|
||||
)
|
||||
return prototype
|
||||
|
||||
|
||||
## Builds one prototype through the exact native animated builder dependency.
|
||||
func build_animated_prototype(
|
||||
animated_model_data: Dictionary,
|
||||
extracted_directory: String
|
||||
) -> Node3D:
|
||||
if _native_animated_builder == null:
|
||||
return null
|
||||
return _native_animated_builder.call(
|
||||
"build",
|
||||
animated_model_data,
|
||||
extracted_directory
|
||||
) as Node3D
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc3kgi23usncx
|
||||
@@ -28,7 +28,6 @@ const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
)
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
@@ -76,6 +75,9 @@ const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
|
||||
const M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const M2_NATIVE_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
@@ -289,6 +291,9 @@ var _m2_static_build_resource_observer := (
|
||||
var _m2_animation_resource_observer := (
|
||||
M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
|
||||
)
|
||||
var _m2_native_animation_resource_observer := (
|
||||
M2_NATIVE_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
|
||||
)
|
||||
var _m2_unique_placement_registry := (
|
||||
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
)
|
||||
@@ -4359,7 +4364,13 @@ func _process_m2_build_jobs() -> void:
|
||||
var resource_snapshot: RefCounted
|
||||
if enable_m2_animated_instances:
|
||||
var native_animated_prototype := (
|
||||
_get_or_load_m2_native_animated_prototype(rel_path)
|
||||
_m2_native_animation_resource_observer.observe(
|
||||
normalized_rel,
|
||||
extracted_dir,
|
||||
_m2_raw_model_repository,
|
||||
_m2_prototype_cache_state,
|
||||
debug_streaming
|
||||
)
|
||||
)
|
||||
if native_animated_prototype != null:
|
||||
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
@@ -4849,52 +4860,6 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
||||
)
|
||||
|
||||
|
||||
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
|
||||
return null
|
||||
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
|
||||
normalized_rel
|
||||
)
|
||||
if cached_animated != null:
|
||||
return cached_animated
|
||||
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
||||
return null
|
||||
var data := _m2_raw_model_repository.load_animated_model_data(
|
||||
extracted_dir,
|
||||
normalized_rel
|
||||
)
|
||||
var animated_surfaces: Array = data.get("animated_surfaces", [])
|
||||
if data.is_empty() or animated_surfaces.is_empty():
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
return null
|
||||
|
||||
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||
if prototype == null or prototype.get_child_count() <= 0:
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
return null
|
||||
|
||||
prototype = _m2_prototype_cache_state.adopt_animated_prototype(
|
||||
normalized_rel,
|
||||
prototype
|
||||
)
|
||||
if debug_streaming:
|
||||
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
|
||||
normalized_rel,
|
||||
animated_surfaces.size(),
|
||||
(data.get("bones", []) as Array).size(),
|
||||
int(data.get("animation_id", -1)),
|
||||
int(data.get("animation_sequence_index", -1)),
|
||||
int(data.get("animation_activity_score", 0)),
|
||||
float(data.get("animation_length", 0.0)),
|
||||
])
|
||||
return prototype
|
||||
|
||||
|
||||
func _is_m2_native_animation_candidate(normalized_rel: String) -> bool:
|
||||
return normalized_rel.to_lower().contains("gryphonroost")
|
||||
|
||||
|
||||
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
|
||||
@@ -12,6 +12,9 @@ const PIPELINE_SCRIPT := preload(
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const NATIVE_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const FIXTURE_DIRECTORY := "user://verify_m2_cached_animation_observer"
|
||||
|
||||
@@ -252,6 +255,7 @@ func _verify_glb_safety(failures: Array[String]) -> void:
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
|
||||
var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("_m2_animation_resource_observer.observe("),
|
||||
"loader delegates cached observation",
|
||||
@@ -269,8 +273,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("func _get_or_load_m2_native_animated_prototype"),
|
||||
"loader retains native observation",
|
||||
loader_source.contains("_m2_native_animation_resource_observer.observe("),
|
||||
"loader delegates native observation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
native_observer_source.contains("func is_native_animation_candidate("),
|
||||
"native observer retains native candidate policy",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
extends SceneTree
|
||||
|
||||
const OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const PROTOTYPE_CACHE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_prototype_cache_state.gd"
|
||||
)
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeRawModelRepository extends RefCounted:
|
||||
var result: Dictionary = {}
|
||||
var call_count: int = 0
|
||||
var last_extracted_directory: String = ""
|
||||
var last_normalized_relative_path: String = ""
|
||||
|
||||
func load_animated_model_data(
|
||||
extracted_directory: String,
|
||||
normalized_relative_path: String
|
||||
) -> Dictionary:
|
||||
call_count += 1
|
||||
last_extracted_directory = extracted_directory
|
||||
last_normalized_relative_path = normalized_relative_path
|
||||
return result
|
||||
|
||||
|
||||
class FakeNativeAnimatedBuilder extends RefCounted:
|
||||
var result: Node3D = null
|
||||
var call_count: int = 0
|
||||
var last_model_data: Dictionary = {}
|
||||
var last_extracted_directory: String = ""
|
||||
|
||||
func build(
|
||||
animated_model_data: Dictionary,
|
||||
extracted_directory: String
|
||||
) -> Node3D:
|
||||
call_count += 1
|
||||
last_model_data = animated_model_data
|
||||
last_extracted_directory = extracted_directory
|
||||
return result
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_candidate_policy(failures)
|
||||
_verify_missing_dependencies(failures)
|
||||
_verify_cache_and_negative_lifecycle(failures)
|
||||
_verify_raw_and_builder_failures(failures)
|
||||
_verify_successful_adoption(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_NATIVE_ANIMATION_RESOURCE_OBSERVER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_NATIVE_ANIMATION_RESOURCE_OBSERVER PASS "
|
||||
+ "cases=33 iterations=20000 elapsed_ms=%.3f" % elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_candidate_policy(failures: Array[String]) -> void:
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new()
|
||||
_expect_false(
|
||||
bool(observer.call("is_native_animation_candidate", "")),
|
||||
"empty path is not candidate",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(observer.call(
|
||||
"is_native_animation_candidate",
|
||||
"World/Generic/GryphonRoost/GryphonRoost.M2"
|
||||
)),
|
||||
"candidate match ignores case",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"is_native_animation_candidate",
|
||||
"creature/gryphon/gryphon.m2"
|
||||
)),
|
||||
"unlisted gryphon is not candidate",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_missing_dependencies(failures: Array[String]) -> void:
|
||||
var path := "world/gryphonroost/dependency.m2"
|
||||
var builder := FakeNativeAnimatedBuilder.new()
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
|
||||
var repository := FakeRawModelRepository.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_null(
|
||||
observer.call("observe", path, "res://data/extracted", null, prototype_cache),
|
||||
"missing repository rejected",
|
||||
failures
|
||||
)
|
||||
_expect_null(
|
||||
observer.call("observe", path, "res://data/extracted", repository, null),
|
||||
"missing cache rejected",
|
||||
failures
|
||||
)
|
||||
repository.result = _animated_model_data()
|
||||
observer = OBSERVER_SCRIPT.new(null)
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", path, "res://data/extracted", repository, prototype_cache
|
||||
),
|
||||
"missing builder rejected",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_cache_and_negative_lifecycle(failures: Array[String]) -> void:
|
||||
var builder := FakeNativeAnimatedBuilder.new()
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
|
||||
var repository := FakeRawModelRepository.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", "", "res://data/extracted", repository,
|
||||
prototype_cache
|
||||
),
|
||||
"empty observation rejected",
|
||||
failures
|
||||
)
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", "world/not_native.m2", "res://data/extracted",
|
||||
repository, prototype_cache
|
||||
),
|
||||
"non-candidate rejected",
|
||||
failures
|
||||
)
|
||||
_expect_equal(repository.call_count, 0, "rejected paths skip repository", failures)
|
||||
var cached_prototype := Node3D.new()
|
||||
prototype_cache.call(
|
||||
"adopt_animated_prototype",
|
||||
"world/gryphonroost/cached.m2",
|
||||
cached_prototype
|
||||
)
|
||||
_expect_same(
|
||||
observer.call(
|
||||
"observe", "world/gryphonroost/cached.m2",
|
||||
"res://data/extracted", repository, prototype_cache
|
||||
),
|
||||
cached_prototype,
|
||||
"cached prototype identity",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call(
|
||||
"mark_animation_static",
|
||||
"world/gryphonroost/static.m2"
|
||||
)
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", "world/gryphonroost/static.m2",
|
||||
"res://data/extracted", repository, prototype_cache
|
||||
),
|
||||
"static-only outcome suppresses load",
|
||||
failures
|
||||
)
|
||||
_expect_equal(repository.call_count, 0, "cache outcomes skip repository", failures)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_raw_and_builder_failures(failures: Array[String]) -> void:
|
||||
var builder := FakeNativeAnimatedBuilder.new()
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
|
||||
var repository := FakeRawModelRepository.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var empty_path := "world/gryphonroost/empty.m2"
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", empty_path, "res://data/extracted", repository,
|
||||
prototype_cache
|
||||
),
|
||||
"empty raw data rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", empty_path)),
|
||||
"empty raw data marks static",
|
||||
failures
|
||||
)
|
||||
repository.result = {"animated_surfaces": []}
|
||||
var surfaces_path := "world/gryphonroost/no_surfaces.m2"
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", surfaces_path, "res://data/extracted", repository,
|
||||
prototype_cache
|
||||
),
|
||||
"empty animated surfaces reject",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", surfaces_path)),
|
||||
"empty surfaces mark static",
|
||||
failures
|
||||
)
|
||||
repository.result = _animated_model_data()
|
||||
var null_path := "world/gryphonroost/null_build.m2"
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", null_path, "res://data/extracted", repository,
|
||||
prototype_cache
|
||||
),
|
||||
"null build rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", null_path)),
|
||||
"null build marks static",
|
||||
failures
|
||||
)
|
||||
var childless_prototype := Node3D.new()
|
||||
builder.result = childless_prototype
|
||||
var childless_path := "world/gryphonroost/childless.m2"
|
||||
_expect_null(
|
||||
observer.call(
|
||||
"observe", childless_path, "res://data/extracted", repository,
|
||||
prototype_cache
|
||||
),
|
||||
"childless build rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call("is_animation_static", childless_path)),
|
||||
"childless build marks static",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
is_instance_valid(childless_prototype),
|
||||
"historical childless candidate lifetime retained",
|
||||
failures
|
||||
)
|
||||
childless_prototype.free()
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_successful_adoption(failures: Array[String]) -> void:
|
||||
var builder := FakeNativeAnimatedBuilder.new()
|
||||
var prototype := Node3D.new()
|
||||
prototype.add_child(Node3D.new())
|
||||
builder.result = prototype
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
|
||||
var repository := FakeRawModelRepository.new()
|
||||
repository.result = _animated_model_data()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var path := "World/Generic/GryphonRoost/GryphonRoost.M2"
|
||||
var observed: Node3D = observer.call(
|
||||
"observe",
|
||||
path,
|
||||
"res://custom/extracted",
|
||||
repository,
|
||||
prototype_cache
|
||||
)
|
||||
_expect_same(observed, prototype, "successful build identity", failures)
|
||||
_expect_same(
|
||||
prototype_cache.call("find_animated_prototype", path),
|
||||
prototype,
|
||||
"successful build adopted",
|
||||
failures
|
||||
)
|
||||
_expect_equal(repository.call_count, 1, "one repository read", failures)
|
||||
_expect_string_equal(
|
||||
repository.last_extracted_directory,
|
||||
"res://custom/extracted",
|
||||
"repository extracted directory",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
repository.last_normalized_relative_path,
|
||||
path,
|
||||
"repository normalized path",
|
||||
failures
|
||||
)
|
||||
_expect_equal(builder.call_count, 1, "one builder call", failures)
|
||||
_expect_true(
|
||||
builder.last_model_data == repository.result,
|
||||
"builder receives exact raw data",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
builder.last_extracted_directory,
|
||||
"res://custom/extracted",
|
||||
"builder extracted directory",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("_m2_native_animation_resource_observer.observe("),
|
||||
"loader delegates native observation",
|
||||
failures
|
||||
)
|
||||
for removed_token in [
|
||||
"func _get_or_load_m2_native_animated_prototype",
|
||||
"func _is_m2_native_animation_candidate",
|
||||
"const M2_NATIVE_ANIMATED_BUILDER_SCRIPT",
|
||||
]:
|
||||
_expect_false(
|
||||
loader_source.contains(removed_token),
|
||||
"loader omits %s" % removed_token,
|
||||
failures
|
||||
)
|
||||
for required_token in [
|
||||
"M2_NATIVE_ANIMATED_BUILDER_SCRIPT",
|
||||
"\"load_animated_model_data\"",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"\"mark_animation_static\"",
|
||||
"M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d",
|
||||
]:
|
||||
_expect_true(
|
||||
observer_source.contains(required_token),
|
||||
"observer owns %s" % required_token,
|
||||
failures
|
||||
)
|
||||
for forbidden_token in [
|
||||
"ResourceLoader",
|
||||
"queue_free",
|
||||
"RenderingServer",
|
||||
"WorkerThreadPool",
|
||||
"RenderBudgetScheduler",
|
||||
"add_child",
|
||||
]:
|
||||
_expect_false(
|
||||
observer_source.contains(forbidden_token),
|
||||
"observer omits %s" % forbidden_token,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(20000):
|
||||
observer.call(
|
||||
"is_native_animation_candidate",
|
||||
"world/model_%d/gryphonroost.m2" % (iteration % 64)
|
||||
)
|
||||
var elapsed_milliseconds := (
|
||||
float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
)
|
||||
_expect_true(
|
||||
elapsed_milliseconds < 1000.0,
|
||||
"20,000 candidate checks remain bounded",
|
||||
failures
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _animated_model_data() -> Dictionary:
|
||||
return {
|
||||
"animated_surfaces": [{"surface": 1}],
|
||||
"bones": [{"bone": 1}, {"bone": 2}],
|
||||
"animation_id": 7,
|
||||
"animation_sequence_index": 3,
|
||||
"animation_activity_score": 9,
|
||||
"animation_length": 1.25,
|
||||
}
|
||||
|
||||
|
||||
func _expect_true(
|
||||
condition: bool,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(
|
||||
condition: bool,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_null(
|
||||
actual: Variant,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(actual == null, label, failures)
|
||||
|
||||
|
||||
func _expect_same(
|
||||
actual: Variant,
|
||||
expected: Variant,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
_expect_true(is_same(actual, expected), label, failures)
|
||||
|
||||
|
||||
func _expect_equal(
|
||||
actual: int,
|
||||
expected: int,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_equal(
|
||||
actual: String,
|
||||
expected: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://cto5qa1vh221v
|
||||
@@ -4,6 +4,12 @@ extends SceneTree
|
||||
|
||||
const CACHE_STATE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd")
|
||||
const CACHE_STATE_PATH := "res://src/render/m2/m2_prototype_cache_state.gd"
|
||||
const NATIVE_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const CACHED_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -93,6 +99,10 @@ func _verify_release_ownership(failures: Array[String]) -> void:
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var state_source := FileAccess.get_file_as_string(CACHE_STATE_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var observer_sources := (
|
||||
FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
+ FileAccess.get_file_as_string(CACHED_OBSERVER_PATH)
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
|
||||
"loader composes prototype cache state",
|
||||
@@ -108,13 +118,17 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
for required_loader_rule in [
|
||||
"_m2_prototype_cache_state.find_static_prototype(",
|
||||
"_m2_prototype_cache_state.adopt_static_prototype(",
|
||||
"_m2_prototype_cache_state.find_animated_prototype(",
|
||||
"\"find_animated_prototype\"",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_model_missing(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(",
|
||||
"_m2_prototype_cache_state.clear_and_release()",
|
||||
]:
|
||||
_expect_true(loader_source.contains(required_loader_rule), "loader delegates %s" % required_loader_rule, failures)
|
||||
_expect_true(
|
||||
(loader_source + observer_sources).contains(required_loader_rule),
|
||||
"renderer delegates %s" % required_loader_rule,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
|
||||
@@ -4,6 +4,9 @@ extends SceneTree
|
||||
|
||||
const REPOSITORY_SCRIPT := preload("res://src/render/m2/m2_raw_model_repository.gd")
|
||||
const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd"
|
||||
const NATIVE_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_native_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -56,6 +59,7 @@ func _verify_rejected_inputs(failures: Array[String]) -> void:
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH)
|
||||
var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"),
|
||||
@@ -69,7 +73,10 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_raw_model_repository.load_animated_model_data("),
|
||||
(
|
||||
loader_source.count("_m2_raw_model_repository.load_animated_model_data(")
|
||||
+ native_observer_source.count("\"load_animated_model_data\"")
|
||||
),
|
||||
1,
|
||||
"animated caller delegates",
|
||||
failures
|
||||
|
||||
Reference in New Issue
Block a user