Files
TheTree_mock/scripts/world/path_service.gd
T

2610 lines
92 KiB
GDScript

class_name PathService
extends RefCounted
signal path_query_measured(duration_us: int)
## Deterministic path graph derived from WorldService-owned passable cells.
##
## Coordinates are Vector3i(x, y, layer), where layer is FRONT (0) or BACK (1).
## This service owns connector configuration, but not the authoritative world cells.
const FRONT := 0
const BACK := 1
const ORTHOGONAL_MOVE_COST := 100
const DEFAULT_CONNECTOR_COST := 150
const DEFAULT_CELL_CAPACITY := 4
const CONGESTION_COST_PER_EXCESS := 75
const MINIMUM_SPEED_MULTIPLIER := 0.25
const STUCK_THRESHOLD_MINUTES := 30
const ELEVATOR_STARVATION_MINUTES := 60
const SNAPSHOT_VERSION := 5
const SHAFT_SNAPSHOT_VERSION := 4
const ELEVATOR_INDEX_SNAPSHOT_VERSION := 3
const MAX_VERTICAL_BLOCKER_OWNERS := 4_096
const MAX_VERTICAL_BLOCKER_CELLS := 262_144
const LEGACY_SNAPSHOT_VERSION := 1
const TRAFFIC_SNAPSHOT_VERSION := 2
const ELEVATOR_REQUEST_QUEUED := &"QUEUED"
const ELEVATOR_REQUEST_BOARDED := &"BOARDED"
const MOVEMENT_WALK := &"WALK"
const MOVEMENT_CONNECTOR := &"CONNECTOR"
const MOVEMENT_ELEVATOR := &"ELEVATOR"
const CONNECTOR_OPERATIONAL := &"OPERATIONAL"
const CONNECTOR_STOPPED := &"STOPPED"
const CONNECTOR_OUTAGE := &"OUTAGE"
const CONNECTOR_MAINTENANCE := &"MAINTENANCE"
const CABIN_IDLE := &"IDLE"
const CABIN_MOVING_UP := &"MOVING_UP"
const CABIN_MOVING_DOWN := &"MOVING_DOWN"
const CABIN_DOOR_OPEN := &"DOOR_OPEN"
const CABIN_OUTAGE := &"OUTAGE"
const ELEVATOR_PRIORITY_RESCUE := 0
const ELEVATOR_PRIORITY_SECURITY := 10
const ELEVATOR_PRIORITY_PROTAGONIST := 20
const ELEVATOR_PRIORITY_VIP := 30
const ELEVATOR_PRIORITY_NORMAL := 40
const ROUTE_OK := &"OK"
const ROUTE_START_NOT_PASSABLE := &"START_NOT_PASSABLE"
const ROUTE_GOAL_NOT_PASSABLE := &"GOAL_NOT_PASSABLE"
const ROUTE_START_BLOCKED := &"START_TEMPORARILY_BLOCKED"
const ROUTE_GOAL_BLOCKED := &"GOAL_TEMPORARILY_BLOCKED"
const ROUTE_ACCESS_LAYER_DENIED := &"ACCESS_LAYER_DENIED"
const ROUTE_CONNECTOR_UNAVAILABLE := &"CONNECTOR_UNAVAILABLE"
const ROUTE_TEMPORARILY_BLOCKED := &"TEMPORARILY_BLOCKED"
const ROUTE_GOAL_UNREACHABLE := &"GOAL_UNREACHABLE"
const _CARDINAL_DIRECTIONS: Array[Vector3i] = [
Vector3i(-1, 0, 0),
Vector3i(0, -1, 0),
Vector3i(0, 1, 0),
Vector3i(1, 0, 0),
]
const _INFINITE_COST := 9223372036854775807
var graph_revision := 0
var traffic_revision := 0
var cache_hits := 0
var cache_misses := 0
var last_error := ""
var last_no_route_reason: StringName = ROUTE_OK
var last_path_cost := 0
var _passable_cells: Dictionary = {}
var _connectors: Dictionary = {}
var _adjacency: Dictionary = {}
var _vertical_walk_blockers: Dictionary = {}
var _vertical_walk_blocked_cells: Dictionary = {}
var _path_cache: Dictionary = {}
var _cell_traffic: Dictionary = {}
var _actor_cells: Dictionary = {}
var _actor_progress: Dictionary = {}
var _vertical_walk_requires_connector := false
var _transit_actor_ids: Dictionary = {}
var _known_elevator_request_ids: Dictionary = {}
var _active_elevator_units: Dictionary = {}
var _pending_graph_revision := -1
var _traffic_loaded_without_graph := false
func rebuild_graph(passable_cells: Variant) -> bool:
var normalization := _normalize_cell_set(passable_cells)
if not normalization[&"ok"]:
last_error = normalization[&"error"]
return false
var next_cells: Dictionary = normalization[&"cells"]
if _traffic_loaded_without_graph:
for cell_value: Variant in _cell_traffic:
if not next_cells.has(cell_value):
last_error = "Snapshot traffic references a cell absent from the authoritative world: %s" % cell_value
return false
for actor_id: Variant in _actor_cells:
if not next_cells.has(_actor_cells[actor_id]):
last_error = "Snapshot actor projection references a non-walkable cell: %s" % actor_id
return false
if next_cells == _passable_cells:
_ensure_cell_traffic_for_graph(next_cells, not _traffic_loaded_without_graph)
_traffic_loaded_without_graph = false
if _pending_graph_revision >= 0:
graph_revision = _pending_graph_revision
_pending_graph_revision = -1
invalidate_all_cache()
last_error = ""
return true
_passable_cells = next_cells
_ensure_cell_traffic_for_graph(next_cells, not _traffic_loaded_without_graph)
_traffic_loaded_without_graph = false
_rebuild_derived_graph()
if _pending_graph_revision >= 0:
graph_revision = _pending_graph_revision
_pending_graph_revision = -1
invalidate_all_cache()
else:
_mark_graph_changed()
last_error = ""
return true
## A shaft is always physical topology, independent of cabin availability.
## Owners may overlap; removing one owner does not clear another owner's cells.
func set_vertical_walk_blockers(owner_id: StringName, cells: Array[Vector3i]) -> bool:
if not _vertical_blocker_owner_is_valid(owner_id):
last_error = "Vertical walk blockers require a bounded, nonempty owner id."
return false
if cells.is_empty() or cells.size() > MAX_VERTICAL_BLOCKER_CELLS:
last_error = "Vertical walk blockers require a bounded, nonempty cell set."
return false
var normalization := _normalize_cell_set(cells)
if not bool(normalization[&"ok"]) or (normalization[&"cells"] as Dictionary).size() != cells.size():
last_error = "Vertical walk blocker cells must be valid and unique."
return false
for cell: Vector3i in cells:
if not _passable_cells.has(cell):
last_error = "Vertical walk blocker cells must belong to the current graph."
return false
var canonical: Array[Vector3i] = cells.duplicate()
canonical.sort_custom(Callable(self, "_coordinate_less"))
if _vertical_walk_blockers.get(owner_id, []) == canonical:
last_error = ""
return true
var candidate := _vertical_walk_blockers.duplicate(true)
candidate[owner_id] = canonical
var count := 0
for owned_cells: Array in candidate.values():
count += owned_cells.size()
if candidate.size() > MAX_VERTICAL_BLOCKER_OWNERS or count > MAX_VERTICAL_BLOCKER_CELLS:
last_error = "Vertical walk blocker ownership capacity exceeded."
return false
_vertical_walk_blockers = candidate
_rebuild_vertical_walk_blocked_cells()
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func remove_vertical_walk_blockers(owner_id: StringName) -> bool:
if not _vertical_walk_blockers.has(owner_id):
last_error = "Unknown vertical walk blocker owner."
return false
_vertical_walk_blockers.erase(owner_id)
_rebuild_vertical_walk_blocked_cells()
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func get_vertical_walk_blockers() -> Dictionary:
return _vertical_walk_blockers.duplicate(true)
func set_vertical_walk_requires_connector(required: bool) -> bool:
if _vertical_walk_requires_connector == required:
last_error = ""
return true
_vertical_walk_requires_connector = required
_mark_graph_changed()
last_error = ""
return true
func get_vertical_walk_requires_connector() -> bool:
return _vertical_walk_requires_connector
func get_transit_actor_ids() -> Array[StringName]:
return _sorted_name_keys(_transit_actor_ids)
func is_actor_in_transit(actor_id: StringName) -> bool:
return _transit_actor_ids.has(actor_id)
func _rebuild_vertical_walk_blocked_cells() -> void:
_vertical_walk_blocked_cells.clear()
for cells: Array in _vertical_walk_blockers.values():
for cell: Vector3i in cells:
_vertical_walk_blocked_cells[cell] = true
func _ordinary_walk_edge_allowed(from_cell: Vector3i, to_cell: Vector3i) -> bool:
return from_cell.y == to_cell.y or (
not _vertical_walk_blocked_cells.has(from_cell)
and not _vertical_walk_blocked_cells.has(to_cell)
)
func _vertical_blocker_owner_is_valid(owner_id: StringName) -> bool:
var value := String(owner_id)
return not value.is_empty() and value.length() <= 128 and value == value.strip_edges()
func configure_connector(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
cost := DEFAULT_CONNECTOR_COST,
bidirectional := true,
access_id: StringName = &"",
enabled := true,
options: Dictionary = {}
) -> bool:
var raw_configuration := options.duplicate(true)
raw_configuration.merge({
&"id": connector_id,
&"from": from_cell,
&"to": to_cell,
&"cost": cost,
&"bidirectional": bidirectional,
&"access_id": access_id,
&"enabled": enabled,
}, true)
var normalized := _normalize_connector(raw_configuration)
if not normalized[&"ok"]:
last_error = normalized[&"error"]
return false
var configuration: Dictionary = normalized[&"connector"]
if _connectors.has(connector_id) and _connectors[connector_id] == configuration:
last_error = ""
return true
for unit_id: Variant in _active_elevator_units:
var active: Dictionary = _active_elevator_units[unit_id]
if active[&"connector_id"] == connector_id:
last_error = "Cannot reconfigure a connector with active elevator requests."
return false
var proposed_connectors := _connectors.duplicate(true)
proposed_connectors[connector_id] = configuration
var index_result := _derive_elevator_indexes_from_connectors(proposed_connectors)
if not index_result[&"ok"]:
last_error = index_result[&"error"]
return false
for request_id: Variant in index_result[&"known_request_ids"]:
if _known_elevator_request_ids.has(request_id) \
and not _active_request_id_exists(StringName(request_id)):
last_error = "Connector configuration reuses a retired elevator request id."
return false
_connectors[connector_id] = configuration
var next_active: Dictionary = index_result[&"active_units"]
for unit_id: Variant in _active_elevator_units:
var active: Dictionary = _active_elevator_units[unit_id]
if active[&"state"] == ELEVATOR_REQUEST_BOARDED:
next_active[unit_id] = active.duplicate(true)
_active_elevator_units = next_active
_refresh_transit_actor_ids()
for request_id: Variant in index_result[&"known_request_ids"]:
_known_elevator_request_ids[request_id] = true
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func add_connector(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
cost := DEFAULT_CONNECTOR_COST,
bidirectional := true,
access_id: StringName = &""
) -> bool:
return configure_connector(
connector_id,
from_cell,
to_cell,
cost,
bidirectional,
access_id,
true
)
func configure_elevator(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
options: Dictionary = {},
access_id: StringName = &""
) -> bool:
var elevator_options := options.duplicate(true)
elevator_options[&"movement_type"] = MOVEMENT_ELEVATOR
var cost := int(_dictionary_value(elevator_options, &"cost", DEFAULT_CONNECTOR_COST))
var bidirectional := bool(_dictionary_value(elevator_options, &"bidirectional", true))
var enabled := bool(_dictionary_value(elevator_options, &"enabled", true))
return configure_connector(
connector_id,
from_cell,
to_cell,
cost,
bidirectional,
access_id,
enabled,
elevator_options
)
func set_connectors(configurations: Array) -> bool:
var normalized_collection := _normalize_connector_collection(configurations)
if not normalized_collection[&"ok"]:
last_error = normalized_collection[&"error"]
return false
var next_connectors: Dictionary = normalized_collection[&"connectors"]
if next_connectors == _connectors:
last_error = ""
return true
for request_id: Variant in normalized_collection[&"known_request_ids"]:
if _known_elevator_request_ids.has(request_id) \
and not _active_request_id_exists(StringName(request_id)):
last_error = "Connector set reuses a retired elevator request id."
return false
for unit_id: Variant in _active_elevator_units:
if (_active_elevator_units[unit_id] as Dictionary)[&"state"] == ELEVATOR_REQUEST_BOARDED:
last_error = "Cannot replace connector set while an elevator trip is boarded."
return false
_connectors = next_connectors
_active_elevator_units = normalized_collection[&"active_units"]
_refresh_transit_actor_ids()
for request_id: Variant in normalized_collection[&"known_request_ids"]:
_known_elevator_request_ids[request_id] = true
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func remove_connector(connector_id: StringName) -> bool:
if not _connectors.has(connector_id):
return false
for unit_id: Variant in _active_elevator_units:
if (_active_elevator_units[unit_id] as Dictionary)[&"connector_id"] == connector_id:
last_error = "Cannot remove a connector with active elevator requests: %s" % connector_id
return false
_connectors.erase(connector_id)
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func set_connector_enabled(connector_id: StringName, enabled: bool) -> bool:
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return false
var connector: Dictionary = _connectors[connector_id]
if bool(connector[&"enabled"]) == enabled:
last_error = ""
return true
connector[&"enabled"] = enabled
_connectors[connector_id] = connector
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func set_connector_operational_state(connector_id: StringName, state: StringName) -> bool:
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return false
if state not in [
CONNECTOR_OPERATIONAL,
CONNECTOR_STOPPED,
CONNECTOR_OUTAGE,
CONNECTOR_MAINTENANCE,
]:
last_error = "Unsupported connector operational state: %s" % state
return false
var connector: Dictionary = _connectors[connector_id]
if connector[&"operational_state"] == state:
last_error = ""
return true
connector[&"operational_state"] = state
if connector[&"movement_type"] == MOVEMENT_ELEVATOR:
connector[&"cabin_state"] = CABIN_IDLE if state == CONNECTOR_OPERATIONAL else CABIN_OUTAGE
_connectors[connector_id] = connector
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func set_elevator_operational(connector_id: StringName, operational: bool) -> bool:
return set_connector_operational_state(
connector_id,
CONNECTOR_OPERATIONAL if operational else CONNECTOR_OUTAGE
)
func set_elevator_cabin_state(connector_id: StringName, cabin_state: StringName) -> bool:
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return false
if cabin_state not in [
CABIN_IDLE,
CABIN_MOVING_UP,
CABIN_MOVING_DOWN,
CABIN_DOOR_OPEN,
CABIN_OUTAGE,
]:
last_error = "Unsupported elevator cabin state: %s" % cabin_state
return false
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
last_error = "Connector is not an elevator: %s" % connector_id
return false
if connector[&"cabin_state"] == cabin_state:
last_error = ""
return true
connector[&"cabin_state"] = cabin_state
connector[&"operational_state"] = (
CONNECTOR_OUTAGE if cabin_state == CABIN_OUTAGE else CONNECTOR_OPERATIONAL
)
_connectors[connector_id] = connector
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func clear_connectors() -> void:
if _connectors.is_empty():
return
if not _active_elevator_units.is_empty():
last_error = "Cannot clear connectors while elevator requests are active."
return
_connectors.clear()
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
func configure_cell_traffic(
cell: Vector3i,
base_capacity: int = DEFAULT_CELL_CAPACITY,
dynamic_cost: int = 0,
temporary_block: bool = false
) -> bool:
if not _passable_cells.has(cell):
last_error = "Traffic configuration requires a passable cell: %s" % cell
return false
if base_capacity <= 0 or dynamic_cost < 0:
last_error = "Cell capacity must be positive and dynamic cost non-negative."
return false
var current: Dictionary = _cell_traffic.get(cell, _default_cell_traffic())
var updated := {
&"base_capacity": base_capacity,
&"current_density": int(current.get(&"current_density", 0)),
&"dynamic_cost": dynamic_cost,
&"temporary_block": temporary_block,
}
if current == updated:
last_error = ""
return true
var may_make_routes_cheaper := (
_traffic_effective_cost(current) > _traffic_effective_cost(updated)
or (bool(current[&"temporary_block"]) and not temporary_block)
)
_cell_traffic[cell] = updated
_mark_traffic_changed([cell], may_make_routes_cheaper)
last_error = ""
return true
func set_cell_base_capacity(cell: Vector3i, base_capacity: int) -> bool:
var current := get_cell_traffic(cell)
if current.is_empty():
last_error = "Unknown path cell: %s" % cell
return false
return configure_cell_traffic(
cell,
base_capacity,
int(current[&"dynamic_cost"]),
bool(current[&"temporary_block"])
)
func set_cell_dynamic_cost(cell: Vector3i, dynamic_cost: int) -> bool:
var current := get_cell_traffic(cell)
if current.is_empty():
last_error = "Unknown path cell: %s" % cell
return false
return configure_cell_traffic(
cell,
int(current[&"base_capacity"]),
dynamic_cost,
bool(current[&"temporary_block"])
)
func set_cell_temporary_block(cell: Vector3i, temporary_block: bool) -> bool:
var current := get_cell_traffic(cell)
if current.is_empty():
last_error = "Unknown path cell: %s" % cell
return false
return configure_cell_traffic(
cell,
int(current[&"base_capacity"]),
int(current[&"dynamic_cost"]),
temporary_block
)
func get_cell_traffic(cell: Vector3i) -> Dictionary:
if not _passable_cells.has(cell):
return {}
return (_cell_traffic.get(cell, _default_cell_traffic()) as Dictionary).duplicate(true)
func get_current_density(cell: Vector3i) -> int:
return int(_cell_traffic.get(cell, {}).get(&"current_density", 0))
func get_congestion_cost(cell: Vector3i) -> int:
var traffic: Dictionary = _cell_traffic.get(cell, {})
if traffic.is_empty():
return 0
return maxi(
0,
int(traffic[&"current_density"]) - int(traffic[&"base_capacity"])
) * CONGESTION_COST_PER_EXCESS
func get_cell_dynamic_cost(cell: Vector3i) -> int:
var traffic: Dictionary = _cell_traffic.get(cell, {})
if traffic.is_empty():
return 0
return int(traffic[&"dynamic_cost"]) + get_congestion_cost(cell)
func get_speed_multiplier(cell: Vector3i) -> float:
var traffic: Dictionary = _cell_traffic.get(cell, {})
if traffic.is_empty():
return 1.0
var capacity := int(traffic[&"base_capacity"])
var density := int(traffic[&"current_density"])
return clampf(
float(capacity) / float(maxi(capacity, density)),
MINIMUM_SPEED_MULTIPLIER,
1.0
)
func project_actor_position(
actor_id: StringName,
cell: Vector3i,
absolute_minute: int = -1
) -> bool:
if actor_id.is_empty() or not _passable_cells.has(cell):
last_error = "Actor projection requires an id and a passable destination."
return false
if bool(_cell_traffic[cell][&"temporary_block"]):
last_error = "Actor cannot enter a temporarily blocked cell."
return false
if absolute_minute >= 0 and _actor_progress.has(actor_id) \
and absolute_minute < int((_actor_progress[actor_id] as Dictionary)[&"last_observed_at"]):
last_error = "Actor projection time cannot move backwards."
return false
if _actor_cells.has(actor_id) and _actor_cells[actor_id] == cell:
if absolute_minute >= 0:
var progress_result := record_actor_progress(actor_id, cell, absolute_minute)
if not bool(progress_result[&"ok"]):
return false
last_error = ""
return true
var changed_cells: Array[Vector3i] = []
if _actor_cells.has(actor_id):
var previous: Vector3i = _actor_cells[actor_id]
if not is_actor_in_transit(actor_id) and not _decrement_density(previous):
last_error = "Actor density projection is inconsistent for: %s" % actor_id
return false
if not is_actor_in_transit(actor_id):
changed_cells.append(previous)
_actor_cells[actor_id] = cell
if _active_elevator_units.get(actor_id, {}).get(&"state", &"") == ELEVATOR_REQUEST_BOARDED:
_transit_actor_ids[actor_id] = true
else:
_transit_actor_ids.erase(actor_id)
_increment_density(cell)
changed_cells.append(cell)
# Leaving the previous cell can make an unselected alternative cheaper, so a
# move requires broad cache invalidation even though congestion itself does
# not advance graph_revision.
if not changed_cells.is_empty():
_mark_traffic_changed(changed_cells, changed_cells.size() > 1)
if absolute_minute >= 0:
record_actor_progress(actor_id, cell, absolute_minute)
last_error = ""
return true
func actor_enter_cell(actor_id: StringName, cell: Vector3i, absolute_minute: int = -1) -> bool:
return project_actor_position(actor_id, cell, absolute_minute)
func actor_leave_cell(actor_id: StringName, clear_progress: bool = true) -> bool:
if actor_id.is_empty() or not _actor_cells.has(actor_id):
last_error = "Actor has no projected cell: %s" % actor_id
return false
var previous: Vector3i = _actor_cells[actor_id]
var was_in_transit := is_actor_in_transit(actor_id)
if not was_in_transit and not _decrement_density(previous):
last_error = "Actor density projection is inconsistent for: %s" % actor_id
return false
_actor_cells.erase(actor_id)
_transit_actor_ids.erase(actor_id)
if clear_progress:
_actor_progress.erase(actor_id)
if not was_in_transit:
_mark_traffic_changed([previous], true)
last_error = ""
return true
func despawn_actor(actor_id: StringName) -> bool:
var removed := false
if _actor_cells.has(actor_id):
removed = actor_leave_cell(actor_id, true)
elif _actor_progress.erase(actor_id):
removed = true
if not removed:
last_error = "Unknown actor id: %s" % actor_id
return false
last_error = ""
return true
func has_actor_projection(actor_id: StringName) -> bool:
return _actor_cells.has(actor_id)
func get_actor_projected_cell(actor_id: StringName) -> Variant:
return _actor_cells.get(actor_id, null)
func project_occupancy(entries: Array, clear_unprojected_progress: bool = true) -> bool:
var next_actor_cells: Dictionary = {}
var next_densities: Dictionary = {}
for raw_entry: Variant in entries:
if not raw_entry is Dictionary:
last_error = "Occupancy projection entries must be Dictionaries."
return false
var actor_id := StringName(str(_dictionary_value(raw_entry, &"actor_id", "")))
var cell: Variant = _dictionary_value(raw_entry, &"cell", null)
if actor_id.is_empty() or not cell is Vector3i or not _passable_cells.has(cell):
last_error = "Occupancy projection contains an invalid actor or cell."
return false
if bool(_cell_traffic[cell][&"temporary_block"]) or next_actor_cells.has(actor_id):
last_error = "Occupancy projection contains a duplicate actor or blocked cell."
return false
next_actor_cells[actor_id] = cell
if _active_elevator_units.get(actor_id, {}).get(&"state", &"") != ELEVATOR_REQUEST_BOARDED:
next_densities[cell] = int(next_densities.get(cell, 0)) + 1
var changed: Dictionary = {}
var may_make_routes_cheaper := false
for cell_value: Variant in _passable_cells:
var cell: Vector3i = cell_value
var old_density := get_current_density(cell)
var next_density := int(next_densities.get(cell, 0))
if old_density != next_density:
changed[cell] = true
if next_density < old_density:
may_make_routes_cheaper = true
var traffic: Dictionary = _cell_traffic[cell]
traffic[&"current_density"] = next_density
_cell_traffic[cell] = traffic
_actor_cells = next_actor_cells
_transit_actor_ids = _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
if clear_unprojected_progress:
var stale_progress_ids: Array = []
for actor_id: Variant in _actor_progress:
if not next_actor_cells.has(actor_id):
stale_progress_ids.append(actor_id)
for actor_id: Variant in stale_progress_ids:
_actor_progress.erase(actor_id)
if not changed.is_empty():
_mark_traffic_changed(changed.keys(), may_make_routes_cheaper)
last_error = ""
return true
func enqueue_elevator_request(connector_id: StringName, request: Dictionary) -> Dictionary:
if not _connectors.has(connector_id):
return _queue_failure(&"UNKNOWN_CONNECTOR", "Unknown connector id: %s" % connector_id)
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
return _queue_failure(&"NOT_ELEVATOR", "Connector is not an elevator: %s" % connector_id)
var normalized := _normalize_elevator_request(request)
if not normalized[&"ok"]:
return _queue_failure(&"INVALID_REQUEST", normalized[&"error"])
var candidate: Dictionary = normalized[&"request"]
var queue: Array = connector[&"request_queue"]
var request_id: StringName = candidate[&"request_id"]
var unit_id: StringName = candidate[&"unit_id"]
if _known_elevator_request_ids.has(request_id):
var active: Dictionary = _active_elevator_units.get(unit_id, {})
if not active.is_empty() \
and active[&"request_id"] == request_id \
and active[&"connector_id"] == connector_id \
and active[&"state"] == ELEVATOR_REQUEST_QUEUED:
for existing_value: Variant in queue:
var existing: Dictionary = existing_value
if existing[&"request_id"] == request_id and existing == candidate:
last_error = ""
return {
&"ok": true,
&"replay": true,
&"request": existing.duplicate(true),
&"queue_size": queue.size(),
}
return _queue_failure(
&"DUPLICATE_REQUEST",
"Elevator request id has already been used on this or another connector."
)
if _active_elevator_units.has(unit_id):
var existing_active: Dictionary = _active_elevator_units[unit_id]
return _queue_failure(
&"UNIT_ALREADY_ACTIVE",
"Elevator unit already has an active %s request." % existing_active[&"state"]
)
if queue.size() >= int(connector[&"queue_capacity"]):
return _queue_failure(&"QUEUE_FULL", "Elevator request queue is full.")
queue.append(candidate)
queue.sort_custom(Callable(self, "_elevator_request_base_less"))
connector[&"request_queue"] = queue
_connectors[connector_id] = connector
_known_elevator_request_ids[request_id] = true
_active_elevator_units[unit_id] = {
&"unit_id": unit_id,
&"request_id": request_id,
&"connector_id": connector_id,
&"state": ELEVATOR_REQUEST_QUEUED,
}
_mark_connector_traffic_changed(connector, false)
last_error = ""
return {
&"ok": true,
&"replay": false,
&"request": candidate.duplicate(true),
&"queue_size": queue.size(),
}
func queue_elevator_request(connector_id: StringName, request: Dictionary) -> Dictionary:
return enqueue_elevator_request(connector_id, request)
func cancel_elevator_request(connector_id: StringName, request_id: StringName) -> bool:
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return false
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
last_error = "Connector is not an elevator: %s" % connector_id
return false
var queue: Array = connector[&"request_queue"]
for index: int in range(queue.size()):
var request: Dictionary = queue[index]
if request[&"request_id"] == request_id:
queue.remove_at(index)
connector[&"request_queue"] = queue
_connectors[connector_id] = connector
_active_elevator_units.erase(request[&"unit_id"])
_refresh_transit_actor_ids()
# Request ids are tombstoned permanently; cancellation permits a new
# id for the same unit but never reuse of the cancelled id.
_mark_connector_traffic_changed(connector, true)
last_error = ""
return true
last_error = "Unknown elevator request id: %s" % request_id
return false
func get_elevator_queue(connector_id: StringName, absolute_minute: int = 0) -> Array[Dictionary]:
var result: Array[Dictionary] = []
if not _connectors.has(connector_id):
return result
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
return result
var queue: Array = (connector[&"request_queue"] as Array).duplicate(true)
queue.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
return _elevator_request_less_at(left, right, absolute_minute)
)
for value: Variant in queue:
result.append((value as Dictionary).duplicate(true))
return result
func elevator_queue_size(connector_id: StringName) -> int:
if not _connectors.has(connector_id):
return 0
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
return 0
return (connector[&"request_queue"] as Array).size()
func dequeue_elevator_boarding(
connector_id: StringName,
current_floor: int,
absolute_minute: int,
available_capacity: int = -1
) -> Array[Dictionary]:
var boarded: Array[Dictionary] = []
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return boarded
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
last_error = "Connector is not an elevator: %s" % connector_id
return boarded
if not _connector_is_operational(connector):
last_error = "Elevator is not operational: %s" % connector_id
return boarded
var boarding_capacity := int(connector[&"cabin_capacity"])
if available_capacity >= 0:
boarding_capacity = mini(boarding_capacity, available_capacity)
if boarding_capacity <= 0:
last_error = ""
return boarded
var queue := get_elevator_queue(connector_id, absolute_minute)
var remaining: Array = []
for request: Dictionary in queue:
if boarded.size() < boarding_capacity and int(request[&"floor_from"]) == current_floor:
boarded.append(request.duplicate(true))
else:
remaining.append(request.duplicate(true))
connector[&"request_queue"] = remaining
connector[&"current_floor"] = current_floor
connector[&"cabin_state"] = CABIN_DOOR_OPEN if not boarded.is_empty() else CABIN_IDLE
_connectors[connector_id] = connector
if not boarded.is_empty():
for request: Dictionary in boarded:
var unit_id: StringName = request[&"unit_id"]
var active: Dictionary = _active_elevator_units.get(unit_id, {})
if not active.is_empty() and active[&"request_id"] == request[&"request_id"]:
active[&"state"] = ELEVATOR_REQUEST_BOARDED
_active_elevator_units[unit_id] = active
_refresh_transit_actor_ids()
_mark_connector_traffic_changed(connector, true)
last_error = ""
return boarded
func complete_elevator_request(connector_id: StringName, request_id: StringName) -> bool:
if connector_id.is_empty() or request_id.is_empty():
last_error = "Elevator completion requires connector and request ids."
return false
for unit_id: StringName in _sorted_name_keys(_active_elevator_units):
var active: Dictionary = _active_elevator_units[unit_id]
if active[&"request_id"] != request_id:
continue
if active[&"connector_id"] != connector_id:
last_error = "Elevator request belongs to another connector."
return false
if active[&"state"] != ELEVATOR_REQUEST_BOARDED:
last_error = "Only a boarded elevator request can complete."
return false
_active_elevator_units.erase(unit_id)
_refresh_transit_actor_ids()
last_error = ""
return true
last_error = "Unknown active elevator request id: %s" % request_id
return false
func complete_elevator_trip(connector_id: StringName, request_id: StringName) -> bool:
return complete_elevator_request(connector_id, request_id)
func get_active_elevator_request(unit_id: StringName) -> Dictionary:
if not _active_elevator_units.has(unit_id):
return {}
return (_active_elevator_units[unit_id] as Dictionary).duplicate(true)
func _active_request_id_exists(request_id: StringName) -> bool:
for unit_id: Variant in _active_elevator_units:
if (_active_elevator_units[unit_id] as Dictionary)[&"request_id"] == request_id:
return true
return false
func estimate_elevator_wait_cost(connector_id: StringName) -> int:
if not _connectors.has(connector_id):
return 0
var connector: Dictionary = _connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
return 0
var queue_size := (connector[&"request_queue"] as Array).size()
var capacity := maxi(int(connector[&"cabin_capacity"]), 1)
# Boarding costs one minute per unit. Each full cabin ahead adds another
# door cycle, represented here as two game minutes.
var cabin_cycles := ceili(float(queue_size) / float(capacity))
return (queue_size + cabin_cycles * 2) * ORTHOGONAL_MOVE_COST
func find_path(
start: Vector3i,
goal: Vector3i,
access_layers: Variant = null,
connector_access: Variant = null
) -> Array[Vector3i]:
var started_usec := Time.get_ticks_usec()
var path := _find_path_internal(start, goal, access_layers, connector_access)
path_query_measured.emit(maxi(Time.get_ticks_usec() - started_usec, 0))
return path
func _find_path_internal(
start: Vector3i,
goal: Vector3i,
access_layers: Variant = null,
connector_access: Variant = null
) -> Array[Vector3i]:
var raw_layers: Variant = access_layers
var raw_connector_access: Variant = connector_access
if access_layers is Dictionary:
var access_profile: Dictionary = access_layers
raw_layers = _profile_value(
access_profile,
[&"allowed_layers", &"access_layers", &"access_layer", &"layer"],
null
)
if connector_access == null:
raw_connector_access = _profile_value(
access_profile,
[&"connector_access", &"connector_access_ids", &"access_ids"],
null
)
var layer_result := _normalize_access_layers(raw_layers)
if not layer_result[&"ok"]:
last_error = layer_result[&"error"]
last_no_route_reason = ROUTE_ACCESS_LAYER_DENIED
last_path_cost = 0
return []
var connector_result := _normalize_access_ids(raw_connector_access)
if not connector_result[&"ok"]:
last_error = connector_result[&"error"]
last_no_route_reason = ROUTE_CONNECTOR_UNAVAILABLE
last_path_cost = 0
return []
var allowed_layers: Array[int] = layer_result[&"values"]
var allowed_access_ids: Array[StringName] = connector_result[&"values"]
if not _passable_cells.has(start):
return _route_failure(ROUTE_START_NOT_PASSABLE)
if not _passable_cells.has(goal):
return _route_failure(ROUTE_GOAL_NOT_PASSABLE)
if start.z not in allowed_layers or goal.z not in allowed_layers:
return _route_failure(ROUTE_ACCESS_LAYER_DENIED)
if _cell_is_temporarily_blocked(start):
return _route_failure(ROUTE_START_BLOCKED)
if _cell_is_temporarily_blocked(goal):
return _route_failure(ROUTE_GOAL_BLOCKED)
var cache_key := _cache_key(start, goal, allowed_layers, allowed_access_ids)
if _path_cache.has(cache_key):
var cached: Array = _path_cache[cache_key]
if _path_is_currently_valid(cached, allowed_layers, allowed_access_ids):
cache_hits += 1
last_no_route_reason = ROUTE_OK
last_path_cost = _calculate_path_cost_for_access(cached, allowed_access_ids)
last_error = ""
return _copy_path(cached)
_path_cache.erase(cache_key)
cache_misses += 1
var path := _run_a_star(start, goal, allowed_layers, allowed_access_ids)
if path.is_empty():
return _route_failure(_diagnose_no_route_reason(
start,
goal,
allowed_layers,
allowed_access_ids
))
_path_cache[cache_key] = _copy_path(path)
last_no_route_reason = ROUTE_OK
last_path_cost = _calculate_path_cost_for_access(path, allowed_access_ids)
last_error = ""
return path
func find_path_report(
start: Vector3i,
goal: Vector3i,
access_layers: Variant = null,
connector_access: Variant = null
) -> Dictionary:
var path := find_path(start, goal, access_layers, connector_access)
return {
&"reachable": not path.is_empty(),
&"reason": last_no_route_reason,
&"cost": last_path_cost,
&"path": path,
&"graph_revision": graph_revision,
&"traffic_revision": traffic_revision,
}
func calculate_path_cost(path: Array, connector_access: Variant = null) -> int:
var access_result := _normalize_access_ids(connector_access)
if not access_result[&"ok"]:
last_error = access_result[&"error"]
return -1
var total := _calculate_path_cost_for_access(path, access_result[&"values"])
last_error = "" if total >= 0 else "Path contains an unavailable edge."
return total
func _calculate_path_cost_for_access(
path: Array,
allowed_access_ids: Array[StringName]
) -> int:
if path.is_empty():
return 0
var total := 0
for index: int in range(1, path.size()):
var from_cell: Variant = path[index - 1]
var to_cell: Variant = path[index]
if not from_cell is Vector3i or not to_cell is Vector3i:
return -1
var edge := _find_edge(from_cell, to_cell, allowed_access_ids)
if edge.is_empty():
return -1
total += _edge_total_cost(edge)
return total
func invalidate_cache_for_cells(cells: Variant) -> int:
var normalization := _normalize_cell_set(cells, false)
if not normalization[&"ok"]:
last_error = normalization[&"error"]
return 0
var affected: Dictionary = normalization[&"cells"]
var keys_to_remove: Array = []
for cache_key: Variant in _path_cache:
var cached_path: Array = _path_cache[cache_key]
if cached_path.is_empty() or _path_touches(cached_path, affected):
keys_to_remove.append(cache_key)
for cache_key: Variant in keys_to_remove:
_path_cache.erase(cache_key)
last_error = ""
return keys_to_remove.size()
func invalidate_local_cache(cells: Variant) -> int:
return invalidate_cache_for_cells(cells)
func invalidate_all_cache() -> int:
var removed := _path_cache.size()
_path_cache.clear()
return removed
func cached_path_count() -> int:
return _path_cache.size()
func has_cell(cell: Vector3i) -> bool:
return _passable_cells.has(cell)
func has_connector(connector_id: StringName) -> bool:
return _connectors.has(connector_id)
func get_connector(connector_id: StringName) -> Dictionary:
if not _connectors.has(connector_id):
return {}
return (_connectors[connector_id] as Dictionary).duplicate(true)
## Resolves the same cheapest accessible edge that find_path/calculate_path_cost
## use, so a movement driver can distinguish riding from an ordinary step.
func get_edge_report(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> Dictionary:
var normalized := _normalize_access_ids(connector_access)
if not bool(normalized[&"ok"]):
last_error = String(normalized[&"error"])
return {}
if _cell_is_temporarily_blocked(from_cell) or _cell_is_temporarily_blocked(to_cell):
return {}
var edge := _find_edge(from_cell, to_cell, normalized[&"values"])
if edge.is_empty():
return {}
var report := edge.duplicate(true)
report[&"from"] = from_cell
report[&"total_cost"] = _edge_total_cost(edge)
return report
func get_step_connector(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> StringName:
return StringName(get_edge_report(from_cell, to_cell, connector_access).get(&"connector_id", &""))
func get_neighbors(cell: Vector3i) -> Array:
if not _adjacency.has(cell):
return []
return (_adjacency[cell] as Array).duplicate(true)
func record_actor_progress(actor_id: StringName, cell: Vector3i, absolute_minute: int) -> Dictionary:
if actor_id.is_empty() or absolute_minute < 0:
last_error = "Progress tracking requires an actor id and non-negative minute."
return {&"ok": false, &"reason": &"INVALID_PROGRESS"}
if _actor_progress.has(actor_id):
var existing: Dictionary = _actor_progress[actor_id]
if absolute_minute < int(existing[&"last_observed_at"]):
last_error = "Actor progress time cannot move backwards."
return {&"ok": false, &"reason": &"TIME_REVERSED"}
var advanced: bool = existing[&"last_cell"] != cell
existing[&"last_cell"] = cell
existing[&"last_observed_at"] = absolute_minute
if advanced:
existing[&"last_progress_at"] = absolute_minute
existing[&"stuck_reported_at"] = -1
_actor_progress[actor_id] = existing
last_error = ""
return {
&"ok": true,
&"advanced": advanced,
&"minutes_without_progress": absolute_minute - int(existing[&"last_progress_at"]),
}
_actor_progress[actor_id] = {
&"actor_id": actor_id,
&"last_cell": cell,
&"last_progress_at": absolute_minute,
&"last_observed_at": absolute_minute,
&"stuck_reported_at": -1,
}
last_error = ""
return {&"ok": true, &"advanced": true, &"minutes_without_progress": 0}
func get_actor_stuck_report(
actor_id: StringName,
absolute_minute: int,
allowed_layers: Variant = null
) -> Dictionary:
if not _actor_progress.has(actor_id):
last_error = "Unknown actor progress id: %s" % actor_id
return {
&"ok": false,
&"actor_id": actor_id,
&"reason": &"PROGRESS_NOT_TRACKED",
&"should_recover": false,
}
var progress: Dictionary = _actor_progress[actor_id]
if absolute_minute < int(progress[&"last_observed_at"]):
last_error = "Stuck evaluation time cannot move backwards."
return {
&"ok": false,
&"actor_id": actor_id,
&"reason": &"TIME_REVERSED",
&"should_recover": false,
}
var minutes_without_progress := absolute_minute - int(progress[&"last_progress_at"])
var should_recover := minutes_without_progress >= STUCK_THRESHOLD_MINUTES
var recovery := {
&"found": false,
&"reason": &"NOT_STUCK",
}
if should_recover:
recovery = find_nearest_walkable_recovery(progress[&"last_cell"], allowed_layers)
if int(progress[&"stuck_reported_at"]) < 0:
progress[&"stuck_reported_at"] = absolute_minute
progress[&"last_observed_at"] = absolute_minute
_actor_progress[actor_id] = progress
last_error = ""
return {
&"ok": true,
&"actor_id": actor_id,
&"reason": &"STUCK" if should_recover else &"PROGRESS_RECENT",
&"should_recover": should_recover,
&"threshold_minutes": STUCK_THRESHOLD_MINUTES,
&"minutes_without_progress": minutes_without_progress,
&"last_cell": progress[&"last_cell"],
&"recovery": recovery,
&"requires_explicit_move": should_recover,
}
func check_actor_stuck(
actor_id: StringName,
absolute_minute: int,
allowed_layers: Variant = null
) -> Dictionary:
return get_actor_stuck_report(actor_id, absolute_minute, allowed_layers)
func clear_actor_progress(actor_id: StringName) -> bool:
if not _actor_progress.has(actor_id):
return false
_actor_progress.erase(actor_id)
return true
func find_nearest_walkable_recovery(
origin: Vector3i,
allowed_layers: Variant = null
) -> Dictionary:
var normalized := _normalize_access_layers(allowed_layers)
if not normalized[&"ok"]:
return {&"found": false, &"reason": &"INVALID_ACCESS_LAYERS"}
var layers: Array[int] = normalized[&"values"]
var candidates: Array[Vector3i] = []
for cell_value: Variant in _passable_cells:
var cell: Vector3i = cell_value
if cell.z in layers and not _cell_is_temporarily_blocked(cell):
candidates.append(cell)
if candidates.is_empty():
return {&"found": false, &"reason": &"NO_SAFE_WALKABLE_CELL"}
candidates.sort_custom(func(left: Vector3i, right: Vector3i) -> bool:
var left_distance := _recovery_distance(origin, left)
var right_distance := _recovery_distance(origin, right)
if left_distance != right_distance:
return left_distance < right_distance
return _coordinate_less(left, right)
)
var target: Vector3i = candidates[0]
if _vertical_walk_requires_connector:
var reachable := false
for candidate: Vector3i in candidates:
if _route_exists_with_overrides(origin, candidate, layers, [], true, false):
target = candidate
reachable = true
break
if not reachable:
return {&"found": false, &"reason": &"NO_SAFE_WALKABLE_CELL"}
return {
&"found": true,
&"reason": &"SAFE_WALKABLE_CELL",
&"cell": target,
&"distance": _recovery_distance(origin, target),
&"implicit_move_applied": false,
}
func create_snapshot() -> Dictionary:
var connector_ids: Array = _connectors.keys()
connector_ids.sort_custom(Callable(self, "_string_name_less"))
var saved_connectors: Array = []
for connector_id: Variant in connector_ids:
var connector: Dictionary = (_connectors[connector_id] as Dictionary).duplicate(true)
var queue: Array = connector[&"request_queue"]
queue.sort_custom(Callable(self, "_elevator_request_base_less"))
connector[&"request_queue"] = queue
saved_connectors.append(connector)
var cells: Array = _passable_cells.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
var saved_cell_traffic: Array = []
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
var traffic: Dictionary = _cell_traffic.get(cell, _default_cell_traffic())
saved_cell_traffic.append({
&"cell": cell,
&"base_capacity": int(traffic[&"base_capacity"]),
&"current_density": int(traffic[&"current_density"]),
&"dynamic_cost": int(traffic[&"dynamic_cost"]),
&"temporary_block": bool(traffic[&"temporary_block"]),
})
var actor_ids: Array = _actor_cells.keys()
actor_ids.sort_custom(Callable(self, "_string_name_less"))
var saved_actor_cells: Array = []
for actor_id: Variant in actor_ids:
saved_actor_cells.append({
&"actor_id": StringName(actor_id),
&"cell": _actor_cells[actor_id],
})
var progress_ids: Array = _actor_progress.keys()
progress_ids.sort_custom(Callable(self, "_string_name_less"))
var saved_progress: Array = []
for actor_id: Variant in progress_ids:
saved_progress.append((_actor_progress[actor_id] as Dictionary).duplicate(true))
var known_request_ids: Array = _known_elevator_request_ids.keys()
known_request_ids.sort_custom(Callable(self, "_string_name_less"))
var saved_known_requests: Array[StringName] = []
for request_id: Variant in known_request_ids:
saved_known_requests.append(StringName(request_id))
var active_unit_ids: Array = _active_elevator_units.keys()
active_unit_ids.sort_custom(Callable(self, "_string_name_less"))
var saved_active_units: Array = []
for unit_id: Variant in active_unit_ids:
saved_active_units.append((_active_elevator_units[unit_id] as Dictionary).duplicate(true))
var saved_vertical_blockers: Array[Dictionary] = []
for owner_id: StringName in _sorted_name_keys(_vertical_walk_blockers):
saved_vertical_blockers.append({&"owner_id": owner_id, &"cells": (_vertical_walk_blockers[owner_id] as Array).duplicate()})
return {
&"schema_version": SNAPSHOT_VERSION,
&"graph_revision": graph_revision,
&"traffic_revision": traffic_revision,
&"connectors": saved_connectors,
&"cell_traffic": saved_cell_traffic,
&"actor_cells": saved_actor_cells,
&"actor_progress": saved_progress,
&"known_elevator_request_ids": saved_known_requests,
&"active_elevator_units": saved_active_units,
&"vertical_walk_blockers": saved_vertical_blockers,
&"vertical_walk_requires_connector": _vertical_walk_requires_connector,
&"transit_actor_ids": get_transit_actor_ids(),
}
func restore_snapshot(snapshot: Dictionary) -> bool:
var version := int(_dictionary_value(snapshot, &"schema_version", -1))
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, ELEVATOR_INDEX_SNAPSHOT_VERSION, SHAFT_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
last_error = "Unsupported PathService snapshot version: %d" % version
return false
if version < SHAFT_SNAPSHOT_VERSION and (snapshot.has(&"vertical_walk_blockers") or snapshot.has("vertical_walk_blockers")):
last_error = "Legacy PathService snapshots cannot contain shaft ownership fields."
return false
if version < SNAPSHOT_VERSION and (snapshot.has(&"vertical_walk_requires_connector") or snapshot.has("vertical_walk_requires_connector") \
or snapshot.has(&"transit_actor_ids") or snapshot.has("transit_actor_ids")):
last_error = "Legacy PathService snapshots cannot contain vertical policy or transit fields."
return false
var next_vertical_policy := false
if version >= SNAPSHOT_VERSION:
var policy_value: Variant = _dictionary_value(snapshot, &"vertical_walk_requires_connector", null)
if typeof(policy_value) != TYPE_BOOL:
last_error = "PathService vertical movement policy must be a bool."
return false
next_vertical_policy = bool(policy_value)
var raw_connectors: Variant = _dictionary_value(snapshot, &"connectors", null)
if not raw_connectors is Array:
last_error = "PathService snapshot connectors must be an Array."
return false
var connector_result := _normalize_connector_collection(raw_connectors)
if not connector_result[&"ok"]:
last_error = connector_result[&"error"]
return false
var next_connectors: Dictionary = connector_result[&"connectors"]
var next_known_requests: Dictionary = connector_result[&"known_request_ids"]
var next_active_units: Dictionary = connector_result[&"active_units"]
if version == LEGACY_SNAPSHOT_VERSION:
var changed := next_connectors != _connectors or not _vertical_walk_blockers.is_empty() or _vertical_walk_requires_connector
_vertical_walk_blockers.clear()
_vertical_walk_requires_connector = false
_transit_actor_ids.clear()
_rebuild_vertical_walk_blocked_cells()
_connectors = next_connectors
_cell_traffic.clear()
_actor_cells.clear()
_actor_progress.clear()
_known_elevator_request_ids = next_known_requests
_active_elevator_units = next_active_units
traffic_revision = 0
_pending_graph_revision = -1
_traffic_loaded_without_graph = false
_ensure_cell_traffic_for_graph(_passable_cells, true)
_rebuild_derived_graph()
if changed:
_mark_graph_changed()
else:
invalidate_all_cache()
last_error = ""
return true
var saved_graph_revision := int(_dictionary_value(snapshot, &"graph_revision", -1))
var saved_traffic_revision := int(_dictionary_value(snapshot, &"traffic_revision", -1))
var raw_cell_traffic: Variant = _dictionary_value(snapshot, &"cell_traffic", null)
var raw_actor_cells: Variant = _dictionary_value(snapshot, &"actor_cells", null)
var raw_progress: Variant = _dictionary_value(snapshot, &"actor_progress", null)
if saved_graph_revision < 0 or saved_traffic_revision < 0 \
or not raw_cell_traffic is Array \
or not raw_actor_cells is Array \
or not raw_progress is Array:
last_error = "PathService traffic snapshot fields are missing or invalid."
return false
var traffic_result := _normalize_snapshot_cell_traffic(raw_cell_traffic)
if not traffic_result[&"ok"]:
last_error = traffic_result[&"error"]
return false
var next_traffic: Dictionary = traffic_result[&"traffic"]
var actor_result := _normalize_snapshot_actor_cells(raw_actor_cells, next_traffic)
if not actor_result[&"ok"]:
last_error = actor_result[&"error"]
return false
var next_actor_cells: Dictionary = actor_result[&"actor_cells"]
var progress_result := _normalize_snapshot_actor_progress(raw_progress)
if not progress_result[&"ok"]:
last_error = progress_result[&"error"]
return false
var next_progress: Dictionary = progress_result[&"progress"]
if version >= ELEVATOR_INDEX_SNAPSHOT_VERSION:
var raw_known_requests: Variant = _dictionary_value(
snapshot,
&"known_elevator_request_ids",
null
)
var raw_active_units: Variant = _dictionary_value(snapshot, &"active_elevator_units", null)
if not raw_known_requests is Array or not raw_active_units is Array:
last_error = "PathService v3 elevator request indexes are missing or invalid."
return false
var index_result := _normalize_snapshot_elevator_indexes(
raw_known_requests,
raw_active_units,
next_connectors,
connector_result[&"active_units"]
)
if not index_result[&"ok"]:
last_error = index_result[&"error"]
return false
next_known_requests = index_result[&"known_request_ids"]
next_active_units = index_result[&"active_units"]
var next_vertical_blockers: Dictionary = {}
if version >= SHAFT_SNAPSHOT_VERSION:
var blockers_result := _normalize_snapshot_vertical_blockers(_dictionary_value(snapshot, &"vertical_walk_blockers", null), next_traffic)
if not bool(blockers_result[&"ok"]):
last_error = String(blockers_result[&"error"])
return false
next_vertical_blockers = blockers_result[&"blockers"]
var next_transit := _derive_transit_actor_ids(next_actor_cells, next_active_units)
if version >= SNAPSHOT_VERSION:
var transit_value: Variant = _dictionary_value(snapshot, &"transit_actor_ids", null)
if not _transit_snapshot_matches(transit_value, next_transit):
last_error = "PathService transit actors must match projected boarded elevator units."
return false
if not _snapshot_densities_match(next_traffic, next_actor_cells, next_transit if version >= SNAPSHOT_VERSION else {}):
last_error = "PathService snapshot density does not match actor projections."
return false
if not _passable_cells.is_empty():
if next_traffic.size() != _passable_cells.size():
last_error = "PathService snapshot traffic topology does not match the current graph."
return false
for cell_value: Variant in next_traffic:
if not _passable_cells.has(cell_value):
last_error = "PathService snapshot traffic references an unknown cell."
return false
_connectors = next_connectors
_vertical_walk_blockers = next_vertical_blockers
_vertical_walk_requires_connector = next_vertical_policy
_transit_actor_ids = next_transit
_rebuild_vertical_walk_blocked_cells()
_cell_traffic = next_traffic
_actor_cells = next_actor_cells
_actor_progress = next_progress
_known_elevator_request_ids = next_known_requests
_active_elevator_units = next_active_units
traffic_revision = saved_traffic_revision
# Older snapshots counted passengers at their boarding anchor. Validate that
# old rule above before a one-time migration to physical floor occupancy.
if version < SNAPSHOT_VERSION and not _transit_actor_ids.is_empty():
_recompute_all_densities()
traffic_revision += 1
_pending_graph_revision = saved_graph_revision if _passable_cells.is_empty() else -1
_traffic_loaded_without_graph = _passable_cells.is_empty() and not next_traffic.is_empty()
if not _passable_cells.is_empty():
graph_revision = saved_graph_revision
_rebuild_derived_graph()
invalidate_all_cache()
last_no_route_reason = ROUTE_OK
last_path_cost = 0
last_error = ""
return true
func _rebuild_derived_graph() -> void:
_adjacency.clear()
var cells: Array = _passable_cells.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
_adjacency[cell] = []
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
for direction in _CARDINAL_DIRECTIONS:
var neighbor := cell + direction
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(cell, neighbor):
_append_edge(
cell,
neighbor,
ORTHOGONAL_MOVE_COST,
&"",
&"",
MOVEMENT_WALK
)
var connector_ids: Array = _connectors.keys()
connector_ids.sort_custom(Callable(self, "_string_name_less"))
for connector_id_value: Variant in connector_ids:
var connector_id: StringName = connector_id_value
var connector: Dictionary = _connectors[connector_id]
if not _connector_is_operational(connector):
continue
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
if not _passable_cells.has(from_cell) or not _passable_cells.has(to_cell):
continue
var cost: int = connector[&"cost"]
var access_id: StringName = connector[&"access_id"]
var movement_type: StringName = connector[&"movement_type"]
_append_edge(from_cell, to_cell, cost, connector_id, access_id, movement_type)
if bool(connector[&"bidirectional"]):
_append_edge(to_cell, from_cell, cost, connector_id, access_id, movement_type)
for cell_value: Variant in cells:
var edges: Array = _adjacency[cell_value]
edges.sort_custom(Callable(self, "_edge_less"))
_adjacency[cell_value] = edges
func _append_edge(
from_cell: Vector3i,
to_cell: Vector3i,
cost: int,
connector_id: StringName,
access_id: StringName,
movement_type: StringName
) -> void:
var edges: Array = _adjacency[from_cell]
edges.append({
&"to": to_cell,
&"cost": cost,
&"base_cost": cost,
&"connector_id": connector_id,
&"access_id": access_id,
&"movement_type": movement_type,
})
_adjacency[from_cell] = edges
func _run_a_star(
start: Vector3i,
goal: Vector3i,
allowed_layers: Array[int],
allowed_access_ids: Array[StringName]
) -> Array[Vector3i]:
if start == goal:
return [start]
var open_set: Dictionary = {start: true}
var closed_set: Dictionary = {}
var came_from: Dictionary = {}
var g_score: Dictionary = {start: 0}
while not open_set.is_empty():
var current := _select_best_open_node(open_set, g_score, goal)
if current == goal:
return _reconstruct_path(came_from, current)
open_set.erase(current)
closed_set[current] = true
var edges: Array = _adjacency.get(current, [])
for edge_value: Variant in edges:
var edge: Dictionary = edge_value
var neighbor: Vector3i = edge[&"to"]
if neighbor.z not in allowed_layers \
or _cell_is_temporarily_blocked(neighbor) \
or not _edge_meets_vertical_policy(current, edge) \
or not _edge_is_accessible(edge, allowed_access_ids):
continue
var tentative_cost: int = int(g_score[current]) + _edge_total_cost(edge)
var known_cost: int = int(g_score.get(neighbor, _INFINITE_COST))
var better_parent := false
if tentative_cost == known_cost and came_from.has(neighbor):
better_parent = _coordinate_less(current, came_from[neighbor])
if tentative_cost > known_cost or (tentative_cost == known_cost and not better_parent):
continue
came_from[neighbor] = current
g_score[neighbor] = tentative_cost
if closed_set.has(neighbor):
closed_set.erase(neighbor)
open_set[neighbor] = true
return []
func _select_best_open_node(open_set: Dictionary, g_score: Dictionary, goal: Vector3i) -> Vector3i:
var best := Vector3i.ZERO
var has_best := false
var best_score := _INFINITE_COST
for candidate_value: Variant in open_set:
var candidate: Vector3i = candidate_value
var score: int = int(g_score[candidate]) + _heuristic(candidate, goal)
if not has_best or score < best_score:
best = candidate
best_score = score
has_best = true
elif score == best_score and _coordinate_less(candidate, best):
best = candidate
return best
func _heuristic(from_cell: Vector3i, to_cell: Vector3i) -> int:
# A connector may be cheaper than the geometric distance. In that case zero is
# the conservative admissible heuristic; without connectors Manhattan is exact.
if not _connectors.is_empty() or from_cell.z != to_cell.z:
return 0
return (absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y)) * ORTHOGONAL_MOVE_COST
func _reconstruct_path(came_from: Dictionary, goal: Vector3i) -> Array[Vector3i]:
var reversed_path: Array[Vector3i] = [goal]
var current := goal
var maximum_length := _adjacency.size() + 1
while came_from.has(current) and reversed_path.size() <= maximum_length:
current = came_from[current]
reversed_path.append(current)
reversed_path.reverse()
return reversed_path
func _edge_meets_vertical_policy(from_cell: Vector3i, edge: Dictionary) -> bool:
return not _vertical_walk_requires_connector or from_cell.y == (edge[&"to"] as Vector3i).y \
or not StringName(edge[&"connector_id"]).is_empty() or edge[&"movement_type"] != MOVEMENT_WALK
func _edge_is_accessible(edge: Dictionary, allowed_access_ids: Array[StringName]) -> bool:
var required_access: StringName = edge[&"access_id"]
if not required_access.is_empty() and required_access not in allowed_access_ids:
return false
var connector_id: StringName = edge[&"connector_id"]
if connector_id.is_empty():
return true
return _connectors.has(connector_id) and _connector_is_operational(_connectors[connector_id])
func _edge_total_cost(edge: Dictionary) -> int:
var to_cell: Vector3i = edge[&"to"]
var total := int(edge.get(&"base_cost", edge.get(&"cost", ORTHOGONAL_MOVE_COST)))
total += get_cell_dynamic_cost(to_cell)
var connector_id: StringName = edge.get(&"connector_id", &"")
if connector_id.is_empty() or not _connectors.has(connector_id):
return total
var connector: Dictionary = _connectors[connector_id]
total += int(connector[&"dynamic_cost"])
if connector[&"movement_type"] == MOVEMENT_ELEVATOR:
total += estimate_elevator_wait_cost(connector_id)
var from_cell: Vector3i = connector[&"from"]
var destination: Vector3i = connector[&"to"]
var floor_distance := absi(destination.y - from_cell.y)
total += floor_distance * int(connector[&"travel_minutes_per_floor"]) * ORTHOGONAL_MOVE_COST
return total
func _find_edge(
from_cell: Vector3i,
to_cell: Vector3i,
allowed_access_ids: Variant = null
) -> Dictionary:
var best: Dictionary = {}
var best_cost := _INFINITE_COST
for edge_value: Variant in _adjacency.get(from_cell, []):
var edge: Dictionary = edge_value
if edge[&"to"] != to_cell:
continue
if not _edge_meets_vertical_policy(from_cell, edge):
continue
if allowed_access_ids is Array and not _edge_is_accessible(edge, allowed_access_ids):
continue
var cost := _edge_total_cost(edge)
if best.is_empty() or cost < best_cost \
or (cost == best_cost and _edge_less(edge, best)):
best = edge
best_cost = cost
return best
func _path_is_currently_valid(
path: Array,
allowed_layers: Array[int],
allowed_access_ids: Array[StringName]
) -> bool:
if path.is_empty():
return false
for cell_value: Variant in path:
if not cell_value is Vector3i:
return false
var cell: Vector3i = cell_value
if not _passable_cells.has(cell) or cell.z not in allowed_layers \
or _cell_is_temporarily_blocked(cell):
return false
for index: int in range(1, path.size()):
var edge := _find_edge(path[index - 1], path[index], allowed_access_ids)
if edge.is_empty():
return false
return true
func _route_failure(reason: StringName) -> Array[Vector3i]:
last_no_route_reason = reason
last_path_cost = 0
last_error = str(reason)
return []
func _cell_is_temporarily_blocked(cell: Vector3i) -> bool:
var traffic: Dictionary = _cell_traffic.get(cell, {})
return not traffic.is_empty() and bool(traffic[&"temporary_block"])
func _diagnose_no_route_reason(
start: Vector3i,
goal: Vector3i,
allowed_layers: Array[int],
allowed_access_ids: Array[StringName]
) -> StringName:
# Diagnose only obstacles which actually separate this start/goal pair. A
# stopped lift or blocked cell in another disconnected region must not leak
# into this route's reason.
if _route_exists_with_overrides(
start,
goal,
allowed_layers,
allowed_access_ids,
true,
false
):
return ROUTE_TEMPORARILY_BLOCKED
if _route_exists_with_overrides(
start,
goal,
allowed_layers,
allowed_access_ids,
false,
true
):
return ROUTE_CONNECTOR_UNAVAILABLE
return ROUTE_GOAL_UNREACHABLE
func _route_exists_with_overrides(
start: Vector3i,
goal: Vector3i,
allowed_layers: Array[int],
allowed_access_ids: Array[StringName],
allow_blocked_cells: bool,
allow_unavailable_connectors: bool
) -> bool:
var frontier: Array[Vector3i] = [start]
var visited: Dictionary = {start: true}
var cursor := 0
var connector_ids := _sorted_name_keys(_connectors)
while cursor < frontier.size():
var current: Vector3i = frontier[cursor]
cursor += 1
if current == goal:
return true
var neighbor_set: Dictionary = {}
for direction: Vector3i in _CARDINAL_DIRECTIONS:
var neighbor := current + direction
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(current, neighbor) \
and (not _vertical_walk_requires_connector or current.y == neighbor.y):
neighbor_set[neighbor] = true
for connector_id: StringName in connector_ids:
var connector: Dictionary = _connectors[connector_id]
var required_access: StringName = connector[&"access_id"]
if not required_access.is_empty() and required_access not in allowed_access_ids:
continue
if not allow_unavailable_connectors and not _connector_is_operational(connector):
continue
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
if current == from_cell:
neighbor_set[to_cell] = true
elif bool(connector[&"bidirectional"]) and current == to_cell:
neighbor_set[from_cell] = true
var neighbors: Array = neighbor_set.keys()
neighbors.sort_custom(Callable(self, "_coordinate_less"))
for neighbor_value: Variant in neighbors:
var neighbor: Vector3i = neighbor_value
if visited.has(neighbor) or neighbor.z not in allowed_layers:
continue
if not allow_blocked_cells and _cell_is_temporarily_blocked(neighbor):
continue
visited[neighbor] = true
frontier.append(neighbor)
return false
func _connector_is_operational(connector: Dictionary) -> bool:
return bool(connector[&"enabled"]) \
and connector[&"operational_state"] == CONNECTOR_OPERATIONAL \
and (
connector[&"movement_type"] != MOVEMENT_ELEVATOR
or connector[&"cabin_state"] != CABIN_OUTAGE
)
func _ensure_cell_traffic_for_graph(cells: Dictionary, reset_missing: bool) -> void:
var next_traffic: Dictionary = {}
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
if _cell_traffic.has(cell):
next_traffic[cell] = (_cell_traffic[cell] as Dictionary).duplicate(true)
elif reset_missing:
next_traffic[cell] = _default_cell_traffic()
else:
next_traffic[cell] = _default_cell_traffic()
_cell_traffic = next_traffic
var invalid_actor_ids: Array = []
for actor_id: Variant in _actor_cells:
if not cells.has(_actor_cells[actor_id]):
invalid_actor_ids.append(actor_id)
for actor_id: Variant in invalid_actor_ids:
_actor_cells.erase(actor_id)
_transit_actor_ids = _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
_recompute_all_densities()
func _default_cell_traffic() -> Dictionary:
return {
&"base_capacity": DEFAULT_CELL_CAPACITY,
&"current_density": 0,
&"dynamic_cost": 0,
&"temporary_block": false,
}
func _increment_density(cell: Vector3i) -> void:
var traffic: Dictionary = _cell_traffic[cell]
traffic[&"current_density"] = int(traffic[&"current_density"]) + 1
_cell_traffic[cell] = traffic
func _decrement_density(cell: Vector3i) -> bool:
if not _cell_traffic.has(cell):
return false
var traffic: Dictionary = _cell_traffic[cell]
var density := int(traffic[&"current_density"])
if density <= 0:
return false
traffic[&"current_density"] = density - 1
_cell_traffic[cell] = traffic
return true
func _recompute_all_densities() -> void:
for cell_value: Variant in _cell_traffic:
var traffic: Dictionary = _cell_traffic[cell_value]
traffic[&"current_density"] = 0
_cell_traffic[cell_value] = traffic
for actor_id: Variant in _actor_cells:
var cell: Vector3i = _actor_cells[actor_id]
if _cell_traffic.has(cell) and not is_actor_in_transit(actor_id):
_increment_density(cell)
static func _derive_transit_actor_ids(actor_cells: Dictionary, active_units: Dictionary) -> Dictionary:
var transit: Dictionary = {}
for actor_id: Variant in active_units:
if actor_cells.has(actor_id) and active_units[actor_id][&"state"] == ELEVATOR_REQUEST_BOARDED:
transit[actor_id] = true
return transit
func _refresh_transit_actor_ids() -> void:
var next := _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
if next == _transit_actor_ids:
return
var changed_cells: Dictionary = {}
var may_make_routes_cheaper := false
for actor_id: Variant in next:
if not _transit_actor_ids.has(actor_id):
changed_cells[_actor_cells[actor_id]] = true
may_make_routes_cheaper = true
for actor_id: Variant in _transit_actor_ids:
if not next.has(actor_id) and _actor_cells.has(actor_id):
changed_cells[_actor_cells[actor_id]] = true
_transit_actor_ids = next
_recompute_all_densities()
if not changed_cells.is_empty():
_mark_traffic_changed(changed_cells.keys(), may_make_routes_cheaper)
static func _transit_snapshot_matches(raw_ids: Variant, expected: Dictionary) -> bool:
if not raw_ids is Array or raw_ids.size() != expected.size():
return false
var previous := ""
for value: Variant in raw_ids:
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME] or String(value).is_empty() \
or not expected.has(StringName(value)) or (not previous.is_empty() and String(value) <= previous):
return false
previous = String(value)
return true
func _mark_traffic_changed(cells: Variant, may_make_routes_cheaper: bool = false) -> void:
traffic_revision += 1
if may_make_routes_cheaper:
# A cached path cannot know which non-selected alternatives were explored.
# Cost decreases and block removal therefore invalidate broadly for
# correctness; cost increases remain locally invalidated.
invalidate_all_cache()
else:
invalidate_cache_for_cells(cells)
func _mark_connector_traffic_changed(
connector: Dictionary,
may_make_routes_cheaper: bool = false
) -> void:
_mark_traffic_changed(
[connector[&"from"], connector[&"to"]],
may_make_routes_cheaper
)
func _traffic_effective_cost(traffic: Dictionary) -> int:
return int(traffic[&"dynamic_cost"]) + maxi(
0,
int(traffic[&"current_density"]) - int(traffic[&"base_capacity"])
) * CONGESTION_COST_PER_EXCESS
func _queue_failure(reason: StringName, message: String) -> Dictionary:
last_error = message
return {&"ok": false, &"reason": reason, &"error": message}
func _normalize_elevator_request(raw: Dictionary) -> Dictionary:
var request_id := StringName(str(_dictionary_value(raw, &"request_id", "")))
var unit_id := StringName(str(_dictionary_value(raw, &"unit_id", "")))
var floor_from_value: Variant = _dictionary_value(raw, &"floor_from", null)
var floor_to_value: Variant = _dictionary_value(raw, &"floor_to", null)
var enqueued_at_value: Variant = _dictionary_value(raw, &"enqueued_at", null)
if request_id.is_empty() or unit_id.is_empty():
return {&"ok": false, &"error": "Elevator request and unit ids must not be empty."}
if not floor_from_value is int or not floor_to_value is int or not enqueued_at_value is int:
return {&"ok": false, &"error": "Elevator floors and enqueue minute must be integers."}
var floor_from := int(floor_from_value)
var floor_to := int(floor_to_value)
var enqueued_at := int(enqueued_at_value)
if floor_from == floor_to or enqueued_at < 0:
return {&"ok": false, &"error": "Elevator request must change floors at a non-negative minute."}
var expected_direction := &"UP" if floor_to > floor_from else &"DOWN"
var direction := StringName(str(_dictionary_value(raw, &"direction", expected_direction)))
if direction != expected_direction:
return {&"ok": false, &"error": "Elevator request direction disagrees with its floors."}
var priority_result := _normalize_elevator_priority(_dictionary_value(
raw,
&"priority",
ELEVATOR_PRIORITY_NORMAL
))
if not priority_result[&"ok"]:
return priority_result
return {
&"ok": true,
&"request": {
&"request_id": request_id,
&"unit_id": unit_id,
&"floor_from": floor_from,
&"floor_to": floor_to,
&"direction": direction,
&"priority": int(priority_result[&"priority"]),
&"enqueued_at": enqueued_at,
},
}
func _normalize_elevator_priority(value: Variant) -> Dictionary:
if value is int:
var priority := int(value)
if priority < ELEVATOR_PRIORITY_RESCUE or priority > ELEVATOR_PRIORITY_NORMAL:
return {&"ok": false, &"error": "Elevator priority is outside the supported range."}
return {&"ok": true, &"priority": priority}
if value is String or value is StringName:
var key := StringName(str(value).to_upper())
var priorities := {
&"RESCUE": ELEVATOR_PRIORITY_RESCUE,
&"SECURITY": ELEVATOR_PRIORITY_SECURITY,
&"PROTAGONIST": ELEVATOR_PRIORITY_PROTAGONIST,
&"VIP": ELEVATOR_PRIORITY_VIP,
&"NORMAL": ELEVATOR_PRIORITY_NORMAL,
}
if priorities.has(key):
return {&"ok": true, &"priority": priorities[key]}
return {&"ok": false, &"error": "Elevator priority must be a known name or integer."}
func _elevator_request_base_less(left: Variant, right: Variant) -> bool:
var left_request: Dictionary = left
var right_request: Dictionary = right
return _compare_elevator_requests(left_request, right_request, -1)
func _elevator_request_less_at(
left: Dictionary,
right: Dictionary,
absolute_minute: int
) -> bool:
return _compare_elevator_requests(left, right, absolute_minute)
func _compare_elevator_requests(
left: Dictionary,
right: Dictionary,
absolute_minute: int
) -> bool:
var left_priority := _effective_elevator_priority(left, absolute_minute)
var right_priority := _effective_elevator_priority(right, absolute_minute)
if left_priority != right_priority:
return left_priority < right_priority
var left_enqueued := int(left[&"enqueued_at"])
var right_enqueued := int(right[&"enqueued_at"])
if left_enqueued != right_enqueued:
return left_enqueued < right_enqueued
var left_unit := str(left[&"unit_id"])
var right_unit := str(right[&"unit_id"])
if left_unit != right_unit:
return left_unit < right_unit
return str(left[&"request_id"]) < str(right[&"request_id"])
func _effective_elevator_priority(request: Dictionary, absolute_minute: int) -> int:
var priority := int(request[&"priority"])
if absolute_minute >= 0 \
and priority == ELEVATOR_PRIORITY_NORMAL \
and absolute_minute - int(request[&"enqueued_at"]) >= ELEVATOR_STARVATION_MINUTES:
return ELEVATOR_PRIORITY_VIP - 1
return priority
func _normalize_cell_set(raw_cells: Variant, validate_layers := true) -> Dictionary:
if not (raw_cells is Array) and not (raw_cells is Dictionary):
return {
&"ok": false,
&"error": "Passable cells must be an Array or Dictionary set.",
}
var values: Array = []
if raw_cells is Array:
values = raw_cells
else:
for key: Variant in raw_cells:
if bool(raw_cells[key]):
values.append(key)
var cells: Dictionary = {}
for value: Variant in values:
if not value is Vector3i:
return {
&"ok": false,
&"error": "Every passable coordinate must be a Vector3i.",
}
var cell: Vector3i = value
if validate_layers and not _is_valid_layer(cell.z):
return {
&"ok": false,
&"error": "Coordinate layer must be FRONT (0) or BACK (1): %s" % cell,
}
cells[cell] = true
return {
&"ok": true,
&"cells": cells,
}
func _normalize_connector(raw: Dictionary) -> Dictionary:
var connector_id := StringName(str(_dictionary_value(raw, &"id", "")))
var from_value: Variant = _dictionary_value(raw, &"from", null)
var to_value: Variant = _dictionary_value(raw, &"to", null)
var cost := int(_dictionary_value(raw, &"cost", DEFAULT_CONNECTOR_COST))
var bidirectional := bool(_dictionary_value(raw, &"bidirectional", true))
var access_id := StringName(str(_dictionary_value(raw, &"access_id", "")))
var enabled := bool(_dictionary_value(raw, &"enabled", true))
var movement_type := StringName(str(_dictionary_value(raw, &"movement_type", MOVEMENT_CONNECTOR)))
var dynamic_cost := int(_dictionary_value(raw, &"dynamic_cost", 0))
var operational_state := StringName(str(_dictionary_value(
raw,
&"operational_state",
CONNECTOR_OPERATIONAL
)))
var cabin_capacity := int(_dictionary_value(raw, &"cabin_capacity", 8))
var queue_capacity := int(_dictionary_value(raw, &"queue_capacity", 256))
var cabin_state := StringName(str(_dictionary_value(raw, &"cabin_state", CABIN_IDLE)))
var current_floor := int(_dictionary_value(
raw,
&"current_floor",
from_value.y if from_value is Vector3i else 0
))
var travel_minutes_per_floor := int(_dictionary_value(raw, &"travel_minutes_per_floor", 1))
var raw_queue: Variant = _dictionary_value(raw, &"request_queue", [])
if connector_id.is_empty():
return {&"ok": false, &"error": "Connector id must not be empty."}
if not from_value is Vector3i or not to_value is Vector3i:
return {&"ok": false, &"error": "Connector endpoints must be Vector3i coordinates."}
var from_cell: Vector3i = from_value
var to_cell: Vector3i = to_value
if not _is_valid_layer(from_cell.z) or not _is_valid_layer(to_cell.z):
return {&"ok": false, &"error": "Connector endpoint layer must be FRONT (0) or BACK (1)."}
if from_cell == to_cell:
return {&"ok": false, &"error": "Connector endpoints must differ."}
if cost <= 0:
return {&"ok": false, &"error": "Connector cost must be a positive integer."}
if dynamic_cost < 0:
return {&"ok": false, &"error": "Connector dynamic cost must be non-negative."}
if movement_type not in [MOVEMENT_CONNECTOR, MOVEMENT_ELEVATOR]:
return {&"ok": false, &"error": "Unsupported connector movement type: %s" % movement_type}
if operational_state not in [
CONNECTOR_OPERATIONAL,
CONNECTOR_STOPPED,
CONNECTOR_OUTAGE,
CONNECTOR_MAINTENANCE,
]:
return {&"ok": false, &"error": "Unsupported connector operational state."}
if cabin_capacity <= 0 or queue_capacity <= 0 or travel_minutes_per_floor <= 0:
return {&"ok": false, &"error": "Elevator capacities and travel time must be positive."}
if cabin_state not in [
CABIN_IDLE,
CABIN_MOVING_UP,
CABIN_MOVING_DOWN,
CABIN_DOOR_OPEN,
CABIN_OUTAGE,
]:
return {&"ok": false, &"error": "Unsupported elevator cabin state."}
if not raw_queue is Array:
return {&"ok": false, &"error": "Connector request queue must be an Array."}
var request_queue: Array = []
var request_ids: Dictionary = {}
for raw_request: Variant in raw_queue:
if not raw_request is Dictionary:
return {&"ok": false, &"error": "Elevator queue entries must be Dictionaries."}
var normalized_request := _normalize_elevator_request(raw_request)
if not normalized_request[&"ok"]:
return normalized_request
var request: Dictionary = normalized_request[&"request"]
if request_ids.has(request[&"request_id"]):
return {&"ok": false, &"error": "Duplicate elevator request id."}
request_ids[request[&"request_id"]] = true
request_queue.append(request)
if request_queue.size() > queue_capacity:
return {&"ok": false, &"error": "Elevator request queue exceeds configured capacity."}
if movement_type != MOVEMENT_ELEVATOR and not request_queue.is_empty():
return {&"ok": false, &"error": "Only elevator connectors can own requests."}
request_queue.sort_custom(Callable(self, "_elevator_request_base_less"))
if movement_type == MOVEMENT_ELEVATOR:
if operational_state != CONNECTOR_OPERATIONAL:
cabin_state = CABIN_OUTAGE
elif cabin_state == CABIN_OUTAGE:
return {&"ok": false, &"error": "An operational elevator cannot have OUTAGE cabin state."}
else:
# Cabin state has no semantic meaning for doors/stairs. Keeping one fixed
# canonical value prevents an OUTAGE connector from changing digest merely
# by passing through snapshot normalization.
cabin_state = CABIN_IDLE
return {
&"ok": true,
&"connector": {
&"id": connector_id,
&"from": from_cell,
&"to": to_cell,
&"cost": cost,
&"bidirectional": bidirectional,
&"access_id": access_id,
&"enabled": enabled,
&"movement_type": movement_type,
&"dynamic_cost": dynamic_cost,
&"operational_state": operational_state,
&"cabin_capacity": cabin_capacity,
&"queue_capacity": queue_capacity,
&"cabin_state": cabin_state,
&"current_floor": current_floor,
&"travel_minutes_per_floor": travel_minutes_per_floor,
&"request_queue": request_queue,
},
}
func _normalize_connector_collection(raw_connectors: Array) -> Dictionary:
var connectors: Dictionary = {}
for raw_configuration: Variant in raw_connectors:
if not raw_configuration is Dictionary:
return {&"ok": false, &"error": "Connector configuration must be a Dictionary."}
var normalized := _normalize_connector(raw_configuration)
if not normalized[&"ok"]:
return normalized
var connector: Dictionary = normalized[&"connector"]
var connector_id: StringName = connector[&"id"]
if connectors.has(connector_id):
return {&"ok": false, &"error": "Duplicate connector id: %s" % connector_id}
connectors[connector_id] = connector
var index_result := _derive_elevator_indexes_from_connectors(connectors)
if not index_result[&"ok"]:
return index_result
return {
&"ok": true,
&"connectors": connectors,
&"known_request_ids": index_result[&"known_request_ids"],
&"active_units": index_result[&"active_units"],
}
func _derive_elevator_indexes_from_connectors(connectors: Dictionary) -> Dictionary:
var known_request_ids: Dictionary = {}
var active_units: Dictionary = {}
for connector_id: StringName in _sorted_name_keys(connectors):
var connector: Dictionary = connectors[connector_id]
for request_value: Variant in connector[&"request_queue"]:
var request: Dictionary = request_value
var request_id: StringName = request[&"request_id"]
var unit_id: StringName = request[&"unit_id"]
if known_request_ids.has(request_id):
return {
&"ok": false,
&"error": "Elevator request id is duplicated across connectors: %s" % request_id,
}
if active_units.has(unit_id):
return {
&"ok": false,
&"error": "Elevator unit has requests on multiple connectors: %s" % unit_id,
}
known_request_ids[request_id] = true
active_units[unit_id] = {
&"unit_id": unit_id,
&"request_id": request_id,
&"connector_id": connector_id,
&"state": ELEVATOR_REQUEST_QUEUED,
}
return {
&"ok": true,
&"known_request_ids": known_request_ids,
&"active_units": active_units,
}
func _normalize_snapshot_elevator_indexes(
raw_known_requests: Array,
raw_active_units: Array,
connectors: Dictionary,
queued_units: Dictionary
) -> Dictionary:
var known_request_ids: Dictionary = {}
for raw_request_id: Variant in raw_known_requests:
if not raw_request_id is String and not raw_request_id is StringName:
return {&"ok": false, &"error": "Known elevator request ids must be names."}
var request_id := StringName(str(raw_request_id))
if request_id.is_empty() or known_request_ids.has(request_id):
return {&"ok": false, &"error": "Known elevator request ids must be unique and non-empty."}
known_request_ids[request_id] = true
var active_units: Dictionary = {}
var active_request_ids: Dictionary = {}
for raw_active: Variant in raw_active_units:
if not raw_active is Dictionary:
return {&"ok": false, &"error": "Active elevator unit entries must be Dictionaries."}
var unit_id := StringName(str(_dictionary_value(raw_active, &"unit_id", "")))
var request_id := StringName(str(_dictionary_value(raw_active, &"request_id", "")))
var connector_id := StringName(str(_dictionary_value(raw_active, &"connector_id", "")))
var state := StringName(str(_dictionary_value(raw_active, &"state", "")))
if unit_id.is_empty() or request_id.is_empty() or connector_id.is_empty() \
or state not in [ELEVATOR_REQUEST_QUEUED, ELEVATOR_REQUEST_BOARDED]:
return {&"ok": false, &"error": "Active elevator unit entry is malformed."}
if active_units.has(unit_id) or active_request_ids.has(request_id):
return {&"ok": false, &"error": "Active elevator request indexes contain duplicates."}
if not known_request_ids.has(request_id) or not connectors.has(connector_id):
return {&"ok": false, &"error": "Active elevator request references unknown state."}
var connector: Dictionary = connectors[connector_id]
if connector[&"movement_type"] != MOVEMENT_ELEVATOR:
return {&"ok": false, &"error": "Active elevator request references a non-elevator connector."}
active_units[unit_id] = {
&"unit_id": unit_id,
&"request_id": request_id,
&"connector_id": connector_id,
&"state": state,
}
active_request_ids[request_id] = true
for unit_id: Variant in queued_units:
if not active_units.has(unit_id) or active_units[unit_id] != queued_units[unit_id]:
return {&"ok": false, &"error": "Queued elevator request is missing from active indexes."}
for unit_id: Variant in active_units:
var active: Dictionary = active_units[unit_id]
if active[&"state"] == ELEVATOR_REQUEST_QUEUED \
and (not queued_units.has(unit_id) or queued_units[unit_id] != active):
return {&"ok": false, &"error": "Active QUEUED index has no matching connector request."}
return {
&"ok": true,
&"known_request_ids": known_request_ids,
&"active_units": active_units,
}
func _normalize_snapshot_vertical_blockers(raw: Variant, traffic: Dictionary) -> Dictionary:
var failure := {&"ok": false, &"error": "Path shaft ownership rows are invalid, unordered, or crosslink unknown cells."}
if not raw is Array or raw.size() > MAX_VERTICAL_BLOCKER_OWNERS:
return failure
var blockers: Dictionary = {}
var previous_owner := ""
var total_cells := 0
for row: Variant in raw:
if not row is Dictionary or row.size() != 2:
return failure
var owner_value: Variant = _dictionary_value(row, &"owner_id", null)
var cells_value: Variant = _dictionary_value(row, &"cells", null)
if typeof(owner_value) not in [TYPE_STRING, TYPE_STRING_NAME] or not cells_value is Array:
return failure
var owner_id := StringName(owner_value)
if not _vertical_blocker_owner_is_valid(owner_id) or String(owner_id) <= previous_owner or cells_value.is_empty():
return failure
previous_owner = String(owner_id)
total_cells += cells_value.size()
if total_cells > MAX_VERTICAL_BLOCKER_CELLS:
return failure
var cells: Array[Vector3i] = []
var seen: Dictionary = {}
for cell: Variant in cells_value:
if not cell is Vector3i or not traffic.has(cell) or seen.has(cell):
return failure
seen[cell] = true
cells.append(cell)
cells.sort_custom(Callable(self, "_coordinate_less"))
if cells != cells_value:
return failure
blockers[owner_id] = cells
return {&"ok": true, &"blockers": blockers}
func _normalize_snapshot_cell_traffic(raw_entries: Array) -> Dictionary:
var traffic: Dictionary = {}
for raw_entry: Variant in raw_entries:
if not raw_entry is Dictionary:
return {&"ok": false, &"error": "Path traffic entries must be Dictionaries."}
var cell: Variant = _dictionary_value(raw_entry, &"cell", null)
var capacity_value: Variant = _dictionary_value(raw_entry, &"base_capacity", null)
var density_value: Variant = _dictionary_value(raw_entry, &"current_density", null)
var cost_value: Variant = _dictionary_value(raw_entry, &"dynamic_cost", null)
var block_value: Variant = _dictionary_value(raw_entry, &"temporary_block", null)
if not cell is Vector3i or not _is_valid_layer(cell.z) \
or not capacity_value is int or not density_value is int \
or not cost_value is int or not block_value is bool:
return {&"ok": false, &"error": "Path traffic entry has invalid field types."}
if traffic.has(cell):
return {&"ok": false, &"error": "Path traffic snapshot contains a duplicate cell."}
if int(capacity_value) <= 0 or int(density_value) < 0 or int(cost_value) < 0:
return {&"ok": false, &"error": "Path traffic values are outside valid bounds."}
traffic[cell] = {
&"base_capacity": int(capacity_value),
&"current_density": int(density_value),
&"dynamic_cost": int(cost_value),
&"temporary_block": bool(block_value),
}
return {&"ok": true, &"traffic": traffic}
func _normalize_snapshot_actor_cells(raw_entries: Array, traffic: Dictionary) -> Dictionary:
var actor_cells: Dictionary = {}
for raw_entry: Variant in raw_entries:
if not raw_entry is Dictionary:
return {&"ok": false, &"error": "Actor cell projections must be Dictionaries."}
var actor_id := StringName(str(_dictionary_value(raw_entry, &"actor_id", "")))
var cell: Variant = _dictionary_value(raw_entry, &"cell", null)
if actor_id.is_empty() or not cell is Vector3i or not traffic.has(cell):
return {&"ok": false, &"error": "Actor projection references an invalid id or cell."}
if actor_cells.has(actor_id):
return {&"ok": false, &"error": "Actor projection contains a duplicate actor id."}
actor_cells[actor_id] = cell
return {&"ok": true, &"actor_cells": actor_cells}
func _normalize_snapshot_actor_progress(raw_entries: Array) -> Dictionary:
var progress: Dictionary = {}
for raw_entry: Variant in raw_entries:
if not raw_entry is Dictionary:
return {&"ok": false, &"error": "Actor progress entries must be Dictionaries."}
var actor_id := StringName(str(_dictionary_value(raw_entry, &"actor_id", "")))
var last_cell: Variant = _dictionary_value(raw_entry, &"last_cell", null)
var progress_value: Variant = _dictionary_value(raw_entry, &"last_progress_at", null)
var observed_value: Variant = _dictionary_value(raw_entry, &"last_observed_at", null)
var stuck_value: Variant = _dictionary_value(raw_entry, &"stuck_reported_at", null)
if actor_id.is_empty() or not last_cell is Vector3i \
or not progress_value is int or not observed_value is int or not stuck_value is int:
return {&"ok": false, &"error": "Actor progress entry has invalid field types."}
var last_progress_at := int(progress_value)
var last_observed_at := int(observed_value)
var stuck_reported_at := int(stuck_value)
if last_progress_at < 0 or last_observed_at < last_progress_at \
or (stuck_reported_at >= 0 and stuck_reported_at < last_progress_at):
return {&"ok": false, &"error": "Actor progress timestamps are inconsistent."}
if progress.has(actor_id):
return {&"ok": false, &"error": "Actor progress contains a duplicate actor id."}
progress[actor_id] = {
&"actor_id": actor_id,
&"last_cell": last_cell,
&"last_progress_at": last_progress_at,
&"last_observed_at": last_observed_at,
&"stuck_reported_at": stuck_reported_at,
}
return {&"ok": true, &"progress": progress}
func _snapshot_densities_match(traffic: Dictionary, actor_cells: Dictionary, transit: Dictionary = {}) -> bool:
var expected: Dictionary = {}
for actor_id: Variant in actor_cells:
if transit.has(actor_id):
continue
var cell: Vector3i = actor_cells[actor_id]
expected[cell] = int(expected.get(cell, 0)) + 1
for cell_value: Variant in traffic:
var saved_density := int((traffic[cell_value] as Dictionary)[&"current_density"])
if saved_density != int(expected.get(cell_value, 0)):
return false
return true
func _recovery_distance(origin: Vector3i, candidate: Vector3i) -> int:
# Layer changes require an explicit connector in normal travel. Recovery is a
# proposal only, so a large layer penalty keeps the actor on its current layer
# whenever any valid local cell exists.
return absi(origin.x - candidate.x) + absi(origin.y - candidate.y) \
+ absi(origin.z - candidate.z) * 1_000_000
func _normalize_access_layers(raw_layers: Variant) -> Dictionary:
var values: Array = []
if raw_layers == null:
values = [FRONT, BACK]
elif raw_layers is int:
values = [raw_layers]
elif raw_layers is Array or raw_layers is PackedInt32Array:
values.assign(raw_layers)
else:
return {&"ok": false, &"error": "Access layers must be an integer or integer Array."}
var unique: Dictionary = {}
for value: Variant in values:
if not value is int or not _is_valid_layer(int(value)):
return {&"ok": false, &"error": "Access layer must be FRONT (0) or BACK (1)."}
unique[int(value)] = true
var normalized: Array[int] = []
for value: Variant in unique:
normalized.append(int(value))
normalized.sort()
return {&"ok": true, &"values": normalized}
func _normalize_access_ids(raw_access_ids: Variant) -> Dictionary:
var values: Array = []
if raw_access_ids == null:
values = []
elif raw_access_ids is String or raw_access_ids is StringName:
values = [raw_access_ids]
elif raw_access_ids is Array or raw_access_ids is PackedStringArray:
values.assign(raw_access_ids)
elif raw_access_ids is Dictionary:
for key: Variant in raw_access_ids:
if bool(raw_access_ids[key]):
values.append(key)
else:
return {&"ok": false, &"error": "Connector access ids must be a name, Array, or Dictionary set."}
var unique: Dictionary = {}
for value: Variant in values:
if not (value is String) and not (value is StringName):
return {&"ok": false, &"error": "Every connector access id must be a String or StringName."}
var access_id := StringName(str(value))
if not access_id.is_empty():
unique[access_id] = true
var normalized: Array[StringName] = []
for value: Variant in unique:
normalized.append(value)
normalized.sort_custom(Callable(self, "_string_name_less"))
return {&"ok": true, &"values": normalized}
func _cache_key(
start: Vector3i,
goal: Vector3i,
allowed_layers: Array[int],
allowed_access_ids: Array[StringName]
) -> String:
var layer_parts := PackedStringArray()
for layer in allowed_layers:
layer_parts.append(str(layer))
var access_parts := PackedStringArray()
for access_id in allowed_access_ids:
var value := str(access_id)
access_parts.append("%d:%s" % [value.length(), value])
return "%d,%d,%d>%d,%d,%d|L:%s|A:%s|R:%d" % [
start.x,
start.y,
start.z,
goal.x,
goal.y,
goal.z,
",".join(layer_parts),
",".join(access_parts),
graph_revision,
]
func _path_touches(path: Array, affected: Dictionary) -> bool:
for cell: Variant in path:
if affected.has(cell):
return true
return false
func _copy_path(path: Array) -> Array[Vector3i]:
var copied: Array[Vector3i] = []
for cell: Variant in path:
copied.append(cell)
return copied
func _mark_graph_changed() -> void:
graph_revision += 1
invalidate_all_cache()
func _is_valid_layer(layer: int) -> bool:
return layer == FRONT or layer == BACK
func _coordinate_less(left: Variant, right: Variant) -> bool:
var left_cell: Vector3i = left
var right_cell: Vector3i = right
if left_cell.x != right_cell.x:
return left_cell.x < right_cell.x
if left_cell.y != right_cell.y:
return left_cell.y < right_cell.y
return left_cell.z < right_cell.z
func _edge_less(left: Variant, right: Variant) -> bool:
var left_edge: Dictionary = left
var right_edge: Dictionary = right
var left_to: Vector3i = left_edge[&"to"]
var right_to: Vector3i = right_edge[&"to"]
if left_to != right_to:
return _coordinate_less(left_to, right_to)
var left_connector := str(left_edge[&"connector_id"])
var right_connector := str(right_edge[&"connector_id"])
if left_connector != right_connector:
return left_connector < right_connector
return int(left_edge[&"cost"]) < int(right_edge[&"cost"])
func _string_name_less(left: Variant, right: Variant) -> bool:
return str(left) < str(right)
func _sorted_name_keys(source: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for key: Variant in source:
result.append(StringName(key))
result.sort_custom(Callable(self, "_string_name_less"))
return result
func _profile_value(profile: Dictionary, keys: Array[StringName], default_value: Variant) -> Variant:
for key in keys:
if profile.has(key):
return profile[key]
var string_key := str(key)
if profile.has(string_key):
return profile[string_key]
return default_value
func _dictionary_value(source: Dictionary, key: StringName, default_value: Variant) -> Variant:
if source.has(key):
return source[key]
var string_key := str(key)
return source.get(string_key, default_value)