refactor: scope active world targets

This commit is contained in:
Rijad Zuzo
2026-08-12 21:58:52 +02:00
parent d12e9515fb
commit acffe6aa0f
8 changed files with 950 additions and 57 deletions
+321
View File
@@ -0,0 +1,321 @@
extends SceneTree
const SHARED_RESOURCE_ID := &"shared_berry"
const SHARED_STORAGE_ID := &"shared_pantry"
const SHARED_ACTIVITY_ID := &"shared_bench"
const SHARED_ANIMAL_ID := &"shared_goat"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var first := _create_context(&"context_one", &"world_one", &"location_one", Vector3.ZERO)
var second := _create_context(
&"context_two", &"world_two", &"location_two", Vector3(1000.0, 0.0, 0.0)
)
root.add_child(first["root"])
root.add_child(second["root"])
await process_frame
_test_context_identity_and_duplicate_local_ids(first, second)
_test_context_scoped_queries(first, second)
_test_activity_interaction_range(first)
_test_cross_kind_uniqueness(first)
await _test_unregister_rebind_and_generation(first)
(first["root"] as Node).free()
(second["root"] as Node).free()
_check(
ResourceNode.get_all().is_empty(), "Context cleanup should release legacy resource views"
)
_check(StorageNode.get_all().is_empty(), "Context cleanup should release legacy storage views")
_check(
ActivitySite.get_all().is_empty(), "Context cleanup should release legacy activity views"
)
_check(AnimalNode.get_all().is_empty(), "Context cleanup should release legacy animal views")
_finish()
func _test_context_identity_and_duplicate_local_ids(first: Dictionary, second: Dictionary) -> void:
var first_adapter := first["adapter"] as ActiveWorldAdapter
var second_adapter := second["adapter"] as ActiveWorldAdapter
_check(
(
first_adapter.get_context_identity()
== {
"context_id": &"context_one",
"world_id": &"world_one",
"location_id": &"location_one",
}
),
"Each adapter should expose its explicit context/world/location identity",
)
_check(
first_adapter.get_target_registry().size() == 4,
"The first context should register all four canonical target kinds",
)
_check(
second_adapter.get_target_registry().size() == 4,
"A second context may reuse every local target ID without a collision",
)
for target_id in [SHARED_RESOURCE_ID, SHARED_STORAGE_ID, SHARED_ACTIVITY_ID, SHARED_ANIMAL_ID]:
var first_provider := first_adapter.get_target_provider(target_id)
var second_provider := second_adapter.get_target_provider(target_id)
_check(
(
first_provider != null
and second_provider != null
and first_provider != second_provider
),
"Canonical lookup for '%s' should resolve only the owning context" % target_id,
)
_check(
(
first_adapter.resolve_target_handle(
second_adapter.get_target_handle(SHARED_RESOURCE_ID)
)
== null
),
"Transient handles from another adapter must not resolve",
)
func _test_context_scoped_queries(first: Dictionary, second: Dictionary) -> void:
var first_adapter := first["adapter"] as ActiveWorldAdapter
var second_adapter := second["adapter"] as ActiveWorldAdapter
var first_resource := first["resource"] as ResourceNode
var second_resource := second["resource"] as ResourceNode
var first_candidates := first_adapter.get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, first_resource.global_position, 2.0
)
var second_candidates := second_adapter.get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, second_resource.global_position, 2.0
)
_check(
first_candidates.size() == 1 and second_candidates.size() == 1,
"Each spatial index should return its own same-ID resource",
)
_check(
(
first_adapter
. get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, second_resource.global_position, 2.0
)
. is_empty()
),
"Equal local target IDs must not leak through another context's spatial query",
)
_check(
first_adapter.get_activity_candidates(SimulationIds.ACTION_REST).size() == 1,
"Activity queries should read the context registry",
)
_check(
(
first_adapter.get_storage_target(SHARED_STORAGE_ID)["position"]
!= second_adapter.get_storage_target(SHARED_STORAGE_ID)["position"]
),
"Storage lookup should return the owning context's position",
)
_check(
first_adapter.get_animal_candidates(SimulationIds.ACTION_FEED_ANIMAL).size() == 1,
"Animal queries should read the context registry",
)
_check(
(
first_adapter
. get_animal_nodes_in_radius((second["animal"] as AnimalNode).global_position, 2.0)
. is_empty()
),
"Animal radius queries must not return another context's same-ID provider",
)
func _test_cross_kind_uniqueness(fixture: Dictionary) -> void:
var adapter := fixture["adapter"] as ActiveWorldAdapter
var conflicting := ActivitySite.new()
conflicting.site_id = SHARED_RESOURCE_ID
conflicting.action_id = SimulationIds.ACTION_REST
conflicting.display_name = "Conflicting bench"
conflicting.debug_label_enabled = false
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
conflicting.add_child(interaction_point)
(fixture["root"] as Node).add_child(conflicting)
_check(
adapter.get_target_registry().size() == 4,
"A same-context cross-kind collision must not replace or add a canonical target",
)
_check(
(
adapter.get_target_registry().get_descriptor(SHARED_RESOURCE_ID).get_target_kind()
== &"resource"
),
"The original target kind should survive a cross-kind registration attempt",
)
conflicting.free()
func _test_activity_interaction_range(fixture: Dictionary) -> void:
var adapter := fixture["adapter"] as ActiveWorldAdapter
var descriptor := adapter.get_target_registry().get_descriptor(SHARED_ACTIVITY_ID)
var capability := descriptor.get_capability(SimulationIds.ACTION_REST)
var interaction_range := float(capability.get_attribute(&"interaction_range", -1.0))
_check(
is_finite(interaction_range) and interaction_range > 0.0,
"Activity capabilities should expose a positive finite generic interaction range",
)
var invalid := ActivitySite.new()
invalid.interaction_range = 0.0
_check(
not invalid.is_interaction_range_valid(),
"ActivitySite should reject a non-positive interaction range",
)
invalid.interaction_range = NAN
_check(
not invalid.is_interaction_range_valid(),
"ActivitySite should reject a non-finite interaction range",
)
invalid.free()
func _test_unregister_rebind_and_generation(fixture: Dictionary) -> void:
var adapter := fixture["adapter"] as ActiveWorldAdapter
var first_resource := fixture["resource"] as ResourceNode
var first_handle := adapter.get_target_handle(SHARED_RESOURCE_ID)
var previous_position := first_resource.global_position
first_resource.free()
_check(
not adapter.get_target_registry().is_handle_valid(first_handle),
"Unloading the provider should invalidate its transient handle",
)
_check(
int(adapter.get_resource_index_stats()["candidate_count"]) == 0,
"Unloading the provider should remove it from the context spatial index",
)
var replacement := _create_resource(
SHARED_RESOURCE_ID, previous_position + Vector3(8.0, 0.0, 0.0)
)
(fixture["root"] as Node).add_child(replacement)
await process_frame
var rebound_handle := adapter.get_target_handle(SHARED_RESOURCE_ID)
_check(
rebound_handle != null and rebound_handle.get_generation() > first_handle.get_generation(),
"Rebinding the same local ID should advance its generation",
)
_check(
adapter.get_target_provider(SHARED_RESOURCE_ID, &"resource") == replacement,
"Rebinding should point canonical lookup at the replacement provider",
)
_check(
(
(
adapter
. get_resource_candidates_in_radius(
SimulationIds.ACTION_GATHER_FOOD, replacement.global_position, 0.1
)
. size()
)
== 1
),
"A rebound resource should be indexed at its new position",
)
func _create_context(
context_id: StringName, world_id: StringName, location_id: StringName, offset: Vector3
) -> Dictionary:
var context_root := Node3D.new()
context_root.name = String(context_id)
context_root.position = offset
var adapter := ActiveWorldAdapter.new()
adapter.name = "ActiveWorldAdapter"
adapter.context_id = context_id
adapter.world_id = world_id
adapter.location_id = location_id
context_root.add_child(adapter)
var resource := _create_resource(SHARED_RESOURCE_ID, Vector3(1.0, 0.0, 0.0))
context_root.add_child(resource)
var storage := _create_storage(SHARED_STORAGE_ID, Vector3(2.0, 0.0, 0.0))
context_root.add_child(storage)
var activity := _create_activity(SHARED_ACTIVITY_ID, Vector3(3.0, 0.0, 0.0))
context_root.add_child(activity)
var animal := _create_animal(SHARED_ANIMAL_ID, Vector3(4.0, 0.0, 0.0))
context_root.add_child(animal)
return {
"root": context_root,
"adapter": adapter,
"resource": resource,
"storage": storage,
"activity": activity,
"animal": animal,
}
func _create_resource(target_id: StringName, position: Vector3) -> ResourceNode:
var node := ResourceNode.new()
node.node_id = target_id
node.action_id = SimulationIds.ACTION_GATHER_FOOD
node.resource_id = SimulationIds.RESOURCE_FOOD
node.debug_label_enabled = false
node.position = position
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _create_storage(target_id: StringName, position: Vector3) -> StorageNode:
var node := StorageNode.new()
node.storage_id = target_id
node.accepted_items = [SimulationIds.RESOURCE_FOOD]
node.debug_label_enabled = false
node.position = position
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _create_activity(target_id: StringName, position: Vector3) -> ActivitySite:
var node := ActivitySite.new()
node.site_id = target_id
node.action_id = SimulationIds.ACTION_REST
node.debug_label_enabled = false
node.position = position
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _create_animal(target_id: StringName, position: Vector3) -> AnimalNode:
var node := AnimalNode.new()
node.animal_id = target_id
node.animal_definition_id = &"domestic_goat"
node.debug_label_enabled = false
node.position = position
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Context-scoped world targets passed: four kinds, isolation, lifecycle")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)