322 lines
10 KiB
GDScript
322 lines
10 KiB
GDScript
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)
|