Files
TheTree_mock/scripts/transport/elevator_service.gd
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
2026-09-30 23:52:24 +09:00

740 lines
36 KiB
GDScript

class_name ElevatorService
extends RefCounted
## One physical cabin per contiguous shaft. Path connectors expose reachability;
## boarding and arrival here remain authoritative for actual movement.
## Dispatch is oldest request first, batching one origin/destination at a time.
const SNAPSHOT_VERSION := 1
const PREFIX := "transport."
const REQUEST_PREFIX := "transport.trip."
const DOOR_MINUTES := 2
const QUEUE_CAPACITY := 256
var last_error := ""
var _path: PathService
var _shafts: Dictionary = {}
var _trips: Dictionary = {}
var _minute := -1
var _sequence := 0
var current_minute: int:
get: return _minute
func configure(path: PathService) -> void:
_path = path
static func configurations_from_facilities(facilities: Array, registry: DefinitionRegistry) -> Array:
var modules: Array[Dictionary] = []
for value: Variant in facilities:
if not value is Dictionary:
continue
var facility: Dictionary = value
var definition_id := StringName(str(facility.get(&"definition_id", "")))
if definition_id not in [&"guest_elevator", &"service_elevator"]:
continue
var definition := registry.get_facility(definition_id)
var origin: Variant = facility.get(&"origin")
if definition == null or not origin is Vector3i:
continue
var width := definition.footprint.x
var height := definition.footprint.y
var cells: Array = facility.get(&"occupied_cells", [])
if not cells.is_empty():
width = 1
height = 1
for cell: Vector3i in cells:
width = maxi(width, cell.x - origin.x + 1)
height = maxi(height, cell.y - origin.y + 1)
modules.append({&"origin": origin, &"width": width, &"height": height,
&"module_id": StringName(str(facility.get(&"instance_id", "")))})
modules.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var ac: Vector3i = a[&"origin"]
var bc: Vector3i = b[&"origin"]
if ac.z != bc.z: return ac.z < bc.z
if ac.x != bc.x: return ac.x < bc.x
if a[&"width"] != b[&"width"]: return a[&"width"] < b[&"width"]
return ac.y < bc.y)
var result: Array = []
for module: Dictionary in modules:
var origin: Vector3i = module[&"origin"]
var width: int = module[&"width"]
var config: Dictionary = {}
if not result.is_empty():
var previous: Dictionary = result.back()
var stops: Array = previous[&"stops"]
var top: Vector3i = stops.back()
if top.x == origin.x and top.z == origin.z and top.y + 1 == origin.y and previous[&"width"] == width:
config = previous
if config.is_empty():
config = {&"shaft_id": StringName("transport.shaft.%d.%d.%d.%d" % [origin.z, origin.x, origin.y, width]),
&"module_ids": [], &"stops": [], &"width": width,
&"capacity": 4 if origin.z == 0 else 8,
&"travel_minutes_per_floor": 10 if origin.z == 0 else 5,
&"access_id": &"" if origin.z == 0 else &"staff"}
result.append(config)
(config[&"module_ids"] as Array).append(module[&"module_id"])
for y: int in range(origin.y, origin.y + int(module[&"height"])):
(config[&"stops"] as Array).append(Vector3i(origin.x, y, origin.z))
return result
func synchronize(configurations: Array, minute: int) -> Dictionary:
if _path == null or minute < 0 or (_minute >= 0 and minute < _minute):
return _failure(&"INVALID_CLOCK", "Elevator synchronization requires a path and current time.")
var normalized := _normalize_configs(configurations)
if not normalized[&"ok"]:
return normalized
var configs: Dictionary = normalized[&"configs"]
for shaft_id: StringName in _names(_shafts):
var car: Dictionary = _shafts[shaft_id]
if configs.has(shaft_id) and configs[shaft_id] == car[&"config"]:
continue
if _shaft_busy(shaft_id):
return _failure(&"ELEVATOR_BUSY", "An occupied or moving shaft cannot be modified.")
# All new IDs must belong to us and must not overwrite an unrelated connector.
var old_ids: Dictionary = {}
for value: Variant in _shafts.values():
for connector: Dictionary in _connector_configs(value[&"config"]):
old_ids[connector[&"id"]] = true
for value: Variant in configs.values():
for connector: Dictionary in _connector_configs(value):
if _path.has_connector(connector[&"id"]) and not old_ids.has(connector[&"id"]):
return _failure(&"CONNECTOR_CONFLICT", "Elevator connector is already owned.")
var path_before := _path.create_snapshot()
var next: Dictionary = {}
for shaft_id: StringName in _names(_shafts):
var car: Dictionary = _shafts[shaft_id]
if configs.has(shaft_id) and configs[shaft_id] == car[&"config"]:
next[shaft_id] = car
continue
for connector: Dictionary in _connector_configs(car[&"config"]):
if not _path.remove_connector(connector[&"id"]):
_path.restore_snapshot(path_before)
return _failure(&"CONNECTOR_BUSY", _path.last_error)
for shaft_id: StringName in _names(configs):
if next.has(shaft_id):
continue
var config: Dictionary = configs[shaft_id]
var car := _new_car(config)
if _shafts.has(shaft_id):
car[&"operational"] = _shafts[shaft_id][&"operational"]
car[&"operator_open"] = _shafts[shaft_id][&"operator_open"]
car[&"powered"] = _shafts[shaft_id][&"powered"]
var previous_floor: int = _shafts[shaft_id][&"current_floor"]
if _has_floor(config, previous_floor): car[&"current_floor"] = previous_floor
for connector: Dictionary in _connector_configs(config):
if not _path.configure_connector(connector[&"id"], connector[&"from"], connector[&"to"],
PathService.ORTHOGONAL_MOVE_COST, true, config[&"access_id"], true, connector):
_path.restore_snapshot(path_before)
return _failure(&"CONNECTOR_CONFIGURATION_FAILED", _path.last_error)
next[shaft_id] = car
_shafts = next
if _minute < 0: _minute = minute
for shaft_id: StringName in _names(_shafts): _publish_cabin(_shafts[shaft_id])
last_error = ""
return {&"ok": true, &"shaft_count": _shafts.size()}
func request_trip(actor_id: StringName, from: Vector3i, to: Vector3i, minute: int,
access: Array[StringName] = []) -> Dictionary:
if actor_id.is_empty() or minute < _minute or minute > _minute + 1:
return _failure(&"INVALID_REQUEST", "Elevator request has invalid actor or time.")
if _trips.has(actor_id):
var existing: Dictionary = _trips[actor_id]
if existing[&"from"] == from and existing[&"to"] == to:
return {&"ok": true, &"replay": true, &"trip": existing.duplicate(true)}
return _failure(&"ACTOR_ALREADY_TRAVELLING", "Actor already owns an elevator trip.")
var selected: Dictionary = {}
for shaft_id: StringName in _names(_shafts):
var car: Dictionary = _shafts[shaft_id]
var config: Dictionary = car[&"config"]
if from != to and (config[&"stops"] as Array).has(from) and (config[&"stops"] as Array).has(to):
selected = car
break
if selected.is_empty(): return _failure(&"NO_ELEVATOR", "No shaft joins those stops.")
var config: Dictionary = selected[&"config"]
if not selected[&"operational"]: return _failure(&"ELEVATOR_STOPPED", "Elevator is stopped.")
if not StringName(config[&"access_id"]).is_empty() and not access.has(config[&"access_id"]):
return _failure(&"ACCESS_DENIED", "Elevator access is not permitted.")
if not _valid_stop(from) or not _valid_stop(to):
return _failure(&"STOP_UNAVAILABLE", "Elevator landing is unavailable.")
var connector_id := _connector_id(config[&"shaft_id"], from.y, to.y)
_sequence += 1
var request_id := StringName("transport.trip.%012d" % _sequence)
var queued := _path.enqueue_elevator_request(connector_id, {&"request_id": request_id,
&"unit_id": actor_id, &"floor_from": from.y, &"floor_to": to.y, &"enqueued_at": minute})
if not queued[&"ok"]:
return _failure(queued.get(&"reason", &"QUEUE_REJECTED"), _path.last_error)
var trip := {&"actor_id": actor_id, &"request_id": request_id, &"shaft_id": config[&"shaft_id"],
&"connector_id": connector_id, &"from": from, &"to": to,
&"enqueued_at": minute, &"state": PathService.ELEVATOR_REQUEST_QUEUED}
_trips[actor_id] = trip
last_error = ""
return {&"ok": true, &"replay": false, &"trip": trip.duplicate(true)}
## Reserve two real seats atomically; no synthetic passenger or hidden capacity.
func request_pair(first: StringName, second: StringName, from: Vector3i, to: Vector3i,
minute: int, access: Array[StringName] = []) -> Dictionary:
if first.is_empty() or second.is_empty() or first == second:
return _failure(&"INVALID_PAIR", "Two distinct passengers are required.")
if _trips.has(first) or _trips.has(second):
if _trips.has(first) and _trips.has(second) and _trips[first].get(&"companion_id", &"") == second and _trips[second].get(&"companion_id", &"") == first and _trips[first][&"from"] == from and _trips[first][&"to"] == to:
return {&"ok": true, &"replay": true, &"trip": _trips[first].duplicate(true)}
return _failure(&"ACTOR_ALREADY_TRAVELLING", "Pair member already owns another trip.")
var path_before := _path.create_snapshot()
var sequence_before := _sequence
var a := request_trip(first, from, to, minute, access)
if not a[&"ok"]:
_path.restore_snapshot(path_before)
_sequence = sequence_before
return a
var b := request_trip(second, from, to, minute, access)
if not b[&"ok"] or _shafts[_trips[first][&"shaft_id"]][&"config"][&"capacity"] < 2:
_path.restore_snapshot(path_before)
_trips.erase(first)
_trips.erase(second)
_sequence = sequence_before
return _failure(&"PAIR_REJECTED", "Both passengers must fit in the same cabin queue.")
_trips[first][&"companion_id"] = second
_trips[second][&"companion_id"] = first
return {&"ok": true, &"replay": false, &"trip": _trips[first].duplicate(true)}
func advance_one_minute(minute: int) -> Array[Dictionary]:
var arrivals: Array[Dictionary] = []
if minute == _minute: return arrivals
if minute != _minute + 1:
last_error = "Elevators must advance in consecutive minutes."
return arrivals
_minute = minute
for shaft_id: StringName in _names(_shafts):
var car: Dictionary = _shafts[shaft_id]
if not car[&"operational"]: continue
var unavailable := false
for actor_id: StringName in car[&"boarded"]:
if not _valid_stop(_trips[actor_id][&"to"]): unavailable = true
if unavailable:
arrivals.append_array(set_operational(shaft_id, false, minute))
continue
if car[&"phase"] == &"IDLE":
_dispatch(car)
elif int(car[&"remaining_minutes"]) > 0:
car[&"remaining_minutes"] -= 1
if int(car[&"remaining_minutes"]) == 0:
_finish_phase(car, arrivals)
_publish_cabin(car)
last_error = ""
return arrivals
func get_actor_trip(actor_id: StringName) -> Dictionary:
return (_trips.get(actor_id, {}) as Dictionary).duplicate(true)
func cancel_actor(actor_id: StringName) -> bool:
var partner: StringName = _trips.get(actor_id, {}).get(&"companion_id", &"")
if not partner.is_empty():
if _trips[actor_id][&"state"] == PathService.ELEVATOR_REQUEST_BOARDED:
last_error = "An onboard pair must alight together at a real landing."
return false
var path_before := _path.create_snapshot()
var trips_before := _trips.duplicate(true)
if not _cancel_single(actor_id) or not _cancel_single(partner):
_path.restore_snapshot(path_before)
_trips = trips_before
return false
return true
return _cancel_single(actor_id)
func _cancel_single(actor_id: StringName) -> bool:
if not _trips.has(actor_id): return true
var trip: Dictionary = _trips[actor_id]
if trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED:
if not _path.complete_elevator_request(trip[&"connector_id"], trip[&"request_id"]):
last_error = _path.last_error
return false
(_shafts[trip[&"shaft_id"]][&"boarded"] as Array).erase(actor_id)
_trips.erase(actor_id)
last_error = ""
return true
if not _path.cancel_elevator_request(trip[&"connector_id"], trip[&"request_id"]):
last_error = _path.last_error
return false
_trips.erase(actor_id)
last_error = ""
return true
func set_operator_open(shaft_id: StringName, enabled: bool, minute: int) -> Array[Dictionary]:
if not _shafts.has(shaft_id) or minute < _minute or minute > _minute + 1:
last_error = "Unknown shaft or invalid operator change time."
return []
_shafts[shaft_id][&"operator_open"] = enabled
return _apply_operational(shaft_id, enabled and _shafts[shaft_id][&"powered"], minute)
func set_powered(shaft_id: StringName, powered: bool, minute: int) -> Array[Dictionary]:
if not _shafts.has(shaft_id) or minute < _minute or minute > _minute + 1:
last_error = "Unknown shaft or invalid power change time."
return []
_shafts[shaft_id][&"powered"] = powered
return _apply_operational(shaft_id, powered and _shafts[shaft_id][&"operator_open"], minute)
func set_operational(shaft_id: StringName, operational: bool, minute: int) -> Array[Dictionary]:
return set_powered(shaft_id, operational, minute)
func _apply_operational(shaft_id: StringName, operational: bool, minute: int) -> Array[Dictionary]:
var arrivals: Array[Dictionary] = []
if not _shafts.has(shaft_id) or minute < _minute or minute > _minute + 1:
last_error = "Unknown shaft or invalid operational change time."
return arrivals
var car: Dictionary = _shafts[shaft_id]
if car[&"operational"] == operational: return arrivals
if not operational:
var landing: Variant = _nearest_valid_stop(car)
if landing == null and not (car[&"boarded"] as Array).is_empty():
last_error = "Occupied elevator has no safe landing; cabin remains held."
# No legal teleportation destination exists. The caller must repair a
# landing; movement remains held and requests remain owned.
car[&"operational"] = false
for actor_id: StringName in _names(_trips):
if not _trips.has(actor_id): continue
var waiting: Dictionary = _trips[actor_id]
if waiting[&"shaft_id"] == shaft_id and waiting[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED and _cancel_single(actor_id):
arrivals.append({&"actor_id": actor_id, &"request_id": waiting[&"request_id"], &"cell": waiting[&"from"], &"emergency": false, &"cancelled": true})
_publish_cabin(car)
return arrivals
for actor_id: StringName in _names(_trips):
var trip: Dictionary = _trips[actor_id]
if trip[&"shaft_id"] != shaft_id: continue
var boarded: bool = trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED
if boarded:
if not _path.complete_elevator_request(trip[&"connector_id"], trip[&"request_id"]): continue
else:
if not _path.cancel_elevator_request(trip[&"connector_id"], trip[&"request_id"]): continue
arrivals.append({&"actor_id": actor_id, &"request_id": trip[&"request_id"],
&"cell": landing if boarded else trip[&"from"], &"emergency": boarded, &"cancelled": not boarded})
_trips.erase(actor_id)
car[&"boarded"] = []
if landing != null: car[&"current_floor"] = (landing as Vector3i).y
_idle(car)
if operational and car[&"phase"] != &"IDLE":
car[&"phase_started_at"] = _minute - (int(car[&"phase_total_minutes"]) - int(car[&"remaining_minutes"]))
car[&"operational"] = operational
_publish_cabin(car)
last_error = ""
return arrivals
func owns_connector(connector_id: StringName) -> bool:
return not get_shaft_for_connector(connector_id).is_empty()
func get_shaft_for_connector(connector_id: StringName) -> StringName:
for shaft_id: StringName in _names(_shafts):
for connector: Dictionary in _connector_configs(_shafts[shaft_id][&"config"]):
if connector[&"id"] == connector_id: return shaft_id
return &""
func get_report() -> Dictionary:
var rows: Array[Dictionary] = []
for shaft_id: StringName in _names(_shafts):
var car: Dictionary = _shafts[shaft_id]
var row: Dictionary = car.duplicate(true)
row.merge(car[&"config"], true)
row[&"visual_floor"] = _visual_floor(car)
row[&"waiting_count"] = 0
row[&"passenger_count"] = (car[&"boarded"] as Array).size()
row[&"passenger_ids"] = car[&"boarded"].duplicate()
for trip: Dictionary in _trips.values():
if trip[&"shaft_id"] == shaft_id and trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED:
row[&"waiting_count"] += 1
rows.append(row)
return {&"current_minute": _minute, &"shafts": rows, &"trips": _ordered_trips()}
func create_snapshot() -> Dictionary:
var cars: Array[Dictionary] = []
for shaft_id: StringName in _names(_shafts): cars.append(_shafts[shaft_id].duplicate(true))
return {&"version": SNAPSHOT_VERSION, &"current_minute": _minute,
&"request_sequence": _sequence, &"shafts": cars, &"trips": _ordered_trips()}
func restore_snapshot(snapshot: Dictionary) -> bool:
if _path == null or not _exact_keys(snapshot, [&"version", &"current_minute", &"request_sequence", &"shafts", &"trips"]):
return _invalid("Elevator snapshot shape is invalid.")
if snapshot[&"version"] != SNAPSHOT_VERSION or not snapshot[&"current_minute"] is int or snapshot[&"current_minute"] < 0 \
or not snapshot[&"request_sequence"] is int or snapshot[&"request_sequence"] < 0 \
or not snapshot[&"shafts"] is Array or not snapshot[&"trips"] is Array:
return _invalid("Elevator snapshot header is invalid.")
var next_shafts: Dictionary = {}
var next_trips: Dictionary = {}
var seen_modules: Dictionary = {}
var expected_connectors: Dictionary = {}
var boarded_owners: Dictionary = {}
for value: Variant in snapshot[&"shafts"]:
if not value is Dictionary: return _invalid("Elevator cabin must be a Dictionary.")
var car: Dictionary = value
if not _exact_keys(car, _new_car({&"stops": [Vector3i.ZERO]}).keys()) or not car[&"config"] is Dictionary:
return _invalid("Elevator cabin fields are invalid.")
var normalized := _normalize_configs([car[&"config"]])
if not normalized[&"ok"]: return false
var config: Dictionary = car[&"config"]
var shaft_id := StringName(config[&"shaft_id"])
if next_shafts.has(shaft_id): return _invalid("Duplicate elevator shaft.")
for module_id: StringName in config[&"module_ids"]:
if seen_modules.has(module_id): return _invalid("Module belongs to multiple shafts.")
seen_modules[module_id] = true
if not _validate_car(car, int(snapshot[&"current_minute"])): return false
for actor: Variant in car[&"boarded"]:
if not (actor is String or actor is StringName) or str(actor).is_empty() or boarded_owners.has(StringName(actor)):
return _invalid("Elevator cabin actor is invalid or duplicated.")
boarded_owners[StringName(actor)] = shaft_id
for expected: Dictionary in _connector_configs(config):
var connector_id: StringName = expected[&"id"]
var live := _path.get_connector(connector_id)
for key: StringName in expected:
if not live.has(key) or live[key] != expected[key]: return _invalid("Path elevator configuration disagrees with cabin.")
if live[&"cabin_state"] != _path_cabin_state(car) \
or live[&"operational_state"] != (PathService.CONNECTOR_OPERATIONAL if car[&"operational"] else PathService.CONNECTOR_STOPPED):
return _invalid("Path elevator operation disagrees with cabin.")
expected_connectors[connector_id] = true
next_shafts[shaft_id] = car.duplicate(true)
for value: Variant in snapshot[&"trips"]:
if not value is Dictionary: return _invalid("Elevator trip must be a Dictionary.")
var trip: Dictionary = value
var keys: Array = [&"actor_id", &"request_id", &"shaft_id", &"connector_id", &"from", &"to", &"enqueued_at", &"state"]
if trip.has(&"companion_id"):
if not (trip[&"companion_id"] is String or trip[&"companion_id"] is StringName) or String(trip[&"companion_id"]).is_empty(): return _invalid("Invalid companion identity.")
keys.append(&"companion_id")
if not _exact_keys(trip, keys):
return _invalid("Elevator trip fields are invalid.")
for key: StringName in [&"actor_id", &"request_id", &"shaft_id", &"connector_id", &"state"]:
if not (trip[key] is String or trip[key] is StringName) or str(trip[key]).is_empty(): return _invalid("Elevator trip names are invalid.")
var actor_id := StringName(trip[&"actor_id"])
var shaft_id := StringName(trip[&"shaft_id"])
if next_trips.has(actor_id) or not next_shafts.has(shaft_id): return _invalid("Elevator trip actor or shaft is duplicated/missing.")
var config: Dictionary = next_shafts[shaft_id][&"config"]
if not trip[&"from"] is Vector3i or not trip[&"to"] is Vector3i \
or trip[&"from"] == trip[&"to"] or not (config[&"stops"] as Array).has(trip[&"from"]) or not (config[&"stops"] as Array).has(trip[&"to"]):
return _invalid("Elevator trip stops are invalid.")
if trip[&"connector_id"] != _connector_id(shaft_id, trip[&"from"].y, trip[&"to"].y) \
or not trip[&"enqueued_at"] is int or trip[&"enqueued_at"] < 0 or trip[&"enqueued_at"] > snapshot[&"current_minute"]:
return _invalid("Elevator trip connector/time is invalid.")
var active := _path.get_active_elevator_request(actor_id)
if active.get(&"request_id") != trip[&"request_id"] or active.get(&"connector_id") != trip[&"connector_id"] or active.get(&"state") != trip[&"state"]:
return _invalid("Elevator trip and Path active owner disagree.")
if trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED:
if boarded_owners.get(actor_id) != shaft_id: return _invalid("Boarded trip has no cabin seat.")
var car: Dictionary = next_shafts[shaft_id]
if trip[&"to"].y != car[&"target_floor"] or trip[&"from"].y != car[&"boarding_floor"]:
return _invalid("Boarded trip and cabin destination disagree.")
elif trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED:
if boarded_owners.has(actor_id): return _invalid("Queued trip also owns a cabin seat.")
var found := false
for queued: Dictionary in _path.get_elevator_queue(trip[&"connector_id"]):
if queued[&"request_id"] == trip[&"request_id"]:
found = queued[&"unit_id"] == actor_id and queued[&"floor_from"] == trip[&"from"].y and queued[&"floor_to"] == trip[&"to"].y and queued[&"enqueued_at"] == trip[&"enqueued_at"] and queued[&"priority"] == PathService.ELEVATOR_PRIORITY_NORMAL
if not found: return _invalid("Queued trip details disagree with Path queue.")
else: return _invalid("Unknown elevator trip state.")
next_trips[actor_id] = trip.duplicate(true)
for actor_id: StringName in next_trips:
var trip: Dictionary = next_trips[actor_id]
var partner: StringName = trip.get(&"companion_id", &"")
if partner.is_empty(): continue
if partner == actor_id or not next_trips.has(partner): return _invalid("Missing companion trip.")
var other: Dictionary = next_trips[partner]
if other.get(&"companion_id", &"") != actor_id: return _invalid("Companion ownership is not reciprocal.")
for key: StringName in [&"shaft_id", &"connector_id", &"from", &"to", &"state", &"enqueued_at"]:
if trip[key] != other[key]: return _invalid("Companion trip was split.")
for actor_id: StringName in boarded_owners:
if not next_trips.has(actor_id): return _invalid("Cabin seat has no matching trip.")
var path_snapshot := _path.create_snapshot()
for connector: Dictionary in path_snapshot[&"connectors"]:
if str(connector[&"id"]).begins_with(PREFIX) and not expected_connectors.has(connector[&"id"]):
return _invalid("Path contains an orphan transport connector.")
for active: Dictionary in path_snapshot[&"active_elevator_units"]:
if expected_connectors.has(active[&"connector_id"]) and not next_trips.has(active[&"unit_id"]):
return _invalid("Path contains an orphan transport passenger.")
for request_id: Variant in path_snapshot[&"known_elevator_request_ids"]:
var name := str(request_id)
if not name.begins_with(REQUEST_PREFIX): continue
var suffix := name.trim_prefix(REQUEST_PREFIX)
if not suffix.is_valid_int() or int(suffix) <= 0 or int(suffix) > snapshot[&"request_sequence"]:
return _invalid("Elevator sequence would reuse an active or retired request ID.")
var all_configs: Array = []
for car: Dictionary in next_shafts.values(): all_configs.append(car[&"config"])
if not _normalize_configs(all_configs)[&"ok"]: return false
_shafts = next_shafts
_trips = next_trips
_minute = snapshot[&"current_minute"]
_sequence = snapshot[&"request_sequence"]
last_error = ""
return true
func _dispatch(car: Dictionary) -> void:
var candidates: Array[Dictionary] = []
for trip: Dictionary in _trips.values():
if trip[&"shaft_id"] == car[&"config"][&"shaft_id"] and trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED and trip[&"enqueued_at"] <= _minute:
candidates.append(trip)
candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if a[&"enqueued_at"] != b[&"enqueued_at"]: return a[&"enqueued_at"] < b[&"enqueued_at"]
return str(a[&"actor_id"]) < str(b[&"actor_id"]))
if candidates.is_empty():
_idle(car)
return
var trip: Dictionary = candidates.front()
car[&"boarding_floor"] = trip[&"from"].y
car[&"target_floor"] = trip[&"to"].y
if car[&"current_floor"] != car[&"boarding_floor"]:
_start_phase(car, &"PICKUP", absi(int(car[&"current_floor"]) - int(car[&"boarding_floor"])) * int(car[&"config"][&"travel_minutes_per_floor"]))
else:
_board(car)
func _board(car: Dictionary) -> void:
var config: Dictionary = car[&"config"]
var connector_id := _connector_id(config[&"shaft_id"], car[&"boarding_floor"], car[&"target_floor"])
var companions: Dictionary = {}
for actor: StringName in _trips:
if _trips[actor].has(&"companion_id"): companions[actor] = _trips[actor][&"companion_id"]
var boarded := _path.dequeue_elevator_boarding(connector_id, car[&"current_floor"], _minute, config[&"capacity"], companions)
car[&"boarded"] = []
for request: Dictionary in boarded:
var actor_id: StringName = request[&"unit_id"]
if not _trips.has(actor_id): continue
_trips[actor_id][&"state"] = PathService.ELEVATOR_REQUEST_BOARDED
(car[&"boarded"] as Array).append(actor_id)
if boarded.is_empty():
_idle(car)
return
_start_phase(car, &"BOARDING", DOOR_MINUTES + boarded.size())
func _finish_phase(car: Dictionary, arrivals: Array[Dictionary]) -> void:
match car[&"phase"]:
&"PICKUP":
car[&"current_floor"] = car[&"boarding_floor"]
_board(car)
&"BOARDING":
_start_phase(car, &"TRAVEL", absi(int(car[&"target_floor"]) - int(car[&"current_floor"])) * int(car[&"config"][&"travel_minutes_per_floor"]))
&"TRAVEL":
car[&"current_floor"] = car[&"target_floor"]
_start_phase(car, &"ALIGHTING", DOOR_MINUTES + (car[&"boarded"] as Array).size())
&"ALIGHTING":
for actor_id: StringName in car[&"boarded"]:
var trip: Dictionary = _trips[actor_id]
if _path.complete_elevator_request(trip[&"connector_id"], trip[&"request_id"]):
arrivals.append({&"actor_id": actor_id, &"request_id": trip[&"request_id"], &"cell": trip[&"to"], &"emergency": false, &"cancelled": false})
_trips.erase(actor_id)
car[&"boarded"] = []
_idle(car)
_dispatch(car)
func _new_car(config: Dictionary) -> Dictionary:
var floor: int = config[&"stops"][0].y
return {&"config": config.duplicate(true), &"operational": true, &"operator_open": true, &"powered": true, &"current_floor": floor,
&"target_floor": floor, &"boarding_floor": floor, &"phase": &"IDLE",
&"remaining_minutes": 0, &"phase_total_minutes": 0, &"phase_started_at": -1,
&"phase_origin_floor": floor, &"phase_passenger_count": 0, &"boarded": []}
func _start_phase(car: Dictionary, phase: StringName, duration: int) -> void:
car[&"phase"] = phase
car[&"remaining_minutes"] = duration
car[&"phase_total_minutes"] = duration
car[&"phase_started_at"] = _minute
car[&"phase_origin_floor"] = car[&"current_floor"]
car[&"phase_passenger_count"] = car[&"boarded"].size()
func _idle(car: Dictionary) -> void:
car[&"phase"] = &"IDLE"
car[&"target_floor"] = car[&"current_floor"]
car[&"boarding_floor"] = car[&"current_floor"]
car[&"phase_origin_floor"] = car[&"current_floor"]
car[&"phase_started_at"] = -1
car[&"remaining_minutes"] = 0
car[&"phase_total_minutes"] = 0
car[&"phase_passenger_count"] = 0
func _visual_floor(car: Dictionary) -> float:
if car[&"phase"] not in [&"PICKUP", &"TRAVEL"]: return float(car[&"current_floor"])
var destination: int = car[&"boarding_floor"] if car[&"phase"] == &"PICKUP" else car[&"target_floor"]
return lerpf(float(car[&"phase_origin_floor"]), float(destination),
1.0 - float(car[&"remaining_minutes"]) / maxf(float(car[&"phase_total_minutes"]), 1.0))
func _nearest_valid_stop(car: Dictionary) -> Variant:
var floor := _visual_floor(car)
var nearest: Variant = null
var distance := INF
for cell: Vector3i in car[&"config"][&"stops"]:
if not _valid_stop(cell): continue
var candidate := absf(float(cell.y) - floor)
if candidate < distance:
nearest = cell
distance = candidate
return nearest
func _valid_stop(cell: Vector3i) -> bool:
var traffic := _path.get_cell_traffic(cell)
return not traffic.is_empty() and not bool(traffic.get(&"temporary_block", false))
func _path_cabin_state(car: Dictionary) -> StringName:
if not car[&"operational"]: return PathService.CABIN_OUTAGE
if car[&"phase"] in [&"BOARDING", &"ALIGHTING"]: return PathService.CABIN_DOOR_OPEN
if car[&"phase"] in [&"PICKUP", &"TRAVEL"]:
var target: int = car[&"boarding_floor"] if car[&"phase"] == &"PICKUP" else car[&"target_floor"]
return PathService.CABIN_MOVING_UP if target > int(car[&"current_floor"]) else PathService.CABIN_MOVING_DOWN
return PathService.CABIN_IDLE
func _publish_cabin(car: Dictionary) -> void:
for connector: Dictionary in _connector_configs(car[&"config"]):
_path.set_connector_operational_state(connector[&"id"], PathService.CONNECTOR_OPERATIONAL if car[&"operational"] else PathService.CONNECTOR_STOPPED)
_path.set_elevator_cabin_state(connector[&"id"], _path_cabin_state(car))
func _shaft_busy(shaft_id: StringName) -> bool:
if _shafts[shaft_id][&"phase"] != &"IDLE": return true
for trip: Dictionary in _trips.values():
if trip[&"shaft_id"] == shaft_id: return true
return false
static func _connector_id(shaft_id: StringName, from: int, to: int) -> StringName:
return StringName("%s.link.%d.%d" % [shaft_id, mini(from, to), maxi(from, to)])
func _connector_configs(config: Dictionary) -> Array[Dictionary]:
var result: Array[Dictionary] = []
var stops: Array = config[&"stops"]
for i: int in range(stops.size()):
for j: int in range(i + 1, stops.size()):
result.append({&"id": _connector_id(config[&"shaft_id"], stops[i].y, stops[j].y),
&"from": stops[i], &"to": stops[j], &"movement_type": PathService.MOVEMENT_ELEVATOR,
&"cost": PathService.ORTHOGONAL_MOVE_COST, &"bidirectional": true, &"access_id": config[&"access_id"],
&"enabled": true, &"cabin_capacity": config[&"capacity"], &"queue_capacity": QUEUE_CAPACITY,
&"travel_minutes_per_floor": config[&"travel_minutes_per_floor"], &"dynamic_cost": 0})
return result
func _normalize_configs(configurations: Array) -> Dictionary:
var configs: Dictionary = {}
var modules: Dictionary = {}
var landing_owners: Dictionary = {}
for value: Variant in configurations:
if not value is Dictionary: return _failure(&"INVALID_CONFIG", "Shaft configuration must be a Dictionary.")
var config: Dictionary = value
if not _exact_keys(config, [&"shaft_id", &"module_ids", &"stops", &"width", &"capacity", &"travel_minutes_per_floor", &"access_id"]):
return _failure(&"INVALID_CONFIG", "Shaft configuration fields are invalid.")
for key: StringName in [&"shaft_id", &"access_id"]:
if not (config[key] is String or config[key] is StringName): return _failure(&"INVALID_CONFIG", "Shaft names are invalid.")
var shaft_id := StringName(config[&"shaft_id"])
if not str(shaft_id).begins_with("transport.shaft.") or configs.has(shaft_id): return _failure(&"INVALID_CONFIG", "Shaft ID is invalid or duplicated.")
if not config[&"stops"] is Array or config[&"stops"].size() < 2 or not config[&"module_ids"] is Array or config[&"module_ids"].is_empty():
return _failure(&"INVALID_CONFIG", "Shaft must have modules and at least two stops.")
for key: StringName in [&"capacity", &"travel_minutes_per_floor", &"width"]:
if not config[key] is int or config[key] <= 0: return _failure(&"INVALID_CONFIG", "Shaft dimensions/timing are invalid.")
var first: Variant = config[&"stops"][0]
if not first is Vector3i or first.z not in [0, 1]: return _failure(&"INVALID_CONFIG", "Shaft stop layer is invalid.")
if config[&"capacity"] != (4 if first.z == 0 else 8) or config[&"travel_minutes_per_floor"] != (10 if first.z == 0 else 5):
return _failure(&"INVALID_CONFIG", "Shaft capacity/speed differs from its layer.")
for i: int in range(config[&"stops"].size()):
var cell: Variant = config[&"stops"][i]
if not cell is Vector3i or cell != Vector3i(first.x, first.y + i, first.z) or landing_owners.has(cell):
return _failure(&"INVALID_CONFIG", "Shaft stops must form a unique contiguous column.")
landing_owners[cell] = true
for module: Variant in config[&"module_ids"]:
if not (module is String or module is StringName) or str(module).is_empty() or modules.has(StringName(module)):
return _failure(&"INVALID_CONFIG", "Shaft module is empty or duplicated.")
modules[StringName(module)] = true
configs[shaft_id] = config.duplicate(true)
return {&"ok": true, &"configs": configs}
func _validate_car(car: Dictionary, minute: int) -> bool:
if not car[&"operational"] is bool or not car[&"operator_open"] is bool or not car[&"powered"] is bool or car[&"operational"] != (car[&"operator_open"] and car[&"powered"]) or not car[&"boarded"] is Array \
or car[&"boarded"].size() > car[&"config"][&"capacity"]:
return _invalid("Elevator cabin operation/capacity is invalid.")
for key: StringName in [&"current_floor", &"target_floor", &"boarding_floor", &"phase_origin_floor", &"remaining_minutes", &"phase_total_minutes", &"phase_started_at", &"phase_passenger_count"]:
if not car[key] is int: return _invalid("Elevator cabin clocks and floors must be integers.")
for key: StringName in [&"current_floor", &"target_floor", &"boarding_floor", &"phase_origin_floor"]:
if not _has_floor(car[&"config"], car[key]): return _invalid("Elevator cabin floor is absent from its shaft.")
if car[&"phase_passenger_count"] < car[&"boarded"].size() or car[&"phase_passenger_count"] > car[&"config"][&"capacity"]:
return _invalid("Elevator phase passenger count is invalid.")
var phase := StringName(str(car[&"phase"]))
if phase not in [&"IDLE", &"PICKUP", &"BOARDING", &"TRAVEL", &"ALIGHTING"]: return _invalid("Unknown elevator phase.")
if phase == &"IDLE":
return _valid_or_error(car[&"boarded"].is_empty() and car[&"phase_passenger_count"] == 0 and car[&"remaining_minutes"] == 0 and car[&"phase_total_minutes"] == 0 and car[&"phase_started_at"] == -1 and car[&"target_floor"] == car[&"current_floor"] and car[&"boarding_floor"] == car[&"current_floor"] and car[&"phase_origin_floor"] == car[&"current_floor"], "Idle cabin contains active phase data.")
if car[&"remaining_minutes"] <= 0 or car[&"remaining_minutes"] > car[&"phase_total_minutes"] or car[&"phase_started_at"] < 0 or car[&"phase_started_at"] > minute:
return _invalid("Elevator phase countdown is invalid.")
# Operational time is exact; an outage with no valid floor may hold a phase.
if car[&"operational"] and car[&"phase_total_minutes"] - car[&"remaining_minutes"] != minute - car[&"phase_started_at"]:
return _invalid("Elevator phase countdown disagrees with simulation time.")
var expected := 0
if phase in [&"BOARDING", &"ALIGHTING"]:
expected = DOOR_MINUTES + int(car[&"phase_passenger_count"])
else:
var destination: int = car[&"boarding_floor"] if phase == &"PICKUP" else car[&"target_floor"]
expected = absi(destination - int(car[&"phase_origin_floor"])) * int(car[&"config"][&"travel_minutes_per_floor"])
if car[&"phase_total_minutes"] != expected or expected <= 0: return _invalid("Elevator phase duration does not match distance/passengers.")
if phase == &"PICKUP" and not car[&"boarded"].is_empty(): return _invalid("Empty pickup cabin contains passengers.")
if car[&"phase_origin_floor"] != car[&"current_floor"]: return _invalid("Elevator phase origin disagrees with logical floor.")
if phase == &"ALIGHTING" and car[&"current_floor"] != car[&"target_floor"]: return _invalid("Alighting cabin is at the wrong floor.")
if phase in [&"BOARDING", &"TRAVEL"] and car[&"current_floor"] != car[&"boarding_floor"]: return _invalid("Boarding/travel origin disagrees with pickup floor.")
return true
func _has_floor(config: Dictionary, floor: int) -> bool:
for cell: Vector3i in config[&"stops"]:
if cell.y == floor: return true
return false
func _ordered_trips() -> Array[Dictionary]:
var trips: Array[Dictionary] = []
for actor_id: StringName in _names(_trips): trips.append(_trips[actor_id].duplicate(true))
return trips
func _names(values: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for value: Variant in values: result.append(StringName(value))
result.sort_custom(func(a: StringName, b: StringName) -> bool: return str(a) < str(b))
return result
func _exact_keys(value: Dictionary, keys: Array) -> bool:
if value.size() != keys.size(): return false
for key: Variant in keys:
if not value.has(key): return false
return true
func _failure(reason: StringName, message: String) -> Dictionary:
last_error = message
return {&"ok": false, &"reason": reason, &"error": message}
func _invalid(message: String) -> bool:
last_error = message
return false
func _valid_or_error(valid: bool, message: String) -> bool:
if not valid: last_error = message
return valid