class_name PathService extends RefCounted signal path_query_measured(duration_us: int) ## Deterministic path graph derived from WorldService-owned passable cells. ## ## Coordinates are Vector3i(x, y, layer), where layer is FRONT (0) or BACK (1). ## This service owns connector configuration, but not the authoritative world cells. const FRONT := 0 const BACK := 1 const ORTHOGONAL_MOVE_COST := 100 const DEFAULT_CONNECTOR_COST := 150 const DEFAULT_CELL_CAPACITY := 4 const CONGESTION_COST_PER_EXCESS := 75 const MINIMUM_SPEED_MULTIPLIER := 0.25 const STUCK_THRESHOLD_MINUTES := 30 const ELEVATOR_STARVATION_MINUTES := 60 const SNAPSHOT_VERSION := 3 const LEGACY_SNAPSHOT_VERSION := 1 const TRAFFIC_SNAPSHOT_VERSION := 2 const ELEVATOR_REQUEST_QUEUED := &"QUEUED" const ELEVATOR_REQUEST_BOARDED := &"BOARDED" const MOVEMENT_WALK := &"WALK" const MOVEMENT_CONNECTOR := &"CONNECTOR" const MOVEMENT_ELEVATOR := &"ELEVATOR" const CONNECTOR_OPERATIONAL := &"OPERATIONAL" const CONNECTOR_STOPPED := &"STOPPED" const CONNECTOR_OUTAGE := &"OUTAGE" const CONNECTOR_MAINTENANCE := &"MAINTENANCE" const CABIN_IDLE := &"IDLE" const CABIN_MOVING_UP := &"MOVING_UP" const CABIN_MOVING_DOWN := &"MOVING_DOWN" const CABIN_DOOR_OPEN := &"DOOR_OPEN" const CABIN_OUTAGE := &"OUTAGE" const ELEVATOR_PRIORITY_RESCUE := 0 const ELEVATOR_PRIORITY_SECURITY := 10 const ELEVATOR_PRIORITY_PROTAGONIST := 20 const ELEVATOR_PRIORITY_VIP := 30 const ELEVATOR_PRIORITY_NORMAL := 40 const ROUTE_OK := &"OK" const ROUTE_START_NOT_PASSABLE := &"START_NOT_PASSABLE" const ROUTE_GOAL_NOT_PASSABLE := &"GOAL_NOT_PASSABLE" const ROUTE_START_BLOCKED := &"START_TEMPORARILY_BLOCKED" const ROUTE_GOAL_BLOCKED := &"GOAL_TEMPORARILY_BLOCKED" const ROUTE_ACCESS_LAYER_DENIED := &"ACCESS_LAYER_DENIED" const ROUTE_CONNECTOR_UNAVAILABLE := &"CONNECTOR_UNAVAILABLE" const ROUTE_TEMPORARILY_BLOCKED := &"TEMPORARILY_BLOCKED" const ROUTE_GOAL_UNREACHABLE := &"GOAL_UNREACHABLE" const _CARDINAL_DIRECTIONS: Array[Vector3i] = [ Vector3i(-1, 0, 0), Vector3i(0, -1, 0), Vector3i(0, 1, 0), Vector3i(1, 0, 0), ] const _INFINITE_COST := 9223372036854775807 var graph_revision := 0 var traffic_revision := 0 var cache_hits := 0 var cache_misses := 0 var last_error := "" var last_no_route_reason: StringName = ROUTE_OK var last_path_cost := 0 var _passable_cells: Dictionary = {} var _connectors: Dictionary = {} var _adjacency: Dictionary = {} var _path_cache: Dictionary = {} var _cell_traffic: Dictionary = {} var _actor_cells: Dictionary = {} var _actor_progress: 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 func configure_connector( connector_id: StringName, from_cell: Vector3i, to_cell: Vector3i, cost := DEFAULT_CONNECTOR_COST, bidirectional := true, access_id: StringName = &"", enabled := true, options: Dictionary = {} ) -> bool: var raw_configuration := options.duplicate(true) raw_configuration.merge({ &"id": connector_id, &"from": from_cell, &"to": to_cell, &"cost": cost, &"bidirectional": bidirectional, &"access_id": access_id, &"enabled": enabled, }, true) var normalized := _normalize_connector(raw_configuration) if not normalized[&"ok"]: last_error = normalized[&"error"] return false var configuration: Dictionary = normalized[&"connector"] if _connectors.has(connector_id) and _connectors[connector_id] == configuration: last_error = "" return true for unit_id: Variant in _active_elevator_units: var active: Dictionary = _active_elevator_units[unit_id] if active[&"connector_id"] == connector_id: last_error = "Cannot reconfigure a connector with active elevator requests." return false var proposed_connectors := _connectors.duplicate(true) proposed_connectors[connector_id] = configuration var index_result := _derive_elevator_indexes_from_connectors(proposed_connectors) if not index_result[&"ok"]: last_error = index_result[&"error"] return false for request_id: Variant in index_result[&"known_request_ids"]: if _known_elevator_request_ids.has(request_id) \ and not _active_request_id_exists(StringName(request_id)): last_error = "Connector configuration reuses a retired elevator request id." return false _connectors[connector_id] = configuration var next_active: Dictionary = index_result[&"active_units"] for unit_id: Variant in _active_elevator_units: var active: Dictionary = _active_elevator_units[unit_id] if active[&"state"] == ELEVATOR_REQUEST_BOARDED: next_active[unit_id] = active.duplicate(true) _active_elevator_units = next_active for request_id: Variant in index_result[&"known_request_ids"]: _known_elevator_request_ids[request_id] = true _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func add_connector( connector_id: StringName, from_cell: Vector3i, to_cell: Vector3i, cost := DEFAULT_CONNECTOR_COST, bidirectional := true, access_id: StringName = &"" ) -> bool: return configure_connector( connector_id, from_cell, to_cell, cost, bidirectional, access_id, true ) func configure_elevator( connector_id: StringName, from_cell: Vector3i, to_cell: Vector3i, options: Dictionary = {}, access_id: StringName = &"" ) -> bool: var elevator_options := options.duplicate(true) elevator_options[&"movement_type"] = MOVEMENT_ELEVATOR var cost := int(_dictionary_value(elevator_options, &"cost", DEFAULT_CONNECTOR_COST)) var bidirectional := bool(_dictionary_value(elevator_options, &"bidirectional", true)) var enabled := bool(_dictionary_value(elevator_options, &"enabled", true)) return configure_connector( connector_id, from_cell, to_cell, cost, bidirectional, access_id, enabled, elevator_options ) func set_connectors(configurations: Array) -> bool: var normalized_collection := _normalize_connector_collection(configurations) if not normalized_collection[&"ok"]: last_error = normalized_collection[&"error"] return false var next_connectors: Dictionary = normalized_collection[&"connectors"] if next_connectors == _connectors: last_error = "" return true for request_id: Variant in normalized_collection[&"known_request_ids"]: if _known_elevator_request_ids.has(request_id) \ and not _active_request_id_exists(StringName(request_id)): last_error = "Connector set reuses a retired elevator request id." return false for unit_id: Variant in _active_elevator_units: if (_active_elevator_units[unit_id] as Dictionary)[&"state"] == ELEVATOR_REQUEST_BOARDED: last_error = "Cannot replace connector set while an elevator trip is boarded." return false _connectors = next_connectors _active_elevator_units = normalized_collection[&"active_units"] for request_id: Variant in normalized_collection[&"known_request_ids"]: _known_elevator_request_ids[request_id] = true _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func remove_connector(connector_id: StringName) -> bool: if not _connectors.has(connector_id): return false for unit_id: Variant in _active_elevator_units: if (_active_elevator_units[unit_id] as Dictionary)[&"connector_id"] == connector_id: last_error = "Cannot remove a connector with active elevator requests: %s" % connector_id return false _connectors.erase(connector_id) _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func set_connector_enabled(connector_id: StringName, enabled: bool) -> bool: if not _connectors.has(connector_id): last_error = "Unknown connector id: %s" % connector_id return false var connector: Dictionary = _connectors[connector_id] if bool(connector[&"enabled"]) == enabled: last_error = "" return true connector[&"enabled"] = enabled _connectors[connector_id] = connector _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func set_connector_operational_state(connector_id: StringName, state: StringName) -> bool: if not _connectors.has(connector_id): last_error = "Unknown connector id: %s" % connector_id return false if state not in [ CONNECTOR_OPERATIONAL, CONNECTOR_STOPPED, CONNECTOR_OUTAGE, CONNECTOR_MAINTENANCE, ]: last_error = "Unsupported connector operational state: %s" % state return false var connector: Dictionary = _connectors[connector_id] if connector[&"operational_state"] == state: last_error = "" return true connector[&"operational_state"] = state if connector[&"movement_type"] == MOVEMENT_ELEVATOR: connector[&"cabin_state"] = CABIN_IDLE if state == CONNECTOR_OPERATIONAL else CABIN_OUTAGE _connectors[connector_id] = connector _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func set_elevator_operational(connector_id: StringName, operational: bool) -> bool: return set_connector_operational_state( connector_id, CONNECTOR_OPERATIONAL if operational else CONNECTOR_OUTAGE ) func set_elevator_cabin_state(connector_id: StringName, cabin_state: StringName) -> bool: if not _connectors.has(connector_id): last_error = "Unknown connector id: %s" % connector_id return false if cabin_state not in [ CABIN_IDLE, CABIN_MOVING_UP, CABIN_MOVING_DOWN, CABIN_DOOR_OPEN, CABIN_OUTAGE, ]: last_error = "Unsupported elevator cabin state: %s" % cabin_state return false var connector: Dictionary = _connectors[connector_id] if connector[&"movement_type"] != MOVEMENT_ELEVATOR: last_error = "Connector is not an elevator: %s" % connector_id return false if connector[&"cabin_state"] == cabin_state: last_error = "" return true connector[&"cabin_state"] = cabin_state connector[&"operational_state"] = ( CONNECTOR_OUTAGE if cabin_state == CABIN_OUTAGE else CONNECTOR_OPERATIONAL ) _connectors[connector_id] = connector _rebuild_derived_graph() _mark_graph_changed() last_error = "" return true func clear_connectors() -> void: if _connectors.is_empty(): return if not _active_elevator_units.is_empty(): last_error = "Cannot clear connectors while elevator requests are active." return _connectors.clear() _rebuild_derived_graph() _mark_graph_changed() last_error = "" func configure_cell_traffic( cell: Vector3i, base_capacity: int = DEFAULT_CELL_CAPACITY, dynamic_cost: int = 0, temporary_block: bool = false ) -> bool: if not _passable_cells.has(cell): last_error = "Traffic configuration requires a passable cell: %s" % cell return false if base_capacity <= 0 or dynamic_cost < 0: last_error = "Cell capacity must be positive and dynamic cost non-negative." return false var current: Dictionary = _cell_traffic.get(cell, _default_cell_traffic()) var updated := { &"base_capacity": base_capacity, &"current_density": int(current.get(&"current_density", 0)), &"dynamic_cost": dynamic_cost, &"temporary_block": temporary_block, } if current == updated: last_error = "" return true var may_make_routes_cheaper := ( _traffic_effective_cost(current) > _traffic_effective_cost(updated) or (bool(current[&"temporary_block"]) and not temporary_block) ) _cell_traffic[cell] = updated _mark_traffic_changed([cell], may_make_routes_cheaper) last_error = "" return true func set_cell_base_capacity(cell: Vector3i, base_capacity: int) -> bool: var current := get_cell_traffic(cell) if current.is_empty(): last_error = "Unknown path cell: %s" % cell return false return configure_cell_traffic( cell, base_capacity, int(current[&"dynamic_cost"]), bool(current[&"temporary_block"]) ) func set_cell_dynamic_cost(cell: Vector3i, dynamic_cost: int) -> bool: var current := get_cell_traffic(cell) if current.is_empty(): last_error = "Unknown path cell: %s" % cell return false return configure_cell_traffic( cell, int(current[&"base_capacity"]), dynamic_cost, bool(current[&"temporary_block"]) ) func set_cell_temporary_block(cell: Vector3i, temporary_block: bool) -> bool: var current := get_cell_traffic(cell) if current.is_empty(): last_error = "Unknown path cell: %s" % cell return false return configure_cell_traffic( cell, int(current[&"base_capacity"]), int(current[&"dynamic_cost"]), temporary_block ) func get_cell_traffic(cell: Vector3i) -> Dictionary: if not _passable_cells.has(cell): return {} return (_cell_traffic.get(cell, _default_cell_traffic()) as Dictionary).duplicate(true) func get_current_density(cell: Vector3i) -> int: return int(_cell_traffic.get(cell, {}).get(&"current_density", 0)) func get_congestion_cost(cell: Vector3i) -> int: var traffic: Dictionary = _cell_traffic.get(cell, {}) if traffic.is_empty(): return 0 return maxi( 0, int(traffic[&"current_density"]) - int(traffic[&"base_capacity"]) ) * CONGESTION_COST_PER_EXCESS func get_cell_dynamic_cost(cell: Vector3i) -> int: var traffic: Dictionary = _cell_traffic.get(cell, {}) if traffic.is_empty(): return 0 return int(traffic[&"dynamic_cost"]) + get_congestion_cost(cell) func get_speed_multiplier(cell: Vector3i) -> float: var traffic: Dictionary = _cell_traffic.get(cell, {}) if traffic.is_empty(): return 1.0 var capacity := int(traffic[&"base_capacity"]) var density := int(traffic[&"current_density"]) return clampf( float(capacity) / float(maxi(capacity, density)), MINIMUM_SPEED_MULTIPLIER, 1.0 ) func project_actor_position( actor_id: StringName, cell: Vector3i, absolute_minute: int = -1 ) -> bool: if actor_id.is_empty() or not _passable_cells.has(cell): last_error = "Actor projection requires an id and a passable destination." return false if bool(_cell_traffic[cell][&"temporary_block"]): last_error = "Actor cannot enter a temporarily blocked cell." return false if absolute_minute >= 0 and _actor_progress.has(actor_id) \ and absolute_minute < int((_actor_progress[actor_id] as Dictionary)[&"last_observed_at"]): last_error = "Actor projection time cannot move backwards." return false if _actor_cells.has(actor_id) and _actor_cells[actor_id] == cell: if absolute_minute >= 0: var progress_result := record_actor_progress(actor_id, cell, absolute_minute) if not bool(progress_result[&"ok"]): return false last_error = "" return true var changed_cells: Array[Vector3i] = [] if _actor_cells.has(actor_id): var previous: Vector3i = _actor_cells[actor_id] if not _decrement_density(previous): last_error = "Actor density projection is inconsistent for: %s" % actor_id return false changed_cells.append(previous) _actor_cells[actor_id] = cell _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. _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] if not _decrement_density(previous): last_error = "Actor density projection is inconsistent for: %s" % actor_id return false _actor_cells.erase(actor_id) if clear_progress: _actor_progress.erase(actor_id) _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 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 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"]) # Request ids are tombstoned permanently; cancellation permits a new # id for the same unit but never reuse of the cancelled id. _mark_connector_traffic_changed(connector, true) last_error = "" return true last_error = "Unknown elevator request id: %s" % request_id return false func get_elevator_queue(connector_id: StringName, absolute_minute: int = 0) -> Array[Dictionary]: var result: Array[Dictionary] = [] if not _connectors.has(connector_id): return result var connector: Dictionary = _connectors[connector_id] if connector[&"movement_type"] != MOVEMENT_ELEVATOR: return result var queue: Array = (connector[&"request_queue"] as Array).duplicate(true) queue.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return _elevator_request_less_at(left, right, absolute_minute) ) for value: Variant in queue: result.append((value as Dictionary).duplicate(true)) return result func elevator_queue_size(connector_id: StringName) -> int: if not _connectors.has(connector_id): return 0 var connector: Dictionary = _connectors[connector_id] if connector[&"movement_type"] != MOVEMENT_ELEVATOR: return 0 return (connector[&"request_queue"] as Array).size() func dequeue_elevator_boarding( connector_id: StringName, current_floor: int, absolute_minute: int, available_capacity: int = -1 ) -> Array[Dictionary]: var boarded: Array[Dictionary] = [] if not _connectors.has(connector_id): last_error = "Unknown connector id: %s" % connector_id return boarded var connector: Dictionary = _connectors[connector_id] if connector[&"movement_type"] != MOVEMENT_ELEVATOR: last_error = "Connector is not an elevator: %s" % connector_id return boarded if not _connector_is_operational(connector): last_error = "Elevator is not operational: %s" % connector_id return boarded var boarding_capacity := int(connector[&"cabin_capacity"]) if available_capacity >= 0: boarding_capacity = mini(boarding_capacity, available_capacity) if boarding_capacity <= 0: last_error = "" return boarded var queue := get_elevator_queue(connector_id, absolute_minute) var remaining: Array = [] for request: Dictionary in queue: if boarded.size() < boarding_capacity and int(request[&"floor_from"]) == current_floor: boarded.append(request.duplicate(true)) else: remaining.append(request.duplicate(true)) connector[&"request_queue"] = remaining connector[&"current_floor"] = current_floor connector[&"cabin_state"] = CABIN_DOOR_OPEN if not boarded.is_empty() else CABIN_IDLE _connectors[connector_id] = connector if not boarded.is_empty(): for request: Dictionary in boarded: var unit_id: StringName = request[&"unit_id"] var active: Dictionary = _active_elevator_units.get(unit_id, {}) if not active.is_empty() and active[&"request_id"] == request[&"request_id"]: active[&"state"] = ELEVATOR_REQUEST_BOARDED _active_elevator_units[unit_id] = active _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) last_error = "" return true last_error = "Unknown active elevator request id: %s" % request_id return false func complete_elevator_trip(connector_id: StringName, request_id: StringName) -> bool: return complete_elevator_request(connector_id, request_id) func get_active_elevator_request(unit_id: StringName) -> Dictionary: if not _active_elevator_units.has(unit_id): return {} return (_active_elevator_units[unit_id] as Dictionary).duplicate(true) func _active_request_id_exists(request_id: StringName) -> bool: for unit_id: Variant in _active_elevator_units: if (_active_elevator_units[unit_id] as Dictionary)[&"request_id"] == request_id: return true return false func estimate_elevator_wait_cost(connector_id: StringName) -> int: if not _connectors.has(connector_id): return 0 var connector: Dictionary = _connectors[connector_id] if connector[&"movement_type"] != MOVEMENT_ELEVATOR: return 0 var queue_size := (connector[&"request_queue"] as Array).size() var capacity := maxi(int(connector[&"cabin_capacity"]), 1) # Boarding costs one minute per unit. Each full cabin ahead adds another # door cycle, represented here as two game minutes. var cabin_cycles := ceili(float(queue_size) / float(capacity)) return (queue_size + cabin_cycles * 2) * ORTHOGONAL_MOVE_COST func find_path( start: Vector3i, goal: Vector3i, access_layers: Variant = null, connector_access: Variant = null ) -> Array[Vector3i]: var started_usec := Time.get_ticks_usec() var path := _find_path_internal(start, goal, access_layers, connector_access) path_query_measured.emit(maxi(Time.get_ticks_usec() - started_usec, 0)) return path func _find_path_internal( start: Vector3i, goal: Vector3i, access_layers: Variant = null, connector_access: Variant = null ) -> Array[Vector3i]: var raw_layers: Variant = access_layers var raw_connector_access: Variant = connector_access if access_layers is Dictionary: var access_profile: Dictionary = access_layers raw_layers = _profile_value( access_profile, [&"allowed_layers", &"access_layers", &"access_layer", &"layer"], null ) if connector_access == null: raw_connector_access = _profile_value( access_profile, [&"connector_access", &"connector_access_ids", &"access_ids"], null ) var layer_result := _normalize_access_layers(raw_layers) if not layer_result[&"ok"]: last_error = layer_result[&"error"] last_no_route_reason = ROUTE_ACCESS_LAYER_DENIED last_path_cost = 0 return [] var connector_result := _normalize_access_ids(raw_connector_access) if not connector_result[&"ok"]: last_error = connector_result[&"error"] last_no_route_reason = ROUTE_CONNECTOR_UNAVAILABLE last_path_cost = 0 return [] var allowed_layers: Array[int] = layer_result[&"values"] var allowed_access_ids: Array[StringName] = connector_result[&"values"] if not _passable_cells.has(start): return _route_failure(ROUTE_START_NOT_PASSABLE) if not _passable_cells.has(goal): return _route_failure(ROUTE_GOAL_NOT_PASSABLE) if start.z not in allowed_layers or goal.z not in allowed_layers: return _route_failure(ROUTE_ACCESS_LAYER_DENIED) if _cell_is_temporarily_blocked(start): return _route_failure(ROUTE_START_BLOCKED) if _cell_is_temporarily_blocked(goal): return _route_failure(ROUTE_GOAL_BLOCKED) var cache_key := _cache_key(start, goal, allowed_layers, allowed_access_ids) if _path_cache.has(cache_key): var cached: Array = _path_cache[cache_key] if _path_is_currently_valid(cached, allowed_layers, allowed_access_ids): cache_hits += 1 last_no_route_reason = ROUTE_OK last_path_cost = _calculate_path_cost_for_access(cached, allowed_access_ids) last_error = "" return _copy_path(cached) _path_cache.erase(cache_key) cache_misses += 1 var path := _run_a_star(start, goal, allowed_layers, allowed_access_ids) if path.is_empty(): return _route_failure(_diagnose_no_route_reason( start, goal, allowed_layers, allowed_access_ids )) _path_cache[cache_key] = _copy_path(path) last_no_route_reason = ROUTE_OK last_path_cost = _calculate_path_cost_for_access(path, allowed_access_ids) last_error = "" return path func find_path_report( start: Vector3i, goal: Vector3i, access_layers: Variant = null, connector_access: Variant = null ) -> Dictionary: var path := find_path(start, goal, access_layers, connector_access) return { &"reachable": not path.is_empty(), &"reason": last_no_route_reason, &"cost": last_path_cost, &"path": path, &"graph_revision": graph_revision, &"traffic_revision": traffic_revision, } func calculate_path_cost(path: Array, connector_access: Variant = null) -> int: var access_result := _normalize_access_ids(connector_access) if not access_result[&"ok"]: last_error = access_result[&"error"] return -1 var total := _calculate_path_cost_for_access(path, access_result[&"values"]) last_error = "" if total >= 0 else "Path contains an unavailable edge." return total func _calculate_path_cost_for_access( path: Array, allowed_access_ids: Array[StringName] ) -> int: if path.is_empty(): return 0 var total := 0 for index: int in range(1, path.size()): var from_cell: Variant = path[index - 1] var to_cell: Variant = path[index] if not from_cell is Vector3i or not to_cell is Vector3i: return -1 var edge := _find_edge(from_cell, to_cell, allowed_access_ids) if edge.is_empty(): return -1 total += _edge_total_cost(edge) return total func invalidate_cache_for_cells(cells: Variant) -> int: var normalization := _normalize_cell_set(cells, false) if not normalization[&"ok"]: last_error = normalization[&"error"] return 0 var affected: Dictionary = normalization[&"cells"] var keys_to_remove: Array = [] for cache_key: Variant in _path_cache: var cached_path: Array = _path_cache[cache_key] if cached_path.is_empty() or _path_touches(cached_path, affected): keys_to_remove.append(cache_key) for cache_key: Variant in keys_to_remove: _path_cache.erase(cache_key) last_error = "" return keys_to_remove.size() func invalidate_local_cache(cells: Variant) -> int: return invalidate_cache_for_cells(cells) func invalidate_all_cache() -> int: var removed := _path_cache.size() _path_cache.clear() return removed func cached_path_count() -> int: return _path_cache.size() func has_cell(cell: Vector3i) -> bool: return _passable_cells.has(cell) func has_connector(connector_id: StringName) -> bool: return _connectors.has(connector_id) func get_connector(connector_id: StringName) -> Dictionary: if not _connectors.has(connector_id): return {} return (_connectors[connector_id] as Dictionary).duplicate(true) 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] return { &"found": true, &"reason": &"SAFE_WALKABLE_CELL", &"cell": target, &"distance": _recovery_distance(origin, target), &"implicit_move_applied": false, } func create_snapshot() -> Dictionary: var connector_ids: Array = _connectors.keys() connector_ids.sort_custom(Callable(self, "_string_name_less")) var saved_connectors: Array = [] for connector_id: Variant in connector_ids: var connector: Dictionary = (_connectors[connector_id] as Dictionary).duplicate(true) var queue: Array = connector[&"request_queue"] queue.sort_custom(Callable(self, "_elevator_request_base_less")) connector[&"request_queue"] = queue saved_connectors.append(connector) var cells: Array = _passable_cells.keys() cells.sort_custom(Callable(self, "_coordinate_less")) var saved_cell_traffic: Array = [] for cell_value: Variant in cells: var cell: Vector3i = cell_value var traffic: Dictionary = _cell_traffic.get(cell, _default_cell_traffic()) saved_cell_traffic.append({ &"cell": cell, &"base_capacity": int(traffic[&"base_capacity"]), &"current_density": int(traffic[&"current_density"]), &"dynamic_cost": int(traffic[&"dynamic_cost"]), &"temporary_block": bool(traffic[&"temporary_block"]), }) var actor_ids: Array = _actor_cells.keys() actor_ids.sort_custom(Callable(self, "_string_name_less")) var saved_actor_cells: Array = [] for actor_id: Variant in actor_ids: saved_actor_cells.append({ &"actor_id": StringName(actor_id), &"cell": _actor_cells[actor_id], }) var progress_ids: Array = _actor_progress.keys() progress_ids.sort_custom(Callable(self, "_string_name_less")) var saved_progress: Array = [] for actor_id: Variant in progress_ids: saved_progress.append((_actor_progress[actor_id] as Dictionary).duplicate(true)) var known_request_ids: Array = _known_elevator_request_ids.keys() known_request_ids.sort_custom(Callable(self, "_string_name_less")) var saved_known_requests: Array[StringName] = [] for request_id: Variant in known_request_ids: saved_known_requests.append(StringName(request_id)) var active_unit_ids: Array = _active_elevator_units.keys() active_unit_ids.sort_custom(Callable(self, "_string_name_less")) var saved_active_units: Array = [] for unit_id: Variant in active_unit_ids: saved_active_units.append((_active_elevator_units[unit_id] as Dictionary).duplicate(true)) 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, } 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, SNAPSHOT_VERSION]: last_error = "Unsupported PathService snapshot version: %d" % version return false 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 _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 == 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"] if not _snapshot_densities_match(next_traffic, next_actor_cells): 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 _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 _pending_graph_revision = saved_graph_revision if _passable_cells.is_empty() else -1 _traffic_loaded_without_graph = _passable_cells.is_empty() and not next_traffic.is_empty() if not _passable_cells.is_empty(): graph_revision = saved_graph_revision _rebuild_derived_graph() invalidate_all_cache() last_no_route_reason = ROUTE_OK last_path_cost = 0 last_error = "" return true func _rebuild_derived_graph() -> void: _adjacency.clear() var cells: Array = _passable_cells.keys() cells.sort_custom(Callable(self, "_coordinate_less")) for cell_value: Variant in cells: var cell: Vector3i = cell_value _adjacency[cell] = [] for cell_value: Variant in cells: var cell: Vector3i = cell_value for direction in _CARDINAL_DIRECTIONS: var neighbor := cell + direction if _passable_cells.has(neighbor): _append_edge( cell, neighbor, ORTHOGONAL_MOVE_COST, &"", &"", MOVEMENT_WALK ) var connector_ids: Array = _connectors.keys() connector_ids.sort_custom(Callable(self, "_string_name_less")) for connector_id_value: Variant in connector_ids: var connector_id: StringName = connector_id_value var connector: Dictionary = _connectors[connector_id] if not _connector_is_operational(connector): continue var from_cell: Vector3i = connector[&"from"] var to_cell: Vector3i = connector[&"to"] if not _passable_cells.has(from_cell) or not _passable_cells.has(to_cell): continue var cost: int = connector[&"cost"] var access_id: StringName = connector[&"access_id"] var movement_type: StringName = connector[&"movement_type"] _append_edge(from_cell, to_cell, cost, connector_id, access_id, movement_type) if bool(connector[&"bidirectional"]): _append_edge(to_cell, from_cell, cost, connector_id, access_id, movement_type) for cell_value: Variant in cells: var edges: Array = _adjacency[cell_value] edges.sort_custom(Callable(self, "_edge_less")) _adjacency[cell_value] = edges func _append_edge( from_cell: Vector3i, to_cell: Vector3i, cost: int, connector_id: StringName, access_id: StringName, movement_type: StringName ) -> void: var edges: Array = _adjacency[from_cell] edges.append({ &"to": to_cell, &"cost": cost, &"base_cost": cost, &"connector_id": connector_id, &"access_id": access_id, &"movement_type": movement_type, }) _adjacency[from_cell] = edges func _run_a_star( start: Vector3i, goal: Vector3i, allowed_layers: Array[int], allowed_access_ids: Array[StringName] ) -> Array[Vector3i]: if start == goal: return [start] var open_set: Dictionary = {start: true} var closed_set: Dictionary = {} var came_from: Dictionary = {} var g_score: Dictionary = {start: 0} while not open_set.is_empty(): var current := _select_best_open_node(open_set, g_score, goal) if current == goal: return _reconstruct_path(came_from, current) open_set.erase(current) closed_set[current] = true var edges: Array = _adjacency.get(current, []) for edge_value: Variant in edges: var edge: Dictionary = edge_value var neighbor: Vector3i = edge[&"to"] if neighbor.z not in allowed_layers \ or _cell_is_temporarily_blocked(neighbor) \ or not _edge_is_accessible(edge, allowed_access_ids): continue var tentative_cost: int = int(g_score[current]) + _edge_total_cost(edge) var known_cost: int = int(g_score.get(neighbor, _INFINITE_COST)) var better_parent := false if tentative_cost == known_cost and came_from.has(neighbor): better_parent = _coordinate_less(current, came_from[neighbor]) if tentative_cost > known_cost or (tentative_cost == known_cost and not better_parent): continue came_from[neighbor] = current g_score[neighbor] = tentative_cost if closed_set.has(neighbor): closed_set.erase(neighbor) open_set[neighbor] = true return [] func _select_best_open_node(open_set: Dictionary, g_score: Dictionary, goal: Vector3i) -> Vector3i: var best := Vector3i.ZERO var has_best := false var best_score := _INFINITE_COST for candidate_value: Variant in open_set: var candidate: Vector3i = candidate_value var score: int = int(g_score[candidate]) + _heuristic(candidate, goal) if not has_best or score < best_score: best = candidate best_score = score has_best = true elif score == best_score and _coordinate_less(candidate, best): best = candidate return best func _heuristic(from_cell: Vector3i, to_cell: Vector3i) -> int: # A connector may be cheaper than the geometric distance. In that case zero is # the conservative admissible heuristic; without connectors Manhattan is exact. if not _connectors.is_empty() or from_cell.z != to_cell.z: return 0 return (absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y)) * ORTHOGONAL_MOVE_COST func _reconstruct_path(came_from: Dictionary, goal: Vector3i) -> Array[Vector3i]: var reversed_path: Array[Vector3i] = [goal] var current := goal var maximum_length := _adjacency.size() + 1 while came_from.has(current) and reversed_path.size() <= maximum_length: current = came_from[current] reversed_path.append(current) reversed_path.reverse() return reversed_path func _edge_is_accessible(edge: Dictionary, allowed_access_ids: Array[StringName]) -> bool: var required_access: StringName = edge[&"access_id"] if not required_access.is_empty() and required_access not in allowed_access_ids: return false var connector_id: StringName = edge[&"connector_id"] if connector_id.is_empty(): return true return _connectors.has(connector_id) and _connector_is_operational(_connectors[connector_id]) func _edge_total_cost(edge: Dictionary) -> int: var to_cell: Vector3i = edge[&"to"] var total := int(edge.get(&"base_cost", edge.get(&"cost", ORTHOGONAL_MOVE_COST))) total += get_cell_dynamic_cost(to_cell) var connector_id: StringName = edge.get(&"connector_id", &"") if connector_id.is_empty() or not _connectors.has(connector_id): return total var connector: Dictionary = _connectors[connector_id] total += int(connector[&"dynamic_cost"]) if connector[&"movement_type"] == MOVEMENT_ELEVATOR: total += estimate_elevator_wait_cost(connector_id) var from_cell: Vector3i = connector[&"from"] var destination: Vector3i = connector[&"to"] var floor_distance := absi(destination.y - from_cell.y) total += floor_distance * int(connector[&"travel_minutes_per_floor"]) * ORTHOGONAL_MOVE_COST return total func _find_edge( from_cell: Vector3i, to_cell: Vector3i, allowed_access_ids: Variant = null ) -> Dictionary: var best: Dictionary = {} var best_cost := _INFINITE_COST for edge_value: Variant in _adjacency.get(from_cell, []): var edge: Dictionary = edge_value if edge[&"to"] != to_cell: continue if allowed_access_ids is Array and not _edge_is_accessible(edge, allowed_access_ids): continue var cost := _edge_total_cost(edge) if best.is_empty() or cost < best_cost \ or (cost == best_cost and _edge_less(edge, best)): best = edge best_cost = cost return best func _path_is_currently_valid( path: Array, allowed_layers: Array[int], allowed_access_ids: Array[StringName] ) -> bool: if path.is_empty(): return false for cell_value: Variant in path: if not cell_value is Vector3i: return false var cell: Vector3i = cell_value if not _passable_cells.has(cell) or cell.z not in allowed_layers \ or _cell_is_temporarily_blocked(cell): return false for index: int in range(1, path.size()): var edge := _find_edge(path[index - 1], path[index], allowed_access_ids) if edge.is_empty(): return false return true func _route_failure(reason: StringName) -> Array[Vector3i]: last_no_route_reason = reason last_path_cost = 0 last_error = str(reason) return [] func _cell_is_temporarily_blocked(cell: Vector3i) -> bool: var traffic: Dictionary = _cell_traffic.get(cell, {}) return not traffic.is_empty() and bool(traffic[&"temporary_block"]) func _diagnose_no_route_reason( start: Vector3i, goal: Vector3i, allowed_layers: Array[int], allowed_access_ids: Array[StringName] ) -> StringName: # Diagnose only obstacles which actually separate this start/goal pair. A # stopped lift or blocked cell in another disconnected region must not leak # into this route's reason. if _route_exists_with_overrides( start, goal, allowed_layers, allowed_access_ids, true, false ): return ROUTE_TEMPORARILY_BLOCKED if _route_exists_with_overrides( start, goal, allowed_layers, allowed_access_ids, false, true ): return ROUTE_CONNECTOR_UNAVAILABLE return ROUTE_GOAL_UNREACHABLE func _route_exists_with_overrides( start: Vector3i, goal: Vector3i, allowed_layers: Array[int], allowed_access_ids: Array[StringName], allow_blocked_cells: bool, allow_unavailable_connectors: bool ) -> bool: var frontier: Array[Vector3i] = [start] var visited: Dictionary = {start: true} var cursor := 0 var connector_ids := _sorted_name_keys(_connectors) while cursor < frontier.size(): var current: Vector3i = frontier[cursor] cursor += 1 if current == goal: return true var neighbor_set: Dictionary = {} for direction: Vector3i in _CARDINAL_DIRECTIONS: var neighbor := current + direction if _passable_cells.has(neighbor): neighbor_set[neighbor] = true for connector_id: StringName in connector_ids: var connector: Dictionary = _connectors[connector_id] var required_access: StringName = connector[&"access_id"] if not required_access.is_empty() and required_access not in allowed_access_ids: continue if not allow_unavailable_connectors and not _connector_is_operational(connector): continue var from_cell: Vector3i = connector[&"from"] var to_cell: Vector3i = connector[&"to"] if current == from_cell: neighbor_set[to_cell] = true elif bool(connector[&"bidirectional"]) and current == to_cell: neighbor_set[from_cell] = true var neighbors: Array = neighbor_set.keys() neighbors.sort_custom(Callable(self, "_coordinate_less")) for neighbor_value: Variant in neighbors: var neighbor: Vector3i = neighbor_value if visited.has(neighbor) or neighbor.z not in allowed_layers: continue if not allow_blocked_cells and _cell_is_temporarily_blocked(neighbor): continue visited[neighbor] = true frontier.append(neighbor) return false func _connector_is_operational(connector: Dictionary) -> bool: return bool(connector[&"enabled"]) \ and connector[&"operational_state"] == CONNECTOR_OPERATIONAL \ and ( connector[&"movement_type"] != MOVEMENT_ELEVATOR or connector[&"cabin_state"] != CABIN_OUTAGE ) func _ensure_cell_traffic_for_graph(cells: Dictionary, reset_missing: bool) -> void: var next_traffic: Dictionary = {} for cell_value: Variant in cells: var cell: Vector3i = cell_value if _cell_traffic.has(cell): next_traffic[cell] = (_cell_traffic[cell] as Dictionary).duplicate(true) elif reset_missing: next_traffic[cell] = _default_cell_traffic() else: next_traffic[cell] = _default_cell_traffic() _cell_traffic = next_traffic var invalid_actor_ids: Array = [] for actor_id: Variant in _actor_cells: if not cells.has(_actor_cells[actor_id]): invalid_actor_ids.append(actor_id) for actor_id: Variant in invalid_actor_ids: _actor_cells.erase(actor_id) _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): _increment_density(cell) 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_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) -> bool: var expected: Dictionary = {} for actor_id: Variant in actor_cells: 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)