Files
TheTree_mock/scripts/world/path_service.gd
T
TakeshiandClaude Opus 5.5 0f23d34e9f Path: Apply "!id" access to layer-crossing and no-route diagnostics
A stopped guest warp now reports CONNECTOR_UNAVAILABLE instead of
GOAL_UNREACHABLE (P14-02 review r2 follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:12:47 +09:00

3116 lines
115 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.5
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 _sorted_passable_cell_keys: Array = []
var _connectors: Dictionary = {}
var _adjacency: Dictionary = {}
var _vertical_walk_blockers: Dictionary = {}
var _vertical_walk_blocked_cells: Dictionary = {}
var _path_cache: Dictionary = {}
# Derived state limited to one synchronous candidate projection, never serialized.
var _query_batch: 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
var previous_configuration: Dictionary = _connectors.get(connector_id, {})
_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_connector_endpoint_edges(previous_configuration, configuration)
_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
# Original 4.3: overlapping units move at half speed. A wide corridor's
# pathfinding capacity must not silently redefine this movement debuff.
return MINIMUM_SPEED_MULTIPLIER if int(traffic[&"current_density"]) > 1 else 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,
companions: Dictionary = {}
) -> 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 selected: Dictionary = {}
var by_actor: Dictionary = {}
for request: Dictionary in queue: by_actor[request[&"unit_id"]] = request
for request: Dictionary in queue:
var actor: StringName = request[&"unit_id"]
if selected.has(actor) or int(request[&"floor_from"]) != current_floor: continue
var partner: StringName = companions.get(actor, &"")
var group: Array[Dictionary] = [request]
if not partner.is_empty():
if companions.get(partner, &"") != actor or not by_actor.has(partner): continue
var other: Dictionary = by_actor[partner]
if other[&"floor_from"] != current_floor or other[&"floor_to"] != request[&"floor_to"]: continue
group.append(other)
if boarded.size() + group.size() > boarding_capacity:
break # Keep the oldest pair together and ahead of later requests.
for member: Dictionary in group:
selected[member[&"unit_id"]] = true
boarded.append(member.duplicate(true))
var remaining: Array = []
for request: Dictionary in queue:
if not selected.has(request[&"unit_id"]): 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
if start.z != goal.z and not _guest_batch_has_permitted_layer_crossing(start, goal, allowed_layers, allowed_access_ids):
return _route_failure(ROUTE_GOAL_UNREACHABLE)
var shared: Variant = _query_batch_path(start, goal, allowed_layers, allowed_access_ids)
var path: Array[Vector3i] = []
if shared == null:
path = _run_a_star(start, goal, allowed_layers, allowed_access_ids)
else:
path.assign(shared)
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
var batch_cost := -1
if allowed_access_ids.is_empty() and allowed_layers == [FRONT,BACK]:
batch_cost = _guest_batch_path_cost(start,goal,path)
last_path_cost = batch_cost if batch_cost >= 0 else _calculate_path_cost_for_access(path, allowed_access_ids)
last_error = ""
return path
# Only the default guest access profile is shared. A mutation invalidates the
# whole batch through the same public cache invalidation paths as ordinary routes.
func begin_guest_path_query_batch(start: Vector3i, goals: Array) -> void:
_query_batch.clear()
# Without connectors the original Manhattan solver stops on goal pop.
# Equal-cost canonical parents can then differ: retain that solver exactly.
if _connectors.is_empty(): return
var layers: Array[int] = [FRONT, BACK]
var access: Array[StringName] = []
var targets: Dictionary = {}
var crossings: Dictionary = {}
for value: Variant in goals:
if not value is Vector3i: continue
var goal: Vector3i = value
if not _passable_cells.has(goal) or _cell_is_temporarily_blocked(goal): continue
if start.z != goal.z:
if not crossings.has(goal.z): crossings[goal.z] = _has_permitted_layer_crossing(start.z, goal.z, access)
if not bool(crossings[goal.z]): continue
targets[goal] = true
if not targets.is_empty() and _passable_cells.has(start) and not _cell_is_temporarily_blocked(start):
_query_batch = {&"start":start, &"targets":targets, &"ready":false, &"tree":{}, &"crossings":crossings}
# Directed layer permission depends only on connectors/access in this synchronous
# default guest batch. All existing invalidation and restore paths clear it.
func _guest_batch_has_permitted_layer_crossing(start: Vector3i, goal: Vector3i, layers: Array[int], access: Array[StringName]) -> bool:
if not _query_batch.is_empty() and start == _query_batch[&"start"] and layers == [FRONT,BACK] and access.is_empty():
var crossings: Dictionary = _query_batch[&"crossings"]
if crossings.has(goal.z): return bool(crossings[goal.z])
return _has_permitted_layer_crossing(start.z,goal.z,access)
func end_guest_path_query_batch() -> void:
_query_batch.clear()
func _query_batch_path(start: Vector3i, goal: Vector3i, layers: Array[int], access: Array[StringName]) -> Variant:
if _query_batch.is_empty() or start != _query_batch[&"start"] \
or layers != [FRONT, BACK] or not access.is_empty() or not _query_batch[&"targets"].has(goal):
return null
if start == goal: return [start]
if not bool(_query_batch[&"ready"]):
_query_batch[&"tree"] = _build_guest_query_tree(start, _query_batch[&"targets"], layers, access)
_query_batch[&"arithmetic_wrapped"] = _guest_tree_arithmetic_wrapped
_query_batch[&"ready"] = true
var tree: Dictionary = _query_batch[&"tree"]
if not bool(tree.get(&"usable", false)): return null
var settled: Dictionary = tree[&"settled"]
if not settled.has(goal): return []
return _reconstruct_path(tree[&"parents"], goal)
# Positive edge costs make every shortest predecessor settle before its child.
# Resolving equal costs by the original coordinate parent rule gives the same
# canonical paths as the admissible goal search. Stop after requested goals, not
# after visiting the entire graph. Any nonpositive edge retains the old solver.
var _guest_tree_workspace: Dictionary = {}
# Derived search state only. Wrapped accumulated costs cannot price a cell path.
var _guest_tree_arithmetic_wrapped: bool = false
func _build_guest_query_tree(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
_guest_tree_arithmetic_wrapped = false
if get_script().resource_path != "res://scripts/world/path_service.gd" or layers != [FRONT,BACK] or not access.is_empty():
return _build_guest_query_tree_uncached(start,goals,layers,access)
return _build_guest_query_tree_shared(start,goals,layers,access)
func _build_guest_query_tree_uncached(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
# The sorted graph cells preserve the original x/y/layer heap and parent order.
# All numeric workspace is local to this search, including the coordinate map.
var cells: Array = _sorted_passable_cell_keys
var indices: Dictionary = {}
for index: int in range(cells.size()): indices[cells[index]] = index
if not indices.has(start): return {&"usable":false}
var costs := PackedInt64Array()
var parents := PackedInt64Array()
var finalized := PackedByteArray()
if costs.resize(cells.size()) != OK or parents.resize(cells.size()) != OK or finalized.resize(cells.size()) != OK:
return {&"usable":false}
costs.fill(_INFINITE_COST)
parents.fill(-1)
var start_index: int = indices[start]
costs[start_index] = 0
var discovered: Array[int] = [start_index]
var parent_discovered: Array[int] = []
var heap_nodes: Array[int] = []
var heap_costs: Array[int] = []
_indexed_tree_heap_push(heap_nodes, heap_costs, start_index, 0)
var pending: Dictionary = goals.duplicate()
var settled: Dictionary = {}
while not heap_nodes.is_empty():
var entry_cost: int = heap_costs[0]
var current_index := _indexed_tree_heap_pop(heap_nodes, heap_costs)
if finalized[current_index] != 0 or entry_cost != costs[current_index]: continue
var current: Vector3i = cells[current_index]
finalized[current_index] = 1
settled[current] = true
pending.erase(current)
if pending.is_empty(): break
var edges: Array = _adjacency.get(current, [])
for value: Variant in edges:
var edge: Dictionary = value
var neighbor: Vector3i = edge[&"to"]
if neighbor.z not in layers or _cell_is_temporarily_blocked(neighbor) \
or not _edge_meets_vertical_policy(current,edge) or not _edge_is_accessible(edge,access): continue
var edge_cost := _edge_total_cost(edge)
if edge_cost <= 0: return {&"usable":false}
if not indices.has(neighbor): return {&"usable":false}
var neighbor_index: int = indices[neighbor]
var cost := costs[current_index] + edge_cost
var known := costs[neighbor_index]
var better_parent := cost == known and parents[neighbor_index] >= 0 and current_index < parents[neighbor_index]
if cost > known or (cost == known and not better_parent): continue
if parents[neighbor_index] < 0:
parent_discovered.append(neighbor_index)
if neighbor_index != start_index: discovered.append(neighbor_index)
parents[neighbor_index] = current_index
costs[neighbor_index] = cost
finalized[neighbor_index] = 0
settled.erase(neighbor)
_indexed_tree_heap_push(heap_nodes, heap_costs, neighbor_index, cost)
# Keep the existing result dictionaries and their first-discovery order.
var result_parents: Dictionary = {}
var result_costs: Dictionary = {}
for index: int in discovered:
var cell: Vector3i = cells[index]
result_costs[cell] = costs[index]
for index: int in parent_discovered:
result_parents[cells[index]] = cells[parents[index]]
return {&"usable":true,&"settled":settled,&"parents":result_parents,&"costs":result_costs}
func _build_guest_query_tree_shared(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
# The sorted graph cells preserve the original x/y/layer heap and parent order.
# All numeric workspace is local to this search, including the coordinate map.
var cells: Array = _sorted_passable_cell_keys
if _guest_tree_workspace.is_empty():
var fresh_indices: Dictionary = {}
for index: int in range(cells.size()): fresh_indices[cells[index]] = index
_guest_tree_workspace = {&"indices":fresh_indices, &"edges":{}}
var indices: Dictionary = _guest_tree_workspace[&"indices"]
if not indices.has(start): return {&"usable":false}
var costs := PackedInt64Array()
var parents := PackedInt64Array()
var finalized := PackedByteArray()
if costs.resize(cells.size()) != OK or parents.resize(cells.size()) != OK or finalized.resize(cells.size()) != OK:
return {&"usable":false}
costs.fill(_INFINITE_COST)
parents.fill(-1)
var start_index: int = indices[start]
costs[start_index] = 0
var discovered: Array[int] = [start_index]
var parent_discovered: Array[int] = []
var heap_nodes: Array[int] = []
var heap_costs: Array[int] = []
_indexed_tree_heap_push(heap_nodes, heap_costs, start_index, 0)
var pending: Dictionary = goals.duplicate()
var settled: Dictionary = {}
while not heap_nodes.is_empty():
var entry_cost: int = heap_costs[0]
var current_index := _indexed_tree_heap_pop(heap_nodes, heap_costs)
if finalized[current_index] != 0 or entry_cost != costs[current_index]: continue
var current: Vector3i = cells[current_index]
finalized[current_index] = 1
settled[current] = true
pending.erase(current)
if pending.is_empty(): break
var edges: Array = _guest_tree_shared_edges(current, layers, access)
for value: Variant in edges:
var row: Array = value
var neighbor: Vector3i = row[0]
var edge_cost: int = row[1]
if edge_cost <= 0: return {&"usable":false}
if not indices.has(neighbor): return {&"usable":false}
var neighbor_index: int = indices[neighbor]
var cost := costs[current_index] + edge_cost
if cost < costs[current_index]: _guest_tree_arithmetic_wrapped = true
var known := costs[neighbor_index]
var better_parent := cost == known and parents[neighbor_index] >= 0 and current_index < parents[neighbor_index]
if cost > known or (cost == known and not better_parent): continue
if parents[neighbor_index] < 0:
parent_discovered.append(neighbor_index)
if neighbor_index != start_index: discovered.append(neighbor_index)
parents[neighbor_index] = current_index
costs[neighbor_index] = cost
finalized[neighbor_index] = 0
settled.erase(neighbor)
_indexed_tree_heap_push(heap_nodes, heap_costs, neighbor_index, cost)
# Keep the existing result dictionaries and their first-discovery order.
var result_parents: Dictionary = {}
var result_costs: Dictionary = {}
for index: int in discovered:
var cell: Vector3i = cells[index]
result_costs[cell] = costs[index]
for index: int in parent_discovered:
result_parents[cells[index]] = cells[parents[index]]
return {&"usable":true,&"settled":settled,&"parents":result_parents,&"costs":result_costs}
func _guest_tree_shared_edges(current: Vector3i, layers: Array[int], access: Array[StringName]) -> Array:
var shared: Dictionary = _guest_tree_workspace[&"edges"]
if shared.has(current): return shared[current]
var result: Array = []
for value: Variant in _adjacency.get(current,[]):
var edge: Dictionary = value
var neighbor: Vector3i = edge[&"to"]
if neighbor.z not in layers or _cell_is_temporarily_blocked(neighbor) \
or not _edge_meets_vertical_policy(current,edge) or not _edge_is_accessible(edge,access): continue
var cost := _edge_total_cost(edge)
result.append([neighbor,cost])
if cost <= 0 or not (_guest_tree_workspace[&"indices"] as Dictionary).has(neighbor): break
shared[current]=result
return result
func _indexed_tree_heap_push(nodes: Array[int], costs: Array[int], node: int, cost: int) -> void:
nodes.append(node)
costs.append(cost)
var index := nodes.size() - 1
while index > 0:
var parent := (index - 1) >> 1
if cost > costs[parent] or (cost == costs[parent] and node >= nodes[parent]): break
nodes[index] = nodes[parent]
costs[index] = costs[parent]
index = parent
nodes[index] = node
costs[index] = cost
func _indexed_tree_heap_pop(nodes: Array[int], costs: Array[int]) -> int:
var first := nodes[0]
var last_node: int = nodes.pop_back()
var last_cost: int = costs.pop_back()
if nodes.is_empty(): return first
var index := 0
while index * 2 + 1 < nodes.size():
var child := index * 2 + 1
if child + 1 < nodes.size() and (costs[child + 1] < costs[child] or (costs[child + 1] == costs[child] and nodes[child + 1] < nodes[child])):
child += 1
if costs[child] > last_cost or (costs[child] == last_cost and nodes[child] >= last_node): break
nodes[index] = nodes[child]
costs[index] = costs[child]
index = child
nodes[index] = last_node
costs[index] = last_cost
return first
# A matching current batch route without arithmetic wrap has an exact cost. Compare
# the whole path, not only its endpoints: a valid alternate may cost differently.
# Mutation/restore invalidation clears the batch even for equal revision numbers.
func _guest_batch_path_cost(start: Vector3i, goal: Vector3i, path: Array) -> int:
if _query_batch.is_empty() or start != _query_batch[&"start"] or not _query_batch[&"targets"].has(goal): return -1
if start == goal and path == [start]: return 0
if not bool(_query_batch[&"ready"]): return -1
if bool(_query_batch.get(&"arithmetic_wrapped", false)): return -1
var tree: Dictionary = _query_batch[&"tree"]
if not bool(tree.get(&"usable",false)) or not tree[&"settled"].has(goal): return -1
if path != _reconstruct_path(tree[&"parents"],goal): return -1
return int(tree[&"costs"][goal])
# Same public last_error behavior as default-access calculate_path_cost.
func calculate_guest_batch_path_cost(start: Vector3i, goal: Vector3i, path: Array) -> int:
var cost := _guest_batch_path_cost(start,goal,path)
if cost < 0: return calculate_path_cost(path)
last_error = ""
return cost
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
_query_batch.clear()
_guest_tree_workspace.clear()
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:
_query_batch.clear()
_guest_tree_workspace.clear()
var removed := _path_cache.size()
_path_cache.clear()
return removed
func cached_path_count() -> int:
return _path_cache.size()
## Compare graph membership without serializing traffic, actors and connectors.
## Duplicate expected cells are invalid, just as in snapshot crosslink validation.
func matches_passable_cells(expected_cells: Array[Vector3i]) -> bool:
if expected_cells.size() != _passable_cells.size():
return false
var seen: Dictionary = {}
for cell: Vector3i in expected_cells:
if seen.has(cell) or not _passable_cells.has(cell):
return false
seen[cell] = true
return true
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_ids_with_prefix(prefix: String) -> Array[StringName]:
var ids: Array[StringName] = []
for connector_id: Variant in _connectors:
if String(connector_id).begins_with(prefix):
ids.append(StringName(connector_id))
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return ids
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
# The caller only needs the selected connector id. Avoid constructing the
# detailed report and its extra cost calculation. Without a connector edge to
# this destination every possible selected edge has the empty connector id.
func get_step_connector(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> StringName:
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 &""
for edge: Dictionary in _adjacency.get(from_cell, []):
if edge[&"to"] == to_cell and not StringName(edge[&"connector_id"]).is_empty():
return StringName(_find_edge(from_cell,to_cell,normalized[&"values"]).get(&"connector_id", &""))
return &""
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:
# A nested sorted-name read can otherwise abort to an empty typed array
# while this outer capture still returns a seemingly valid Dictionary.
for transit_actor_id: Variant in _transit_actor_ids:
if typeof(transit_actor_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return {}
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 = _sorted_passable_cell_keys
var saved_cell_traffic: Array = []
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
var traffic: Dictionary = _cell_traffic[cell] if _cell_traffic.has(cell) else _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
## A single connector changes only the outgoing edges of its old/new endpoints.
## Keep the same walk-edge rules, sorted connector iteration and edge ordering
## as the full rebuild. Topology changes still use the full graph rebuild.
func _rebuild_connector_endpoint_edges(previous: Dictionary, current: Dictionary) -> void:
var affected: Dictionary = {}
for configuration: Dictionary in [previous, current]:
if configuration.is_empty():
continue
for key: StringName in [&"from", &"to"]:
var cell: Vector3i = configuration[key]
if _passable_cells.has(cell):
affected[cell] = true
var cells: Array = affected.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
_adjacency[cell] = []
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"]
if affected.has(from_cell):
_append_edge(from_cell, to_cell, cost, connector_id, access_id, movement_type)
if bool(connector[&"bidirectional"]) and affected.has(to_cell):
_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 _rebuild_derived_graph() -> void:
_adjacency.clear()
var cells: Array = _passable_cells.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
_sorted_passable_cell_keys = cells
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 connector_scale := _positive_connector_heuristic_scale()
var settled_goal_cost := _INFINITE_COST
var frontier: Array[Array] = []
_heap_push(frontier, [start, 0, _query_distance_lower_bound(start, goal, connector_scale)])
var closed_set: Dictionary = {}
var came_from: Dictionary = {}
var g_score: Dictionary = {start: 0}
while not frontier.is_empty():
var entry := _heap_pop(frontier)
if connector_scale > 0 and int(entry[2]) > settled_goal_cost:
break
var current: Vector3i = entry[0]
if closed_set.has(current) or int(entry[1]) != int(g_score[current]):
continue
if current == goal:
if connector_scale <= 0:
return _reconstruct_path(came_from, current)
settled_goal_cost = int(entry[1])
continue
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)
_heap_push(frontier, [neighbor, tentative_cost, tentative_cost + _query_distance_lower_bound(neighbor, goal, connector_scale)])
if settled_goal_cost < _INFINITE_COST:
return _reconstruct_path(came_from, goal)
return []
## Every permitted edge costs at least scale * its Manhattan displacement.
## A zero-cost connector keeps the existing conservative search unchanged.
func _positive_connector_heuristic_scale() -> int:
if _connectors.is_empty():
return 0
var scale := ORTHOGONAL_MOVE_COST
for connector_value: Variant in _connectors.values():
var connector: Dictionary = connector_value
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
var displacement := absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y) + absi(from_cell.z - to_cell.z)
if displacement > 0:
scale = mini(scale, int(connector[&"cost"]) / displacement)
if scale <= 0:
return 0
return scale
func _query_distance_lower_bound(from_cell: Vector3i, to_cell: Vector3i, connector_scale: int) -> int:
if connector_scale <= 0:
return _heuristic(from_cell, to_cell)
return (absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y) + absi(from_cell.z - to_cell.z)) * connector_scale
# Query-local heap. Stale improved-cost entries are skipped on removal.
# Keep the original f-score / x / y / layer order, including reopened nodes.
func _heap_less(left: Array, right: Array) -> bool:
return int(left[2]) < int(right[2]) or (int(left[2]) == int(right[2]) and _coordinate_less(left[0], right[0]))
func _heap_push(heap: Array[Array], entry: Array) -> void:
heap.append(entry)
var index := heap.size() - 1
while index > 0:
var parent := (index - 1) >> 1
if not _heap_less(entry, heap[parent]):
break
heap[index] = heap[parent]
index = parent
heap[index] = entry
func _heap_pop(heap: Array[Array]) -> Array:
var first := heap[0]
var last: Array = heap.pop_back()
if heap.is_empty():
return first
var index := 0
while index * 2 + 1 < heap.size():
var child := index * 2 + 1
if child + 1 < heap.size() and _heap_less(heap[child + 1], heap[child]):
child += 1
if not _heap_less(heap[child], last):
break
heap[index] = heap[child]
index = child
heap[index] = last
return first
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
## "" is public, "id" needs id, and "!id" is open to everyone except actors
## that carry id (guest-only warp).
static func _access_id_permits(required_access: StringName, allowed_access_ids: Array[StringName]) -> bool:
if String(required_access).begins_with("!"):
return StringName(String(required_access).substr(1)) not in allowed_access_ids
return required_access.is_empty() or required_access in allowed_access_ids
func _edge_is_accessible(edge: Dictionary, allowed_access_ids: Array[StringName]) -> bool:
if not _access_id_permits(edge[&"access_id"], 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"])
## Cardinal walking never changes layers. Even an unavailable or blocked
## connector must be retained here so the ordinary solver preserves its reason.
## Only the absence of any permitted directed crossing proves unreachable.
func _has_permitted_layer_crossing(from_layer: int, to_layer: int, allowed_access_ids: Array[StringName]) -> bool:
for connector: Dictionary in _connectors.values():
if not _access_id_permits(connector[&"access_id"], allowed_access_ids):
continue
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
if from_cell.z == from_layer and to_cell.z == to_layer:
return true
if bool(connector[&"bidirectional"]) and to_cell.z == from_layer and from_cell.z == to_layer:
return true
return false
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]
if not _access_id_permits(connector[&"access_id"], 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)
## Rescue uses BACK circulation, with FRONT access legs at either endpoint.
## Optional managed elevators must have room for both patient and carrier.
func find_backyard_path(start: Vector3i, goal: Vector3i, allow_elevators := false) -> Array[Vector3i]:
if not has_cell(start) or not has_cell(goal) or _cell_is_temporarily_blocked(start) or _cell_is_temporarily_blocked(goal):
return []
if start == goal:
return [start]
var first := Vector4i(start.x, start.y, start.z, 2 if start.z == 1 else 0)
var frontier: Array[Vector4i] = [first]
var visited := {first: true}
var parents: Dictionary = {}
var cursor := 0
while cursor < frontier.size():
var state := frontier[cursor]
cursor += 1
var cell := Vector3i(state.x,state.y,state.z)
for edge: Dictionary in _adjacency.get(cell, []):
var next: Vector3i = edge[&"to"]
if _cell_is_temporarily_blocked(next) or not _edge_meets_vertical_policy(cell, edge) or not _edge_is_accessible(edge, [&"staff"]):
continue
var connector: Dictionary = _connectors.get(edge[&"connector_id"], {})
if connector.get(&"movement_type", &"") == MOVEMENT_ELEVATOR:
if not allow_elevators or cell.z != 1 or next.z != 1 or not String(edge[&"connector_id"]).begins_with("transport.") or int(connector.get(&"cabin_capacity", 0)) < 2:
continue
if state.w == 3 and next.z == 1:
continue
var phase := state.w
if next.z == 1: phase = 1 if state.w == 0 else 2
elif state.w == 1: continue
elif state.w == 2: phase = 3
var key := Vector4i(next.x,next.y,next.z,phase)
if visited.has(key): continue
visited[key] = true
parents[key] = state
if next == goal and phase > 0:
var result: Array[Vector3i] = [next]
while parents.has(key):
key = parents[key]
result.push_front(Vector3i(key.x,key.y,key.z))
return result
frontier.append(key)
return []