Files
2026-07-17 00:34:30 +02:00

280 lines
9.2 KiB
GDScript

class_name LoadedResourceSpatialIndex
extends RefCounted
const DEFAULT_CELL_SIZE := 24.0
var cell_size := DEFAULT_CELL_SIZE
var _entries: Dictionary = {}
var _action_cells: Dictionary = {}
var _all_cells: Dictionary = {}
var _profiles: Dictionary = {}
var _dirty_profiles: Dictionary = {}
var _next_registration_order := 0
func configure(configured_cell_size: float) -> void:
cell_size = maxf(configured_cell_size, 1.0)
func clear() -> void:
_entries.clear()
_action_cells.clear()
_all_cells.clear()
_profiles.clear()
_dirty_profiles.clear()
_next_registration_order = 0
func register_node(node: ResourceNode, state: ResourceStateRecord = null) -> bool:
if node == null or node.node_id.is_empty() or node.interaction_point == null:
return false
var registration_order := _next_registration_order
var existing: Dictionary = _entries.get(node.node_id, {})
if not existing.is_empty():
registration_order = int(existing["registration_order"])
_remove_entry(existing)
else:
_next_registration_order += 1
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 position := node.global_transform * node.interaction_point.position
var safety_risk := state.get_safety_risk() if state != null else node.safety_risk
var comfort_distance := state.get_comfort_distance() if state != null else node.comfort_distance
var discovery_priority := (
state.get_discovery_priority() if state != null else node.discovery_priority
)
var entry := {
"target_id": node.node_id,
"action_id": action_id,
"resource_id": resource_id,
"position": position,
"safety_risk": safety_risk,
"comfort_distance": comfort_distance,
"discovery_priority": discovery_priority,
"registration_order": registration_order,
"cell": _cell_for(position),
"node": node,
}
_entries[node.node_id] = entry
_add_to_cell(_all_cells, entry["cell"], node.node_id)
var cells: Dictionary = _action_cells.get(action_id, {})
_add_to_cell(cells, entry["cell"], node.node_id)
_action_cells[action_id] = cells
_dirty_profiles[action_id] = true
return true
func unregister_node(node_id: StringName) -> void:
var entry: Dictionary = _entries.get(node_id, {})
if entry.is_empty():
return
_remove_entry(entry)
func get_all_candidates(action_id: StringName) -> Array[Dictionary]:
var matching_entries: Array[Dictionary] = []
for entry_value in _entries.values():
var entry: Dictionary = entry_value
if entry["action_id"] == action_id:
matching_entries.append(entry)
matching_entries.sort_custom(_entry_order_less)
return _entries_to_candidates(matching_entries)
func get_candidates_in_radius(
action_id: StringName,
origin: Vector3,
max_distance: float,
min_distance_exclusive: float = -1.0
) -> Array[Dictionary]:
var cells: Dictionary = _action_cells.get(action_id, {})
var matching_entries := _get_entries_in_radius(
cells, origin, max_distance, min_distance_exclusive
)
return _entries_to_candidates(matching_entries)
func get_nodes_in_radius(origin: Vector3, max_distance: float) -> Array[ResourceNode]:
var nodes: Array[ResourceNode] = []
for entry in _get_entries_in_radius(_all_cells, origin, max_distance):
var node := entry["node"] as ResourceNode
if is_instance_valid(node):
nodes.append(node)
return nodes
func get_query_profile(action_id: StringName, origin: Vector3) -> Dictionary:
_ensure_profile(action_id)
var profile: Dictionary = _profiles.get(action_id, {})
if profile.is_empty():
return {"candidate_count": 0, "initial_radius": cell_size, "max_distance": 0.0}
var minimum: Vector3 = profile["minimum"]
var maximum: Vector3 = profile["maximum"]
var farthest_delta := Vector3(
maxf(absf(origin.x - minimum.x), absf(origin.x - maximum.x)),
maxf(absf(origin.y - minimum.y), absf(origin.y - maximum.y)),
maxf(absf(origin.z - minimum.z), absf(origin.z - maximum.z))
)
var result := profile.duplicate()
result.erase("minimum")
result.erase("maximum")
result["initial_radius"] = cell_size
result["max_distance"] = farthest_delta.length()
return result
func get_stats() -> Dictionary:
var action_counts := {}
for entry_value in _entries.values():
var action_id: StringName = entry_value["action_id"]
action_counts[String(action_id)] = int(action_counts.get(String(action_id), 0)) + 1
return {
"candidate_count": _entries.size(),
"occupied_cell_count": _all_cells.size(),
"cell_size": cell_size,
"action_counts": action_counts,
}
func _ensure_profile(action_id: StringName) -> void:
if not _dirty_profiles.has(action_id) and _profiles.has(action_id):
return
var count := 0
var minimum := Vector3.ZERO
var maximum := Vector3.ZERO
var min_safety_risk := INF
var max_comfort_distance := 0.0
var max_discovery_priority := -INF
for entry_value in _entries.values():
var entry: Dictionary = entry_value
if entry["action_id"] != action_id:
continue
var position: Vector3 = entry["position"]
if count == 0:
minimum = position
maximum = position
else:
minimum = minimum.min(position)
maximum = maximum.max(position)
count += 1
min_safety_risk = minf(min_safety_risk, float(entry["safety_risk"]))
max_comfort_distance = maxf(max_comfort_distance, float(entry["comfort_distance"]))
max_discovery_priority = maxf(max_discovery_priority, float(entry["discovery_priority"]))
if count == 0:
_profiles.erase(action_id)
else:
_profiles[action_id] = {
"candidate_count": count,
"occupied_cell_count": (_action_cells.get(action_id, {}) as Dictionary).size(),
"minimum": minimum,
"maximum": maximum,
"min_safety_risk": min_safety_risk,
"max_comfort_distance": max_comfort_distance,
"max_discovery_priority": max_discovery_priority,
}
_dirty_profiles.erase(action_id)
func _get_entries_in_radius(
cells: Dictionary, origin: Vector3, max_distance: float, min_distance_exclusive: float = -1.0
) -> Array[Dictionary]:
var matching_entries: Array[Dictionary] = []
if cells.is_empty() or max_distance < 0.0:
return matching_entries
var minimum_cell := _cell_for(
Vector3(origin.x - max_distance, origin.y, origin.z - max_distance)
)
var maximum_cell := _cell_for(
Vector3(origin.x + max_distance, origin.y, origin.z + max_distance)
)
var candidate_ids := _get_candidate_ids(cells, minimum_cell, maximum_cell)
var maximum_distance_squared := max_distance * max_distance
var minimum_distance_squared := min_distance_exclusive * min_distance_exclusive
for node_id in candidate_ids:
var entry: Dictionary = _entries.get(node_id, {})
if entry.is_empty():
continue
var distance_squared := origin.distance_squared_to(entry["position"])
if distance_squared > maximum_distance_squared:
continue
if min_distance_exclusive >= 0.0 and distance_squared <= minimum_distance_squared:
continue
matching_entries.append(entry)
matching_entries.sort_custom(_entry_order_less)
return matching_entries
func _get_candidate_ids(cells: Dictionary, minimum_cell: Vector2i, maximum_cell: Vector2i) -> Array:
var candidate_ids: Array = []
var rectangle_cell_count := (
(maximum_cell.x - minimum_cell.x + 1) * (maximum_cell.y - minimum_cell.y + 1)
)
if rectangle_cell_count <= cells.size() * 4:
for cell_x in range(minimum_cell.x, maximum_cell.x + 1):
for cell_y in range(minimum_cell.y, maximum_cell.y + 1):
candidate_ids.append_array(cells.get(Vector2i(cell_x, cell_y), []))
return candidate_ids
for cell_value in cells:
var cell: Vector2i = cell_value
if (
cell.x >= minimum_cell.x
and cell.x <= maximum_cell.x
and cell.y >= minimum_cell.y
and cell.y <= maximum_cell.y
):
candidate_ids.append_array(cells[cell])
return candidate_ids
func _entries_to_candidates(entries: Array[Dictionary]) -> Array[Dictionary]:
var candidates: Array[Dictionary] = []
for entry in entries:
var candidate := {
"target_id": String(entry["target_id"]),
"position": entry["position"],
"resource_id": String(entry["resource_id"]),
"safety_risk": entry["safety_risk"],
"comfort_distance": entry["comfort_distance"],
"discovery_priority": entry["discovery_priority"],
"registration_order": entry["registration_order"],
}
candidates.append(candidate)
return candidates
func _remove_entry(entry: Dictionary) -> void:
var node_id := StringName(entry["target_id"])
var action_id := StringName(entry["action_id"])
_remove_from_cell(_all_cells, entry["cell"], node_id)
var cells: Dictionary = _action_cells.get(action_id, {})
_remove_from_cell(cells, entry["cell"], node_id)
if cells.is_empty():
_action_cells.erase(action_id)
else:
_action_cells[action_id] = cells
_entries.erase(node_id)
_dirty_profiles[action_id] = true
func _cell_for(position: Vector3) -> Vector2i:
return Vector2i(floori(position.x / cell_size), floori(position.z / cell_size))
func _add_to_cell(cells: Dictionary, cell: Vector2i, node_id: StringName) -> void:
var ids: Array = cells.get(cell, [])
ids.append(node_id)
cells[cell] = ids
func _remove_from_cell(cells: Dictionary, cell: Vector2i, node_id: StringName) -> void:
var ids: Array = cells.get(cell, [])
ids.erase(node_id)
if ids.is_empty():
cells.erase(cell)
else:
cells[cell] = ids
func _entry_order_less(first: Dictionary, second: Dictionary) -> bool:
return int(first["registration_order"]) < int(second["registration_order"])