class_name ActiveWorldAdapter extends Node const LoadedResourceSpatialIndexScript := preload( "res://world/resource_nodes/LoadedResourceSpatialIndex.gd" ) const DEFAULT_CONTEXT_ID := &"jajce_interior" const DEFAULT_WORLD_ID := SimulationIds.REGIONAL_WORLD_BOSNIA const DEFAULT_LOCATION_ID := SimulationIds.REGIONAL_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 woodpile_storage: StorageNode @export_range(4.0, 128.0, 1.0) var resource_cell_size := 24.0 var _resource_index := LoadedResourceSpatialIndexScript.new() var _target_registry: WorldTargetRegistry var _target_handles_by_provider_id: Dictionary = {} func _ready() -> void: _resource_index.configure(resource_cell_size) add_to_group("active_world_adapter") _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: _resource_index.clear() _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) 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: 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, 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) func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: return _resource_index.get_all_candidates(action_id) func get_resource_candidates_in_radius( action_id: StringName, origin: Vector3, max_distance: float, min_distance_exclusive: float = -1.0 ) -> Array[Dictionary]: return _resource_index.get_candidates_in_radius( action_id, origin, max_distance, min_distance_exclusive ) func get_resource_query_profile(action_id: StringName, origin: Vector3) -> Dictionary: return _resource_index.get_query_profile(action_id, origin) func get_resource_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[ResourceNode]: return _resource_index.get_nodes_in_radius(origin, max_distance) func get_resource_index_stats() -> Dictionary: return _resource_index.get_stats() func get_animal_candidates(action_id: StringName) -> Array[Dictionary]: _ensure_target_registry() var candidates: Array[Dictionary] = [] for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL, action_id): var descriptor := _target_registry.get_descriptor(target_id) if descriptor == null: continue ( candidates . append( { "target_id": String(target_id), "position": descriptor.get_local_position(), "display_name": descriptor.get_display_name(), } ) ) return candidates func get_animal_routine_sites(species_id: StringName, animal_id: StringName) -> Array[Dictionary]: var candidates: Array[Dictionary] = [] for site in AnimalRoutineSite.get_all(): if not owns_target_node(site) or not site.supports_animal(animal_id, species_id): continue ( candidates . append( { "site_id": String(site.site_id), "position": site.get_routine_position(), "display_name": site.display_name, } ) ) candidates.sort_custom( func(first: Dictionary, second: Dictionary) -> bool: return String(first["site_id"]) < String(second["site_id"]) ) return candidates func get_animal_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[AnimalNode]: _ensure_target_registry() var nodes: Array[AnimalNode] = [] var maximum_distance_squared := maxf(max_distance, 0.0) * maxf(max_distance, 0.0) for target_id in _target_registry.get_target_ids(TARGET_KIND_ANIMAL): 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.sort_custom( func(first: AnimalNode, second: AnimalNode) -> bool: var first_distance := origin.distance_squared_to(first.get_interaction_position()) var second_distance := origin.distance_squared_to(second.get_interaction_position()) if not is_equal_approx(first_distance, second_distance): return first_distance < second_distance return String(first.animal_id) < String(second.animal_id) ) return nodes func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO) -> Dictionary: var best_candidate: Dictionary = {} var best_distance := INF for candidate in get_activity_candidates(action_id): var position: Vector3 = candidate["position"] var distance := origin.distance_squared_to(position) if distance < best_distance: best_distance = distance best_candidate = candidate if not best_candidate.is_empty(): return best_candidate match action_id: SimulationIds.ACTION_EAT: return get_storage_target(SimulationIds.STORAGE_VILLAGE_PANTRY) SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD: return get_storage_target(SimulationIds.STORAGE_VILLAGE_PANTRY) SimulationIds.ACTION_DEPOSIT_WOOD: return get_storage_target(SimulationIds.STORAGE_VILLAGE_WOODPILE) return {} func get_activity_candidates(action_id: StringName) -> Array[Dictionary]: _ensure_target_registry() var candidates: Array[Dictionary] = [] for target_id in _target_registry.get_target_ids(TARGET_KIND_ACTIVITY, action_id): var descriptor := _target_registry.get_descriptor(target_id) if descriptor == null: continue ( candidates . append( { "target_id": String(target_id), "position": descriptor.get_local_position(), "capacity": int(descriptor.get_metadata_value(&"capacity", 1)), } ) ) return candidates func get_storage_target(storage_id: StringName) -> Dictionary: _ensure_target_registry() var descriptor := _target_registry.get_descriptor(storage_id) if descriptor == null or descriptor.get_target_kind() != TARGET_KIND_STORAGE: return {} return { "target_id": String(storage_id), "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