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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0nt3xtw0rld7arg
|
||||||
+401
-34
@@ -5,30 +5,207 @@ const LoadedResourceSpatialIndexScript := preload(
|
|||||||
"res://world/resource_nodes/LoadedResourceSpatialIndex.gd"
|
"res://world/resource_nodes/LoadedResourceSpatialIndex.gd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DEFAULT_CONTEXT_ID := &"jajce_interior"
|
||||||
|
const DEFAULT_WORLD_ID := &"regional_bosnia"
|
||||||
|
const DEFAULT_LOCATION_ID := &"location_jajce"
|
||||||
|
const TARGET_KIND_RESOURCE := &"resource"
|
||||||
|
const TARGET_KIND_STORAGE := &"storage"
|
||||||
|
const TARGET_KIND_ACTIVITY := &"activity"
|
||||||
|
const TARGET_KIND_ANIMAL := &"animal"
|
||||||
|
const TARGET_GROUPS: Array[StringName] = [
|
||||||
|
&"resource_nodes", &"storage_nodes", &"activity_sites", &"animal_nodes"
|
||||||
|
]
|
||||||
|
|
||||||
|
@export var context_id: StringName = DEFAULT_CONTEXT_ID
|
||||||
|
@export var world_id: StringName = DEFAULT_WORLD_ID
|
||||||
|
@export var location_id: StringName = DEFAULT_LOCATION_ID
|
||||||
@export var pantry_storage: StorageNode
|
@export var pantry_storage: StorageNode
|
||||||
@export var woodpile_storage: StorageNode
|
@export var woodpile_storage: StorageNode
|
||||||
@export_range(4.0, 128.0, 1.0) var resource_cell_size := 24.0
|
@export_range(4.0, 128.0, 1.0) var resource_cell_size := 24.0
|
||||||
|
|
||||||
var _resource_index := LoadedResourceSpatialIndexScript.new()
|
var _resource_index := LoadedResourceSpatialIndexScript.new()
|
||||||
|
var _target_registry: WorldTargetRegistry
|
||||||
|
var _target_handles_by_provider_id: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_resource_index.configure(resource_cell_size)
|
_resource_index.configure(resource_cell_size)
|
||||||
add_to_group("active_world_adapter")
|
add_to_group("active_world_adapter")
|
||||||
rebuild_resource_index()
|
_ensure_target_registry()
|
||||||
|
rebuild_target_registry()
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_resource_index.clear()
|
||||||
|
_target_handles_by_provider_id.clear()
|
||||||
|
if _target_registry != null:
|
||||||
|
_target_registry.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func configure_context_identity(
|
||||||
|
next_context_id: StringName, next_world_id: StringName, next_location_id: StringName
|
||||||
|
) -> bool:
|
||||||
|
if next_context_id.is_empty() or next_world_id.is_empty() or next_location_id.is_empty():
|
||||||
|
return false
|
||||||
|
context_id = next_context_id
|
||||||
|
world_id = next_world_id
|
||||||
|
location_id = next_location_id
|
||||||
|
_target_registry = WorldTargetRegistry.new(context_id, world_id, location_id)
|
||||||
|
_target_handles_by_provider_id.clear()
|
||||||
|
_resource_index.clear()
|
||||||
|
if is_node_ready():
|
||||||
|
rebuild_target_registry()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_context_identity() -> Dictionary:
|
||||||
|
_ensure_target_registry()
|
||||||
|
return _target_registry.get_identity()
|
||||||
|
|
||||||
|
|
||||||
|
func get_target_registry() -> WorldTargetRegistry:
|
||||||
|
_ensure_target_registry()
|
||||||
|
return _target_registry
|
||||||
|
|
||||||
|
|
||||||
|
func get_target_handle(target_id: StringName) -> WorldTargetHandle:
|
||||||
|
_ensure_target_registry()
|
||||||
|
return _target_registry.get_handle(target_id)
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_target_handle(handle: WorldTargetHandle) -> WorldTargetDescriptor:
|
||||||
|
_ensure_target_registry()
|
||||||
|
return _target_registry.resolve_handle(handle)
|
||||||
|
|
||||||
|
|
||||||
|
func get_target_provider(target_id: StringName, target_kind: StringName = &"") -> Object:
|
||||||
|
_ensure_target_registry()
|
||||||
|
var handle := _target_registry.get_handle(target_id)
|
||||||
|
if handle == null or (not target_kind.is_empty() and handle.get_target_kind() != target_kind):
|
||||||
|
return null
|
||||||
|
return _target_registry.get_provider(handle)
|
||||||
|
|
||||||
|
|
||||||
|
func owns_target_node(target: Node) -> bool:
|
||||||
|
return find_for_node(target) == self
|
||||||
|
|
||||||
|
|
||||||
|
static func find_for_node(target: Node) -> ActiveWorldAdapter:
|
||||||
|
if target == null:
|
||||||
|
return null
|
||||||
|
var current: Node = target
|
||||||
|
while current != null:
|
||||||
|
if current is ActiveWorldAdapter:
|
||||||
|
return current as ActiveWorldAdapter
|
||||||
|
for child in current.get_children():
|
||||||
|
if child is ActiveWorldAdapter:
|
||||||
|
return child as ActiveWorldAdapter
|
||||||
|
current = current.get_parent()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func nodes_share_context(first: Node, second: Node) -> bool:
|
||||||
|
if first == null or second == null:
|
||||||
|
return false
|
||||||
|
return find_for_node(first) == find_for_node(second)
|
||||||
|
|
||||||
|
|
||||||
|
func rebuild_target_registry() -> void:
|
||||||
|
_ensure_target_registry()
|
||||||
|
_target_registry.clear()
|
||||||
|
_target_handles_by_provider_id.clear()
|
||||||
|
_resource_index.clear()
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
for group_id in TARGET_GROUPS:
|
||||||
|
for candidate in get_tree().get_nodes_in_group(group_id):
|
||||||
|
var provider := candidate as Node
|
||||||
|
if provider != null and owns_target_node(provider):
|
||||||
|
register_world_target(provider)
|
||||||
|
|
||||||
|
|
||||||
func rebuild_resource_index() -> void:
|
func rebuild_resource_index() -> void:
|
||||||
_resource_index.clear()
|
_resource_index.clear()
|
||||||
for node in ResourceNode.get_all():
|
_ensure_target_registry()
|
||||||
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_RESOURCE):
|
||||||
|
var node := get_target_provider(target_id, TARGET_KIND_RESOURCE) as ResourceNode
|
||||||
|
if node != null:
|
||||||
_resource_index.register_node(node, node.state)
|
_resource_index.register_node(node, node.state)
|
||||||
|
|
||||||
|
|
||||||
|
func register_world_target(provider: Node) -> WorldTargetHandle:
|
||||||
|
if provider == null or not owns_target_node(provider):
|
||||||
|
return null
|
||||||
|
_bind_provider(provider)
|
||||||
|
var descriptor := _descriptor_for_provider(provider)
|
||||||
|
if descriptor == null:
|
||||||
|
return null
|
||||||
|
var handle := _register_descriptor(provider, descriptor)
|
||||||
|
if handle != null and provider is ResourceNode:
|
||||||
|
var resource := provider as ResourceNode
|
||||||
|
if not _resource_index.register_node(resource, resource.state):
|
||||||
|
unregister_world_target(provider)
|
||||||
|
return null
|
||||||
|
return handle
|
||||||
|
|
||||||
|
|
||||||
|
func unregister_world_target(provider: Node) -> bool:
|
||||||
|
if provider == null:
|
||||||
|
return false
|
||||||
|
_ensure_target_registry()
|
||||||
|
var provider_id := provider.get_instance_id()
|
||||||
|
var handle := _target_handles_by_provider_id.get(provider_id) as WorldTargetHandle
|
||||||
|
if handle == null or _target_registry.get_provider(handle) != provider:
|
||||||
|
_target_handles_by_provider_id.erase(provider_id)
|
||||||
|
return false
|
||||||
|
var target_id := handle.get_target_id()
|
||||||
|
var target_kind := handle.get_target_kind()
|
||||||
|
var removed := _target_registry.unregister_target(handle)
|
||||||
|
_target_handles_by_provider_id.erase(provider_id)
|
||||||
|
if removed and target_kind == TARGET_KIND_RESOURCE:
|
||||||
|
_resource_index.unregister_node(target_id)
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_world_target_position(provider: Node) -> bool:
|
||||||
|
if provider == null or not owns_target_node(provider):
|
||||||
|
return false
|
||||||
|
_ensure_target_registry()
|
||||||
|
var handle := (
|
||||||
|
_target_handles_by_provider_id.get(provider.get_instance_id()) as WorldTargetHandle
|
||||||
|
)
|
||||||
|
if handle == null or not _target_registry.is_handle_valid(handle, true):
|
||||||
|
return register_world_target(provider) != null
|
||||||
|
var position := _interaction_position_for(provider)
|
||||||
|
if not _target_registry.update_target_position(handle, position):
|
||||||
|
return false
|
||||||
|
if provider is ResourceNode:
|
||||||
|
var resource := provider as ResourceNode
|
||||||
|
return _resource_index.register_node(resource, resource.state)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func register_resource_node(node: ResourceNode, state: ResourceStateRecord = null) -> bool:
|
func register_resource_node(node: ResourceNode, state: ResourceStateRecord = null) -> bool:
|
||||||
return _resource_index.register_node(node, state)
|
if node == null or not owns_target_node(node):
|
||||||
|
return false
|
||||||
|
_bind_provider(node)
|
||||||
|
var descriptor := _resource_descriptor(node, state)
|
||||||
|
var handle := _register_descriptor(node, descriptor)
|
||||||
|
return handle != null and _resource_index.register_node(node, state)
|
||||||
|
|
||||||
|
|
||||||
func unregister_resource_node(node_id: StringName) -> void:
|
func unregister_resource_node(node_id: StringName, provider: ResourceNode = null) -> void:
|
||||||
|
if provider != null:
|
||||||
|
unregister_world_target(provider)
|
||||||
|
return
|
||||||
|
_ensure_target_registry()
|
||||||
|
var handle := _target_registry.get_handle(node_id)
|
||||||
|
if handle == null or handle.get_target_kind() != TARGET_KIND_RESOURCE:
|
||||||
|
return
|
||||||
|
var registered_provider := _target_registry.get_provider(handle)
|
||||||
|
if registered_provider != null:
|
||||||
|
_target_handles_by_provider_id.erase(registered_provider.get_instance_id())
|
||||||
|
if _target_registry.unregister_target(handle):
|
||||||
_resource_index.unregister_node(node_id)
|
_resource_index.unregister_node(node_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,31 +237,29 @@ func get_resource_index_stats() -> Dictionary:
|
|||||||
|
|
||||||
|
|
||||||
func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
|
func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||||
|
_ensure_target_registry()
|
||||||
var candidates: Array[Dictionary] = []
|
var candidates: Array[Dictionary] = []
|
||||||
for node in AnimalNode.get_all():
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL, action_id):
|
||||||
if not node.supports_action(action_id):
|
var descriptor := _target_registry.get_descriptor(target_id)
|
||||||
|
if descriptor == null:
|
||||||
continue
|
continue
|
||||||
(
|
(
|
||||||
candidates
|
candidates
|
||||||
. append(
|
. append(
|
||||||
{
|
{
|
||||||
"target_id": String(node.animal_id),
|
"target_id": String(target_id),
|
||||||
"position": node.get_interaction_position(),
|
"position": descriptor.get_local_position(),
|
||||||
"display_name": node.display_name,
|
"display_name": descriptor.get_display_name(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
candidates.sort_custom(
|
|
||||||
func(first: Dictionary, second: Dictionary) -> bool:
|
|
||||||
return String(first["target_id"]) < String(second["target_id"])
|
|
||||||
)
|
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
func get_animal_routine_sites(species_id: StringName, animal_id: StringName) -> Array[Dictionary]:
|
func get_animal_routine_sites(species_id: StringName, animal_id: StringName) -> Array[Dictionary]:
|
||||||
var candidates: Array[Dictionary] = []
|
var candidates: Array[Dictionary] = []
|
||||||
for site in AnimalRoutineSite.get_all():
|
for site in AnimalRoutineSite.get_all():
|
||||||
if not site.supports_animal(animal_id, species_id):
|
if not owns_target_node(site) or not site.supports_animal(animal_id, species_id):
|
||||||
continue
|
continue
|
||||||
(
|
(
|
||||||
candidates
|
candidates
|
||||||
@@ -104,10 +279,21 @@ func get_animal_routine_sites(species_id: StringName, animal_id: StringName) ->
|
|||||||
|
|
||||||
|
|
||||||
func get_animal_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[AnimalNode]:
|
func get_animal_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[AnimalNode]:
|
||||||
|
_ensure_target_registry()
|
||||||
var nodes: Array[AnimalNode] = []
|
var nodes: Array[AnimalNode] = []
|
||||||
var maximum_distance_squared := maxf(max_distance, 0.0) * maxf(max_distance, 0.0)
|
var maximum_distance_squared := maxf(max_distance, 0.0) * maxf(max_distance, 0.0)
|
||||||
for node in AnimalNode.get_all():
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL):
|
||||||
if origin.distance_squared_to(node.get_interaction_position()) <= maximum_distance_squared:
|
var descriptor := _target_registry.get_descriptor(target_id)
|
||||||
|
if (
|
||||||
|
descriptor == null
|
||||||
|
or (
|
||||||
|
origin.distance_squared_to(descriptor.get_local_position())
|
||||||
|
> maximum_distance_squared
|
||||||
|
)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var node := get_target_provider(target_id, TARGET_KIND_ANIMAL) as AnimalNode
|
||||||
|
if node != null:
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
nodes.sort_custom(
|
nodes.sort_custom(
|
||||||
func(first: AnimalNode, second: AnimalNode) -> bool:
|
func(first: AnimalNode, second: AnimalNode) -> bool:
|
||||||
@@ -142,33 +328,214 @@ func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO)
|
|||||||
|
|
||||||
|
|
||||||
func get_activity_candidates(action_id: StringName) -> Array[Dictionary]:
|
func get_activity_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||||
|
_ensure_target_registry()
|
||||||
var candidates: Array[Dictionary] = []
|
var candidates: Array[Dictionary] = []
|
||||||
for candidate in ActivitySite.get_all():
|
for target_id in _target_registry.get_target_ids(TARGET_KIND_ACTIVITY, action_id):
|
||||||
var site := candidate as ActivitySite
|
var descriptor := _target_registry.get_descriptor(target_id)
|
||||||
if site == null:
|
if descriptor == null:
|
||||||
continue
|
continue
|
||||||
if not site.supports_action(action_id):
|
(
|
||||||
continue
|
candidates
|
||||||
candidates.append(
|
. append(
|
||||||
{
|
{
|
||||||
"target_id": String(site.site_id),
|
"target_id": String(target_id),
|
||||||
"position": site.get_interaction_position(),
|
"position": descriptor.get_local_position(),
|
||||||
"capacity": site.capacity
|
"capacity": int(descriptor.get_metadata_value(&"capacity", 1)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
func get_storage_target(storage_id: StringName) -> Dictionary:
|
func get_storage_target(storage_id: StringName) -> Dictionary:
|
||||||
var storage_node: StorageNode
|
_ensure_target_registry()
|
||||||
match storage_id:
|
var descriptor := _target_registry.get_descriptor(storage_id)
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY:
|
if descriptor == null or descriptor.get_target_kind() != TARGET_KIND_STORAGE:
|
||||||
storage_node = pantry_storage
|
|
||||||
SimulationIds.STORAGE_VILLAGE_WOODPILE:
|
|
||||||
storage_node = woodpile_storage
|
|
||||||
if storage_node == null:
|
|
||||||
return {}
|
return {}
|
||||||
return {
|
return {
|
||||||
"target_id": String(storage_node.storage_id),
|
"target_id": String(storage_id),
|
||||||
"position": storage_node.get_interaction_position()
|
"position": descriptor.get_local_position(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_target_registry() -> void:
|
||||||
|
if _target_registry == null:
|
||||||
|
_target_registry = WorldTargetRegistry.new(context_id, world_id, location_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _register_descriptor(provider: Node, descriptor: WorldTargetDescriptor) -> WorldTargetHandle:
|
||||||
|
_ensure_target_registry()
|
||||||
|
if provider == null or descriptor == null or not descriptor.is_valid():
|
||||||
|
return null
|
||||||
|
var provider_id := provider.get_instance_id()
|
||||||
|
var handle := _target_handles_by_provider_id.get(provider_id) as WorldTargetHandle
|
||||||
|
if handle != null and _target_registry.is_handle_valid(handle, true):
|
||||||
|
if (
|
||||||
|
handle.get_target_id() == descriptor.get_target_id()
|
||||||
|
and handle.get_target_kind() == descriptor.get_target_kind()
|
||||||
|
):
|
||||||
|
return handle if _target_registry.update_target(handle, descriptor) else null
|
||||||
|
unregister_world_target(provider)
|
||||||
|
|
||||||
|
var existing_handle := _target_registry.get_handle(descriptor.get_target_id())
|
||||||
|
if existing_handle != null:
|
||||||
|
if _target_registry.get_provider(existing_handle) != provider:
|
||||||
|
_target_registry.register_target(descriptor, provider)
|
||||||
|
return null
|
||||||
|
handle = existing_handle
|
||||||
|
if not _target_registry.update_target(handle, descriptor):
|
||||||
|
return null
|
||||||
|
else:
|
||||||
|
handle = _target_registry.register_target(descriptor, provider)
|
||||||
|
if handle != null:
|
||||||
|
_target_handles_by_provider_id[provider_id] = handle
|
||||||
|
return handle
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_provider(provider: Node) -> void:
|
||||||
|
if provider.has_method("_bind_active_world_adapter"):
|
||||||
|
provider.call("_bind_active_world_adapter", self)
|
||||||
|
|
||||||
|
|
||||||
|
func _descriptor_for_provider(provider: Node) -> WorldTargetDescriptor:
|
||||||
|
if provider is ResourceNode:
|
||||||
|
var resource := provider as ResourceNode
|
||||||
|
return _resource_descriptor(resource, resource.state)
|
||||||
|
if provider is StorageNode:
|
||||||
|
return _storage_descriptor(provider as StorageNode)
|
||||||
|
if provider is ActivitySite:
|
||||||
|
return _activity_descriptor(provider as ActivitySite)
|
||||||
|
if provider is AnimalNode:
|
||||||
|
return _animal_descriptor(provider as AnimalNode)
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _resource_descriptor(
|
||||||
|
node: ResourceNode, state: ResourceStateRecord = null
|
||||||
|
) -> WorldTargetDescriptor:
|
||||||
|
if node == null or node.node_id.is_empty():
|
||||||
|
return null
|
||||||
|
var action_id := state.get_action_id() if state != null else node.action_id
|
||||||
|
var resource_id := state.get_resource_id() if state != null else node.resource_id
|
||||||
|
var capability := (
|
||||||
|
WorldTargetCapability
|
||||||
|
. new(
|
||||||
|
action_id,
|
||||||
|
{
|
||||||
|
"resource_id": String(resource_id),
|
||||||
|
"can_npcs_use": node.can_npcs_use,
|
||||||
|
"can_player_use": node.can_player_use,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
WorldTargetDescriptor
|
||||||
|
. new(
|
||||||
|
node.node_id,
|
||||||
|
TARGET_KIND_RESOURCE,
|
||||||
|
[capability],
|
||||||
|
_interaction_position_for(node),
|
||||||
|
String(node.name),
|
||||||
|
{
|
||||||
|
"resource_id": String(resource_id),
|
||||||
|
"safety_risk": state.get_safety_risk() if state != null else node.safety_risk,
|
||||||
|
"comfort_distance":
|
||||||
|
state.get_comfort_distance() if state != null else node.comfort_distance,
|
||||||
|
"discovery_priority":
|
||||||
|
state.get_discovery_priority() if state != null else node.discovery_priority,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _storage_descriptor(node: StorageNode) -> WorldTargetDescriptor:
|
||||||
|
if node == null or node.storage_id.is_empty():
|
||||||
|
return null
|
||||||
|
var capability_ids: Array[StringName] = []
|
||||||
|
if node.accepts_item(SimulationIds.RESOURCE_FOOD):
|
||||||
|
(
|
||||||
|
capability_ids
|
||||||
|
. append_array(
|
||||||
|
[
|
||||||
|
SimulationIds.ACTION_EAT,
|
||||||
|
SimulationIds.ACTION_DEPOSIT_FOOD,
|
||||||
|
SimulationIds.ACTION_WITHDRAW_FOOD,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if node.accepts_item(SimulationIds.RESOURCE_WOOD):
|
||||||
|
capability_ids.append(SimulationIds.ACTION_DEPOSIT_WOOD)
|
||||||
|
var capabilities: Array[WorldTargetCapability] = []
|
||||||
|
for capability_id in capability_ids:
|
||||||
|
capabilities.append(WorldTargetCapability.new(capability_id))
|
||||||
|
var accepted_item_ids: Array[String] = []
|
||||||
|
for item_id in node.accepted_items:
|
||||||
|
accepted_item_ids.append(String(item_id))
|
||||||
|
return WorldTargetDescriptor.new(
|
||||||
|
node.storage_id,
|
||||||
|
TARGET_KIND_STORAGE,
|
||||||
|
capabilities,
|
||||||
|
_interaction_position_for(node),
|
||||||
|
node.display_name,
|
||||||
|
{"accepted_item_ids": accepted_item_ids}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _activity_descriptor(node: ActivitySite) -> WorldTargetDescriptor:
|
||||||
|
if node == null or node.site_id.is_empty() or not node.is_interaction_range_valid():
|
||||||
|
return null
|
||||||
|
var capability_ids: Array[StringName] = [node.action_id]
|
||||||
|
if node.action_id == SimulationIds.ACTION_PATROL:
|
||||||
|
capability_ids.append(SimulationIds.ACTION_DEFEND)
|
||||||
|
var capabilities: Array[WorldTargetCapability] = []
|
||||||
|
for capability_id in capability_ids:
|
||||||
|
capabilities.append(
|
||||||
|
WorldTargetCapability.new(capability_id, {"interaction_range": node.interaction_range})
|
||||||
|
)
|
||||||
|
return WorldTargetDescriptor.new(
|
||||||
|
node.site_id,
|
||||||
|
TARGET_KIND_ACTIVITY,
|
||||||
|
capabilities,
|
||||||
|
_interaction_position_for(node),
|
||||||
|
node.display_name,
|
||||||
|
{"capacity": node.capacity}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _animal_descriptor(node: AnimalNode) -> WorldTargetDescriptor:
|
||||||
|
if node == null or node.animal_id.is_empty():
|
||||||
|
return null
|
||||||
|
var capabilities: Array[WorldTargetCapability] = []
|
||||||
|
for capability_id in node.supported_action_ids:
|
||||||
|
capabilities.append(WorldTargetCapability.new(capability_id))
|
||||||
|
return (
|
||||||
|
WorldTargetDescriptor
|
||||||
|
. new(
|
||||||
|
node.animal_id,
|
||||||
|
TARGET_KIND_ANIMAL,
|
||||||
|
capabilities,
|
||||||
|
_interaction_position_for(node),
|
||||||
|
node.display_name,
|
||||||
|
{
|
||||||
|
"animal_definition_id": String(node.animal_definition_id),
|
||||||
|
"species_id": String(node.species_id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _interaction_position_for(provider: Node) -> Vector3:
|
||||||
|
if provider is ResourceNode:
|
||||||
|
var resource := provider as ResourceNode
|
||||||
|
return (
|
||||||
|
resource.interaction_point.global_position
|
||||||
|
if resource.interaction_point != null
|
||||||
|
else resource.global_position
|
||||||
|
)
|
||||||
|
if provider is StorageNode:
|
||||||
|
return (provider as StorageNode).get_interaction_position()
|
||||||
|
if provider is ActivitySite:
|
||||||
|
return (provider as ActivitySite).get_interaction_position()
|
||||||
|
if provider is AnimalNode:
|
||||||
|
return (provider as AnimalNode).get_interaction_position()
|
||||||
|
return Vector3.ZERO
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ static var _all: Array = []
|
|||||||
@export var action_id: StringName = SimulationIds.ACTION_REST
|
@export var action_id: StringName = SimulationIds.ACTION_REST
|
||||||
@export var display_name := "Activity Site"
|
@export var display_name := "Activity Site"
|
||||||
@export_range(1, 12, 1) var capacity := 1
|
@export_range(1, 12, 1) var capacity := 1
|
||||||
|
@export_range(0.1, 100.0, 0.1, "or_greater") var interaction_range := 3.0
|
||||||
@export var debug_label_enabled := true
|
@export var debug_label_enabled := true
|
||||||
|
|
||||||
@onready var interaction_point: Marker3D = $InteractionPoint
|
@onready var interaction_point: Marker3D = $InteractionPoint
|
||||||
|
|
||||||
var _last_label_text := ""
|
var _last_label_text := ""
|
||||||
|
var _active_world_adapter: ActiveWorldAdapter
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -19,7 +21,12 @@ func _ready() -> void:
|
|||||||
push_error("ActivitySite at %s has empty site_id" % get_path())
|
push_error("ActivitySite at %s has empty site_id" % get_path())
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
return
|
return
|
||||||
var existing: ActivitySite = get_by_id(site_id) as ActivitySite
|
if not is_interaction_range_valid():
|
||||||
|
push_error("ActivitySite '%s' must have a positive finite interaction_range" % site_id)
|
||||||
|
_update_presentation()
|
||||||
|
return
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
var existing: ActivitySite = get_by_id_in_context(site_id, _active_world_adapter)
|
||||||
if existing != null:
|
if existing != null:
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
@@ -31,11 +38,14 @@ func _ready() -> void:
|
|||||||
return
|
return
|
||||||
_all.append(self)
|
_all.append(self)
|
||||||
add_to_group("activity_sites")
|
add_to_group("activity_sites")
|
||||||
|
set_notify_transform(true)
|
||||||
|
_notify_world_adapter()
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
set_process(false)
|
set_process(false)
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
|
_unregister_from_world_adapter()
|
||||||
_all.erase(self)
|
_all.erase(self)
|
||||||
|
|
||||||
|
|
||||||
@@ -52,6 +62,41 @@ func supports_action(candidate_action_id: StringName) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_interaction_range_valid() -> bool:
|
||||||
|
return is_finite(interaction_range) and interaction_range > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready():
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.refresh_world_target_position(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _notify_world_adapter() -> void:
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
not is_instance_valid(_active_world_adapter)
|
||||||
|
or not _active_world_adapter.owns_target_node(self)
|
||||||
|
):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if _active_world_adapter != null:
|
||||||
|
_active_world_adapter.register_world_target(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_active_world_adapter(adapter: ActiveWorldAdapter) -> void:
|
||||||
|
if adapter != null and adapter.owns_target_node(self):
|
||||||
|
_active_world_adapter = adapter
|
||||||
|
|
||||||
|
|
||||||
|
func _unregister_from_world_adapter() -> void:
|
||||||
|
if not is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.unregister_world_target(self)
|
||||||
|
_active_world_adapter = null
|
||||||
|
|
||||||
|
|
||||||
func _update_presentation() -> void:
|
func _update_presentation() -> void:
|
||||||
if not has_node("DebugLabel"):
|
if not has_node("DebugLabel"):
|
||||||
return
|
return
|
||||||
@@ -78,5 +123,14 @@ static func get_by_id(search_id: StringName):
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func get_by_id_in_context(
|
||||||
|
search_id: StringName, adapter: ActiveWorldAdapter
|
||||||
|
) -> ActivitySite:
|
||||||
|
for node in _all:
|
||||||
|
if node.site_id == search_id and ActiveWorldAdapter.find_for_node(node) == adapter:
|
||||||
|
return node as ActivitySite
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
static func get_all() -> Array:
|
static func get_all() -> Array:
|
||||||
return _all.duplicate()
|
return _all.duplicate()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var path_pending := false
|
|||||||
var has_reported_navigation_result := true
|
var has_reported_navigation_result := true
|
||||||
var _animal_definition: AnimalDefinition
|
var _animal_definition: AnimalDefinition
|
||||||
var _presentation_parameters: Dictionary = {}
|
var _presentation_parameters: Dictionary = {}
|
||||||
|
var _active_world_adapter: ActiveWorldAdapter
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -59,7 +60,8 @@ func _ready() -> void:
|
|||||||
initial_enabled = false
|
initial_enabled = false
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
return
|
return
|
||||||
var existing := get_by_id(animal_id)
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
var existing := get_by_id_in_context(animal_id, _active_world_adapter)
|
||||||
if existing != null:
|
if existing != null:
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
@@ -73,12 +75,15 @@ func _ready() -> void:
|
|||||||
_all.append(self)
|
_all.append(self)
|
||||||
add_to_group("animal_nodes")
|
add_to_group("animal_nodes")
|
||||||
add_to_group("grass_interactors")
|
add_to_group("grass_interactors")
|
||||||
|
set_notify_transform(true)
|
||||||
_try_register_with_simulation()
|
_try_register_with_simulation()
|
||||||
|
_notify_world_adapter()
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
navigation_request_id += 1
|
navigation_request_id += 1
|
||||||
|
_unregister_from_world_adapter()
|
||||||
_all.erase(self)
|
_all.erase(self)
|
||||||
_disconnect_state()
|
_disconnect_state()
|
||||||
state = null
|
state = null
|
||||||
@@ -105,6 +110,7 @@ func bind_state(animal_state: AnimalStateRecord) -> bool:
|
|||||||
call_deferred("_sync_navigation_from_state")
|
call_deferred("_sync_navigation_from_state")
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
hunger_changed.emit(animal_id, state.get_hunger())
|
hunger_changed.emit(animal_id, state.get_hunger())
|
||||||
|
_notify_world_adapter()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
@@ -126,6 +132,37 @@ func _try_register_with_simulation() -> void:
|
|||||||
manager.animal_care.register_node(self)
|
manager.animal_care.register_node(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready():
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.refresh_world_target_position(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _notify_world_adapter() -> void:
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
not is_instance_valid(_active_world_adapter)
|
||||||
|
or not _active_world_adapter.owns_target_node(self)
|
||||||
|
):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if _active_world_adapter != null:
|
||||||
|
_active_world_adapter.register_world_target(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_active_world_adapter(adapter: ActiveWorldAdapter) -> void:
|
||||||
|
if adapter != null and adapter.owns_target_node(self):
|
||||||
|
_active_world_adapter = adapter
|
||||||
|
|
||||||
|
|
||||||
|
func _unregister_from_world_adapter() -> void:
|
||||||
|
if not is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.unregister_world_target(self)
|
||||||
|
_active_world_adapter = null
|
||||||
|
|
||||||
|
|
||||||
func get_interaction_position() -> Vector3:
|
func get_interaction_position() -> Vector3:
|
||||||
return interaction_point.global_position if interaction_point != null else global_position
|
return interaction_point.global_position if interaction_point != null else global_position
|
||||||
|
|
||||||
@@ -381,5 +418,12 @@ static func get_by_id(search_id: StringName) -> AnimalNode:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func get_by_id_in_context(search_id: StringName, adapter: ActiveWorldAdapter) -> AnimalNode:
|
||||||
|
for node in _all:
|
||||||
|
if node.animal_id == search_id and ActiveWorldAdapter.find_for_node(node) == adapter:
|
||||||
|
return node
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
static func get_all() -> Array[AnimalNode]:
|
static func get_all() -> Array[AnimalNode]:
|
||||||
return _all.duplicate()
|
return _all.duplicate()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ static var _all: Array[ResourceNode] = []
|
|||||||
@onready var interaction_point: Marker3D = $InteractionPoint
|
@onready var interaction_point: Marker3D = $InteractionPoint
|
||||||
|
|
||||||
var state: ResourceStateRecord
|
var state: ResourceStateRecord
|
||||||
|
var _active_world_adapter: ActiveWorldAdapter
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -34,7 +35,8 @@ func _ready() -> void:
|
|||||||
initial_enabled = false
|
initial_enabled = false
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
return
|
return
|
||||||
var existing := get_by_id(node_id)
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
var existing := get_by_id_in_context(node_id, _active_world_adapter)
|
||||||
if existing != null:
|
if existing != null:
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
@@ -49,12 +51,12 @@ func _ready() -> void:
|
|||||||
add_to_group("resource_nodes")
|
add_to_group("resource_nodes")
|
||||||
set_notify_transform(true)
|
set_notify_transform(true)
|
||||||
_try_register_with_simulation()
|
_try_register_with_simulation()
|
||||||
_notify_world_adapters()
|
_notify_world_adapter()
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
_unregister_from_world_adapters()
|
_unregister_from_world_adapter()
|
||||||
_all.erase(self)
|
_all.erase(self)
|
||||||
if state != null and state.changed.is_connected(_on_state_changed):
|
if state != null and state.changed.is_connected(_on_state_changed):
|
||||||
state.changed.disconnect(_on_state_changed)
|
state.changed.disconnect(_on_state_changed)
|
||||||
@@ -77,7 +79,7 @@ func bind_state(resource_state: ResourceStateRecord) -> bool:
|
|||||||
state.depleted.connect(_on_state_depleted)
|
state.depleted.connect(_on_state_depleted)
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
amount_changed.emit(node_id, state.get_amount_remaining())
|
amount_changed.emit(node_id, state.get_amount_remaining())
|
||||||
_notify_world_adapters()
|
_notify_world_adapter()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
@@ -92,23 +94,37 @@ func _try_register_with_simulation() -> void:
|
|||||||
|
|
||||||
func _notification(what: int) -> void:
|
func _notification(what: int) -> void:
|
||||||
if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready():
|
if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready():
|
||||||
_notify_world_adapters()
|
_refresh_world_target_position()
|
||||||
|
|
||||||
|
|
||||||
func _notify_world_adapters() -> void:
|
func _notify_world_adapter() -> void:
|
||||||
if not is_inside_tree():
|
if not is_inside_tree():
|
||||||
return
|
return
|
||||||
for adapter in get_tree().get_nodes_in_group("active_world_adapter"):
|
if (
|
||||||
if adapter.has_method("register_resource_node"):
|
not is_instance_valid(_active_world_adapter)
|
||||||
adapter.register_resource_node(self, state)
|
or not _active_world_adapter.owns_target_node(self)
|
||||||
|
):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if _active_world_adapter != null:
|
||||||
|
_active_world_adapter.register_resource_node(self, state)
|
||||||
|
|
||||||
|
|
||||||
func _unregister_from_world_adapters() -> void:
|
func _bind_active_world_adapter(adapter: ActiveWorldAdapter) -> void:
|
||||||
if not is_inside_tree():
|
if adapter != null and adapter.owns_target_node(self):
|
||||||
return
|
_active_world_adapter = adapter
|
||||||
for adapter in get_tree().get_nodes_in_group("active_world_adapter"):
|
|
||||||
if adapter.has_method("unregister_resource_node"):
|
|
||||||
adapter.unregister_resource_node(node_id)
|
func _refresh_world_target_position() -> void:
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.refresh_world_target_position(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _unregister_from_world_adapter() -> void:
|
||||||
|
if not is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.unregister_resource_node(node_id, self)
|
||||||
|
_active_world_adapter = null
|
||||||
|
|
||||||
|
|
||||||
func get_amount_remaining() -> float:
|
func get_amount_remaining() -> float:
|
||||||
@@ -177,5 +193,14 @@ static func get_by_id(search_id: StringName) -> ResourceNode:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func get_by_id_in_context(
|
||||||
|
search_id: StringName, adapter: ActiveWorldAdapter
|
||||||
|
) -> ResourceNode:
|
||||||
|
for node in _all:
|
||||||
|
if node.node_id == search_id and ActiveWorldAdapter.find_for_node(node) == adapter:
|
||||||
|
return node
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
static func get_all() -> Array[ResourceNode]:
|
static func get_all() -> Array[ResourceNode]:
|
||||||
return _all.duplicate()
|
return _all.duplicate()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const PRESENTATION_UPDATE_INTERVAL_SECONDS := 0.25
|
|||||||
var state: StorageStateRecord
|
var state: StorageStateRecord
|
||||||
var _last_label_text := ""
|
var _last_label_text := ""
|
||||||
var _presentation_elapsed := 0.0
|
var _presentation_elapsed := 0.0
|
||||||
|
var _active_world_adapter: ActiveWorldAdapter
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -24,7 +25,8 @@ func _ready() -> void:
|
|||||||
push_error("StorageNode at %s has empty storage_id" % get_path())
|
push_error("StorageNode at %s has empty storage_id" % get_path())
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
return
|
return
|
||||||
var existing: StorageNode = get_by_id(storage_id) as StorageNode
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
var existing: StorageNode = get_by_id_in_context(storage_id, _active_world_adapter)
|
||||||
if existing != null:
|
if existing != null:
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
@@ -36,7 +38,9 @@ func _ready() -> void:
|
|||||||
return
|
return
|
||||||
_all.append(self)
|
_all.append(self)
|
||||||
add_to_group("storage_nodes")
|
add_to_group("storage_nodes")
|
||||||
|
set_notify_transform(true)
|
||||||
_try_register_with_simulation()
|
_try_register_with_simulation()
|
||||||
|
_notify_world_adapter()
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
set_process(debug_label_enabled)
|
set_process(debug_label_enabled)
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ func _process(delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
|
_unregister_from_world_adapter()
|
||||||
_all.erase(self)
|
_all.erase(self)
|
||||||
state = null
|
state = null
|
||||||
|
|
||||||
@@ -59,6 +64,7 @@ func bind_state(storage_state: StorageStateRecord) -> bool:
|
|||||||
return false
|
return false
|
||||||
state = storage_state
|
state = storage_state
|
||||||
_update_presentation()
|
_update_presentation()
|
||||||
|
_notify_world_adapter()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +85,37 @@ func _try_register_with_simulation() -> void:
|
|||||||
manager.register_storage_node(self)
|
manager.register_storage_node(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready():
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.refresh_world_target_position(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _notify_world_adapter() -> void:
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
not is_instance_valid(_active_world_adapter)
|
||||||
|
or not _active_world_adapter.owns_target_node(self)
|
||||||
|
):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if _active_world_adapter != null:
|
||||||
|
_active_world_adapter.register_world_target(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_active_world_adapter(adapter: ActiveWorldAdapter) -> void:
|
||||||
|
if adapter != null and adapter.owns_target_node(self):
|
||||||
|
_active_world_adapter = adapter
|
||||||
|
|
||||||
|
|
||||||
|
func _unregister_from_world_adapter() -> void:
|
||||||
|
if not is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter = ActiveWorldAdapter.find_for_node(self)
|
||||||
|
if is_instance_valid(_active_world_adapter):
|
||||||
|
_active_world_adapter.unregister_world_target(self)
|
||||||
|
_active_world_adapter = null
|
||||||
|
|
||||||
|
|
||||||
func _update_presentation() -> void:
|
func _update_presentation() -> void:
|
||||||
if not has_node("DebugLabel"):
|
if not has_node("DebugLabel"):
|
||||||
return
|
return
|
||||||
@@ -112,5 +149,12 @@ static func get_by_id(search_id: StringName):
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
static func get_by_id_in_context(search_id: StringName, adapter: ActiveWorldAdapter) -> StorageNode:
|
||||||
|
for node in _all:
|
||||||
|
if node.storage_id == search_id and ActiveWorldAdapter.find_for_node(node) == adapter:
|
||||||
|
return node as StorageNode
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
static func get_all() -> Array:
|
static func get_all() -> Array:
|
||||||
return _all.duplicate()
|
return _all.duplicate()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ extends RefCounted
|
|||||||
var last_error := ""
|
var last_error := ""
|
||||||
|
|
||||||
var _context_id: StringName
|
var _context_id: StringName
|
||||||
|
var _world_id: StringName
|
||||||
|
var _location_id: StringName
|
||||||
var _entries_by_id: Dictionary = {}
|
var _entries_by_id: Dictionary = {}
|
||||||
var _generation_by_id: Dictionary = {}
|
var _generation_by_id: Dictionary = {}
|
||||||
var _sorted_target_ids: Array[StringName] = []
|
var _sorted_target_ids: Array[StringName] = []
|
||||||
@@ -11,8 +13,14 @@ var _target_ids_by_kind: Dictionary = {}
|
|||||||
var _target_ids_by_capability: Dictionary = {}
|
var _target_ids_by_capability: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _init(owned_context_id: StringName = &"") -> void:
|
func _init(
|
||||||
|
owned_context_id: StringName = &"",
|
||||||
|
owned_world_id: StringName = &"",
|
||||||
|
owned_location_id: StringName = &""
|
||||||
|
) -> void:
|
||||||
_context_id = owned_context_id
|
_context_id = owned_context_id
|
||||||
|
_world_id = owned_world_id
|
||||||
|
_location_id = owned_location_id
|
||||||
|
|
||||||
|
|
||||||
func register_target(
|
func register_target(
|
||||||
@@ -75,6 +83,19 @@ func update_target(handle: WorldTargetHandle, descriptor: WorldTargetDescriptor)
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func update_target_position(handle: WorldTargetHandle, local_position: Vector3) -> bool:
|
||||||
|
last_error = ""
|
||||||
|
var entry := _get_handle_entry(handle)
|
||||||
|
if entry.is_empty():
|
||||||
|
return _fail_bool("Target handle is stale or belongs to another registry")
|
||||||
|
if not local_position.is_finite():
|
||||||
|
return _fail_bool("Target position must be finite")
|
||||||
|
var previous := entry["descriptor"] as WorldTargetDescriptor
|
||||||
|
entry["descriptor"] = previous.with_local_position(local_position)
|
||||||
|
_entries_by_id[previous.get_target_id()] = entry
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func unregister_target(handle: WorldTargetHandle) -> bool:
|
func unregister_target(handle: WorldTargetHandle) -> bool:
|
||||||
last_error = ""
|
last_error = ""
|
||||||
var entry := _get_handle_entry(handle)
|
var entry := _get_handle_entry(handle)
|
||||||
@@ -112,6 +133,22 @@ func get_context_id() -> StringName:
|
|||||||
return _context_id
|
return _context_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_world_id() -> StringName:
|
||||||
|
return _world_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_location_id() -> StringName:
|
||||||
|
return _location_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_identity() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"context_id": _context_id,
|
||||||
|
"world_id": _world_id,
|
||||||
|
"location_id": _location_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func size() -> int:
|
func size() -> int:
|
||||||
return _entries_by_id.size()
|
return _entries_by_id.size()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user