Files
TheTree_mock/scripts/world/path_service.gd
T
TakeshiandClaude Opus 5.5 0754f412a8 P14-03: Price whole-shaft elevator waits in A* route choice
Riders queued for any floor pair in a shaft now delay every pair: the
elevator service publishes the rest of the shaft queue as each connector's
dynamic cost, refreshed on trip changes and each cabin minute, and restore
rejects saves whose wait disagrees with their trips. A two-shaft fixture with
real guests shows rush diversion, shared cabins within capacity, recovery,
route loss and warp-exit congestion.

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

3137 lines
116 KiB
GDScript

class_name PathService
extends RefCounted
signal path_query_measured(duration_us: int)
## Deterministic path graph derived from WorldService-owned passable cells.
##
## Coordinates are Vector3i(x, y, layer), where layer is FRONT (0) or BACK (1).
## This service owns connector configuration, but not the authoritative world cells.
const FRONT := 0
const BACK := 1
const ORTHOGONAL_MOVE_COST := 100
const DEFAULT_CONNECTOR_COST := 150
const DEFAULT_CELL_CAPACITY := 4
const CONGESTION_COST_PER_EXCESS := 75
const MINIMUM_SPEED_MULTIPLIER := 0.5
const STUCK_THRESHOLD_MINUTES := 30
const ELEVATOR_STARVATION_MINUTES := 60
const SNAPSHOT_VERSION := 5
const SHAFT_SNAPSHOT_VERSION := 4
const ELEVATOR_INDEX_SNAPSHOT_VERSION := 3
const MAX_VERTICAL_BLOCKER_OWNERS := 4_096
const MAX_VERTICAL_BLOCKER_CELLS := 262_144
const LEGACY_SNAPSHOT_VERSION := 1
const TRAFFIC_SNAPSHOT_VERSION := 2
const ELEVATOR_REQUEST_QUEUED := &"QUEUED"
const ELEVATOR_REQUEST_BOARDED := &"BOARDED"
const MOVEMENT_WALK := &"WALK"
const MOVEMENT_CONNECTOR := &"CONNECTOR"
const MOVEMENT_ELEVATOR := &"ELEVATOR"
const CONNECTOR_OPERATIONAL := &"OPERATIONAL"
const CONNECTOR_STOPPED := &"STOPPED"
const CONNECTOR_OUTAGE := &"OUTAGE"
const CONNECTOR_MAINTENANCE := &"MAINTENANCE"
const CABIN_IDLE := &"IDLE"
const CABIN_MOVING_UP := &"MOVING_UP"
const CABIN_MOVING_DOWN := &"MOVING_DOWN"
const CABIN_DOOR_OPEN := &"DOOR_OPEN"
const CABIN_OUTAGE := &"OUTAGE"
const ELEVATOR_PRIORITY_RESCUE := 0
const ELEVATOR_PRIORITY_SECURITY := 10
const ELEVATOR_PRIORITY_PROTAGONIST := 20
const ELEVATOR_PRIORITY_VIP := 30
const ELEVATOR_PRIORITY_NORMAL := 40
const ROUTE_OK := &"OK"
const ROUTE_START_NOT_PASSABLE := &"START_NOT_PASSABLE"
const ROUTE_GOAL_NOT_PASSABLE := &"GOAL_NOT_PASSABLE"
const ROUTE_START_BLOCKED := &"START_TEMPORARILY_BLOCKED"
const ROUTE_GOAL_BLOCKED := &"GOAL_TEMPORARILY_BLOCKED"
const ROUTE_ACCESS_LAYER_DENIED := &"ACCESS_LAYER_DENIED"
const ROUTE_CONNECTOR_UNAVAILABLE := &"CONNECTOR_UNAVAILABLE"
const ROUTE_TEMPORARILY_BLOCKED := &"TEMPORARILY_BLOCKED"
const ROUTE_GOAL_UNREACHABLE := &"GOAL_UNREACHABLE"
const _CARDINAL_DIRECTIONS: Array[Vector3i] = [
Vector3i(-1, 0, 0),
Vector3i(0, -1, 0),
Vector3i(0, 1, 0),
Vector3i(1, 0, 0),
]
const _INFINITE_COST := 9223372036854775807
var graph_revision := 0
var traffic_revision := 0
var cache_hits := 0
var cache_misses := 0
var last_error := ""
var last_no_route_reason: StringName = ROUTE_OK
var last_path_cost := 0
var _passable_cells: Dictionary = {}
var _sorted_passable_cell_keys: Array = []
var _connectors: Dictionary = {}
var _adjacency: Dictionary = {}
var _vertical_walk_blockers: Dictionary = {}
var _vertical_walk_blocked_cells: Dictionary = {}
var _path_cache: Dictionary = {}
# Derived state limited to one synchronous candidate projection, never serialized.
var _query_batch: Dictionary = {}
var _cell_traffic: Dictionary = {}
var _actor_cells: Dictionary = {}
var _actor_progress: Dictionary = {}
var _vertical_walk_requires_connector := false
var _transit_actor_ids: Dictionary = {}
var _known_elevator_request_ids: Dictionary = {}
var _active_elevator_units: Dictionary = {}
var _pending_graph_revision := -1
var _traffic_loaded_without_graph := false
func rebuild_graph(passable_cells: Variant) -> bool:
var normalization := _normalize_cell_set(passable_cells)
if not normalization[&"ok"]:
last_error = normalization[&"error"]
return false
var next_cells: Dictionary = normalization[&"cells"]
if _traffic_loaded_without_graph:
for cell_value: Variant in _cell_traffic:
if not next_cells.has(cell_value):
last_error = "Snapshot traffic references a cell absent from the authoritative world: %s" % cell_value
return false
for actor_id: Variant in _actor_cells:
if not next_cells.has(_actor_cells[actor_id]):
last_error = "Snapshot actor projection references a non-walkable cell: %s" % actor_id
return false
if next_cells == _passable_cells:
_ensure_cell_traffic_for_graph(next_cells, not _traffic_loaded_without_graph)
_traffic_loaded_without_graph = false
if _pending_graph_revision >= 0:
graph_revision = _pending_graph_revision
_pending_graph_revision = -1
invalidate_all_cache()
last_error = ""
return true
_passable_cells = next_cells
_ensure_cell_traffic_for_graph(next_cells, not _traffic_loaded_without_graph)
_traffic_loaded_without_graph = false
_rebuild_derived_graph()
if _pending_graph_revision >= 0:
graph_revision = _pending_graph_revision
_pending_graph_revision = -1
invalidate_all_cache()
else:
_mark_graph_changed()
last_error = ""
return true
## A shaft is always physical topology, independent of cabin availability.
## Owners may overlap; removing one owner does not clear another owner's cells.
func set_vertical_walk_blockers(owner_id: StringName, cells: Array[Vector3i]) -> bool:
if not _vertical_blocker_owner_is_valid(owner_id):
last_error = "Vertical walk blockers require a bounded, nonempty owner id."
return false
if cells.is_empty() or cells.size() > MAX_VERTICAL_BLOCKER_CELLS:
last_error = "Vertical walk blockers require a bounded, nonempty cell set."
return false
var normalization := _normalize_cell_set(cells)
if not bool(normalization[&"ok"]) or (normalization[&"cells"] as Dictionary).size() != cells.size():
last_error = "Vertical walk blocker cells must be valid and unique."
return false
for cell: Vector3i in cells:
if not _passable_cells.has(cell):
last_error = "Vertical walk blocker cells must belong to the current graph."
return false
var canonical: Array[Vector3i] = cells.duplicate()
canonical.sort_custom(Callable(self, "_coordinate_less"))
if _vertical_walk_blockers.get(owner_id, []) == canonical:
last_error = ""
return true
var candidate := _vertical_walk_blockers.duplicate(true)
candidate[owner_id] = canonical
var count := 0
for owned_cells: Array in candidate.values():
count += owned_cells.size()
if candidate.size() > MAX_VERTICAL_BLOCKER_OWNERS or count > MAX_VERTICAL_BLOCKER_CELLS:
last_error = "Vertical walk blocker ownership capacity exceeded."
return false
_vertical_walk_blockers = candidate
_rebuild_vertical_walk_blocked_cells()
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func remove_vertical_walk_blockers(owner_id: StringName) -> bool:
if not _vertical_walk_blockers.has(owner_id):
last_error = "Unknown vertical walk blocker owner."
return false
_vertical_walk_blockers.erase(owner_id)
_rebuild_vertical_walk_blocked_cells()
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func get_vertical_walk_blockers() -> Dictionary:
return _vertical_walk_blockers.duplicate(true)
func set_vertical_walk_requires_connector(required: bool) -> bool:
if _vertical_walk_requires_connector == required:
last_error = ""
return true
_vertical_walk_requires_connector = required
_mark_graph_changed()
last_error = ""
return true
func get_vertical_walk_requires_connector() -> bool:
return _vertical_walk_requires_connector
func get_transit_actor_ids() -> Array[StringName]:
return _sorted_name_keys(_transit_actor_ids)
func is_actor_in_transit(actor_id: StringName) -> bool:
return _transit_actor_ids.has(actor_id)
func _rebuild_vertical_walk_blocked_cells() -> void:
_vertical_walk_blocked_cells.clear()
for cells: Array in _vertical_walk_blockers.values():
for cell: Vector3i in cells:
_vertical_walk_blocked_cells[cell] = true
func _ordinary_walk_edge_allowed(from_cell: Vector3i, to_cell: Vector3i) -> bool:
return from_cell.y == to_cell.y or (
not _vertical_walk_blocked_cells.has(from_cell)
and not _vertical_walk_blocked_cells.has(to_cell)
)
func _vertical_blocker_owner_is_valid(owner_id: StringName) -> bool:
var value := String(owner_id)
return not value.is_empty() and value.length() <= 128 and value == value.strip_edges()
func configure_connector(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
cost := DEFAULT_CONNECTOR_COST,
bidirectional := true,
access_id: StringName = &"",
enabled := true,
options: Dictionary = {}
) -> bool:
var raw_configuration := options.duplicate(true)
raw_configuration.merge({
&"id": connector_id,
&"from": from_cell,
&"to": to_cell,
&"cost": cost,
&"bidirectional": bidirectional,
&"access_id": access_id,
&"enabled": enabled,
}, true)
var normalized := _normalize_connector(raw_configuration)
if not normalized[&"ok"]:
last_error = normalized[&"error"]
return false
var configuration: Dictionary = normalized[&"connector"]
if _connectors.has(connector_id) and _connectors[connector_id] == configuration:
last_error = ""
return true
for unit_id: Variant in _active_elevator_units:
var active: Dictionary = _active_elevator_units[unit_id]
if active[&"connector_id"] == connector_id:
last_error = "Cannot reconfigure a connector with active elevator requests."
return false
var proposed_connectors := _connectors.duplicate(true)
proposed_connectors[connector_id] = configuration
var index_result := _derive_elevator_indexes_from_connectors(proposed_connectors)
if not index_result[&"ok"]:
last_error = index_result[&"error"]
return false
for request_id: Variant in index_result[&"known_request_ids"]:
if _known_elevator_request_ids.has(request_id) \
and not _active_request_id_exists(StringName(request_id)):
last_error = "Connector configuration reuses a retired elevator request id."
return false
var previous_configuration: Dictionary = _connectors.get(connector_id, {})
_connectors[connector_id] = configuration
var next_active: Dictionary = index_result[&"active_units"]
for unit_id: Variant in _active_elevator_units:
var active: Dictionary = _active_elevator_units[unit_id]
if active[&"state"] == ELEVATOR_REQUEST_BOARDED:
next_active[unit_id] = active.duplicate(true)
_active_elevator_units = next_active
_refresh_transit_actor_ids()
for request_id: Variant in index_result[&"known_request_ids"]:
_known_elevator_request_ids[request_id] = true
_rebuild_connector_endpoint_edges(previous_configuration, configuration)
_mark_graph_changed()
last_error = ""
return true
func add_connector(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
cost := DEFAULT_CONNECTOR_COST,
bidirectional := true,
access_id: StringName = &""
) -> bool:
return configure_connector(
connector_id,
from_cell,
to_cell,
cost,
bidirectional,
access_id,
true
)
func configure_elevator(
connector_id: StringName,
from_cell: Vector3i,
to_cell: Vector3i,
options: Dictionary = {},
access_id: StringName = &""
) -> bool:
var elevator_options := options.duplicate(true)
elevator_options[&"movement_type"] = MOVEMENT_ELEVATOR
var cost := int(_dictionary_value(elevator_options, &"cost", DEFAULT_CONNECTOR_COST))
var bidirectional := bool(_dictionary_value(elevator_options, &"bidirectional", true))
var enabled := bool(_dictionary_value(elevator_options, &"enabled", true))
return configure_connector(
connector_id,
from_cell,
to_cell,
cost,
bidirectional,
access_id,
enabled,
elevator_options
)
func set_connectors(configurations: Array) -> bool:
var normalized_collection := _normalize_connector_collection(configurations)
if not normalized_collection[&"ok"]:
last_error = normalized_collection[&"error"]
return false
var next_connectors: Dictionary = normalized_collection[&"connectors"]
if next_connectors == _connectors:
last_error = ""
return true
for request_id: Variant in normalized_collection[&"known_request_ids"]:
if _known_elevator_request_ids.has(request_id) \
and not _active_request_id_exists(StringName(request_id)):
last_error = "Connector set reuses a retired elevator request id."
return false
for unit_id: Variant in _active_elevator_units:
if (_active_elevator_units[unit_id] as Dictionary)[&"state"] == ELEVATOR_REQUEST_BOARDED:
last_error = "Cannot replace connector set while an elevator trip is boarded."
return false
_connectors = next_connectors
_active_elevator_units = normalized_collection[&"active_units"]
_refresh_transit_actor_ids()
for request_id: Variant in normalized_collection[&"known_request_ids"]:
_known_elevator_request_ids[request_id] = true
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func remove_connector(connector_id: StringName) -> bool:
if not _connectors.has(connector_id):
return false
for unit_id: Variant in _active_elevator_units:
if (_active_elevator_units[unit_id] as Dictionary)[&"connector_id"] == connector_id:
last_error = "Cannot remove a connector with active elevator requests: %s" % connector_id
return false
_connectors.erase(connector_id)
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
func set_connector_enabled(connector_id: StringName, enabled: bool) -> bool:
if not _connectors.has(connector_id):
last_error = "Unknown connector id: %s" % connector_id
return false
var connector: Dictionary = _connectors[connector_id]
if bool(connector[&"enabled"]) == enabled:
last_error = ""
return true
connector[&"enabled"] = enabled
_connectors[connector_id] = connector
_rebuild_derived_graph()
_mark_graph_changed()
last_error = ""
return true
## 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 []