refactor(M03): extract WMO placement resolver
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free contract, dependency and timing regression for WMO placements.
|
||||
|
||||
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
|
||||
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_path_normalization(failures)
|
||||
_verify_positive_unique_id(failures)
|
||||
_verify_synthetic_unique_keys(failures)
|
||||
_verify_default_transform(failures)
|
||||
_verify_position_rotation_and_scale(failures)
|
||||
_verify_unclamped_scale(failures)
|
||||
_verify_node_property_equivalence(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
_verify_fresh_stateless_results(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_PLACEMENT_RESOLVER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_PLACEMENT_RESOLVER PASS cases=9 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_path_normalization(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
_expect_equal_string(
|
||||
resolver.normalize_relative_path("World\\WMO\\Stormwind\\Keep.WMO"),
|
||||
"world/wmo/stormwind/keep.wmo",
|
||||
"slash and case normalization",
|
||||
failures
|
||||
)
|
||||
_expect_equal_string(resolver.normalize_relative_path(""), "", "empty path", failures)
|
||||
|
||||
|
||||
func _verify_positive_unique_id(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
_expect_equal_string(
|
||||
resolver.resolve_unique_key({"unique_id": 11785}, "31:49", 6),
|
||||
"uid:11785",
|
||||
"positive unique ID",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_synthetic_unique_keys(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
for placement in [{}, {"unique_id": 0}, {"unique_id": -4}]:
|
||||
_expect_equal_string(
|
||||
resolver.resolve_unique_key(placement, "31:49", 6),
|
||||
"tile:31:49:6",
|
||||
"synthetic unique key",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_default_transform(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
var transform: Transform3D = resolver.resolve_world_transform({})
|
||||
_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis", failures)
|
||||
_expect_vector_near(transform.origin, Vector3.ZERO, "default origin", failures)
|
||||
|
||||
|
||||
func _verify_position_rotation_and_scale(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
var position := Vector3(10.0, 20.0, 30.0)
|
||||
var rotation := Vector3(0.2, -0.4, 0.6)
|
||||
var transform: Transform3D = resolver.resolve_world_transform({
|
||||
"pos": position,
|
||||
"rot": rotation,
|
||||
"scale": 2.5,
|
||||
})
|
||||
_expect_basis_near(
|
||||
transform.basis,
|
||||
Basis.from_euler(rotation).scaled(Vector3.ONE * 2.5),
|
||||
"rotated scaled basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(transform.origin, position, "world origin", failures)
|
||||
|
||||
|
||||
func _verify_unclamped_scale(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
var zero_transform: Transform3D = resolver.resolve_world_transform({"scale": 0.0})
|
||||
_expect_basis_near(
|
||||
zero_transform.basis,
|
||||
Basis.IDENTITY.scaled(Vector3.ZERO),
|
||||
"zero scale",
|
||||
failures
|
||||
)
|
||||
var negative_transform: Transform3D = resolver.resolve_world_transform({"scale": -2.0})
|
||||
_expect_basis_near(
|
||||
negative_transform.basis,
|
||||
Basis.IDENTITY.scaled(Vector3.ONE * -2.0),
|
||||
"negative scale",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_node_property_equivalence(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
for scale_value in [2.5, 0.0, -2.0]:
|
||||
var placement := {
|
||||
"pos": Vector3(10.0, 20.0, 30.0),
|
||||
"rot": Vector3(0.2, -0.4, 0.6),
|
||||
"scale": scale_value,
|
||||
}
|
||||
var historical_node := Node3D.new()
|
||||
historical_node.position = placement["pos"]
|
||||
historical_node.rotation = placement["rot"]
|
||||
historical_node.scale = Vector3.ONE * scale_value
|
||||
var historical_transform := historical_node.transform
|
||||
historical_node.free()
|
||||
var resolved_transform: Transform3D = resolver.resolve_world_transform(placement)
|
||||
_expect_basis_near(
|
||||
resolved_transform.basis,
|
||||
historical_transform.basis,
|
||||
"Node3D property equivalence scale %s" % scale_value,
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolved_transform.origin,
|
||||
historical_transform.origin,
|
||||
"Node3D origin equivalence scale %s" % scale_value,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var resolver_source := _read_text(RESOLVER_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
|
||||
"loader composes resolver",
|
||||
failures
|
||||
)
|
||||
_expect_equal_int(
|
||||
loader_source.count("_wmo_placement_resolver.normalize_relative_path("),
|
||||
3,
|
||||
"three normalization adapters",
|
||||
failures
|
||||
)
|
||||
_expect_equal_int(
|
||||
loader_source.count("_wmo_placement_resolver.resolve_unique_key("),
|
||||
1,
|
||||
"one identity adapter",
|
||||
failures
|
||||
)
|
||||
_expect_equal_int(
|
||||
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
|
||||
3,
|
||||
"three transform adapters",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not loader_source.contains("func _normalize_wmo_rel_path("),
|
||||
"loader omits normalization formula",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not loader_source.contains("func _wmo_unique_key("),
|
||||
"loader omits identity formula",
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"Node",
|
||||
"ResourceLoader",
|
||||
"RenderingServer",
|
||||
"WorkerThreadPool",
|
||||
"Mutex",
|
||||
"FileAccess",
|
||||
]:
|
||||
_expect_true(
|
||||
not resolver_source.contains(forbidden_dependency),
|
||||
"resolver omits %s" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_fresh_stateless_results(failures: Array[String]) -> void:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
var placement := {"pos": Vector3(1.0, 2.0, 3.0), "scale": 4.0}
|
||||
var first: Transform3D = resolver.resolve_world_transform(placement)
|
||||
first.origin = Vector3.ZERO
|
||||
var second: Transform3D = resolver.resolve_world_transform(placement)
|
||||
_expect_vector_near(second.origin, Vector3(1.0, 2.0, 3.0), "stateless result", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var resolver := RESOLVER_SCRIPT.new()
|
||||
var placement := {
|
||||
"unique_id": 11785,
|
||||
"pos": Vector3(10.0, 20.0, 30.0),
|
||||
"rot": Vector3(0.1, 0.2, 0.3),
|
||||
"scale": 1.5,
|
||||
}
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(20000):
|
||||
resolver.normalize_relative_path("World\\WMO\\Stormwind\\Keep.WMO")
|
||||
resolver.resolve_unique_key(placement, "31:49", iteration)
|
||||
resolver.resolve_world_transform(placement)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "20000 placement triples bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_basis_near(
|
||||
actual_basis: Basis,
|
||||
expected_basis: Basis,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_basis.is_equal_approx(expected_basis):
|
||||
failures.append("%s basis mismatch" % label)
|
||||
|
||||
|
||||
func _expect_vector_near(
|
||||
actual_value: Vector3,
|
||||
expected_value: Vector3,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_value.is_equal_approx(expected_value):
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_equal_string(
|
||||
actual_value: String,
|
||||
expected_value: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_equal_int(
|
||||
actual_value: int,
|
||||
expected_value: int,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
Reference in New Issue
Block a user