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 ## Cost-only update (no topology change), e.g. waiting riders elsewhere in the ## same elevator shaft. Any change may make routes cheaper, so all caches drop. func set_connector_dynamic_cost(connector_id: StringName, dynamic_cost: int) -> bool: if not _connectors.has(connector_id): last_error = "Unknown connector id: %s" % connector_id return false if dynamic_cost < 0: last_error = "Connector dynamic cost must be non-negative." return false var connector: Dictionary = _connectors[connector_id] if int(connector[&"dynamic_cost"]) == dynamic_cost: last_error = "" return true connector[&"dynamic_cost"] = dynamic_cost _connectors[connector_id] = connector traffic_revision += 1 invalidate_all_cache() 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 []