280 lines
9.2 KiB
GDScript
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"])
|