refactor: scope active world targets
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user