Connect physical elevators and staff commutes to construction, saves, and cutaway UI

This commit is contained in:
Takeshi
2026-09-09 12:58:40 +09:00
parent 4d72de8973
commit 0f12c748e9
46 changed files with 3267 additions and 165 deletions
@@ -468,6 +468,12 @@ func demolish_facility(
})
_facilities.erase(instance_id)
# A completed construction job owns its live facility. Once demolition has
# committed, retire both records so snapshots cannot retain an orphaned
# COMPLETED job. Sequence counters and emitted domain events retain history.
var source_job_id := StringName(instance.get(&"source_job_id", &""))
_jobs.erase(source_job_id)
_last_progress_minutes.erase(source_job_id)
_mutation_in_progress = false
facility_demolished.emit(instance_id, refund)
return CommandResultType.succeeded(0, {
+168
View File
@@ -0,0 +1,168 @@
class_name StaffCommuteService
extends RefCounted
## Commutes derive from the saved work plan, Shift and current actor position.
## Runtime owns movement credit, Path projection, elevator rides and events.
## This adapter deliberately owns no additional snapshot payload.
const ROUTE_BLOCKED := &"COMMUTE_ROUTE_BLOCKED"
const WORKPLACE_MISSING := &"COMMUTE_WORKPLACE_MISSING"
const WAITING_SHIFT := &"WAITING_FOR_SHIFT"
const MOVING_TO_WORK := &"MOVING_TO_WORK"
var _runtime: Variant
func configure(runtime: Variant) -> StaffCommuteService:
_runtime = runtime
_runtime.staff_service.configure_attendance_driver(Callable(self, "can_attend_shift"))
return self
## A porter checks in once at the warehouse, then remains dispatchable at the
## end of a delivery. Fixed-post workers return before resuming desk service.
func can_attend_shift(staff: StaffRuntime, shift: ShiftInstance, _minute: int) -> bool:
if _runtime == null or staff == null or shift == null:
return false
if _in_transit(staff.staff_id) and (staff.current_task_id.is_empty() or shift.actual_started_at < 0):
return false
if shift.actual_started_at >= 0 and (staff.role_id == &"porter" or not staff.current_task_id.is_empty()):
return true
var facility: FacilityInstance = _runtime.facility_service.get_instance(shift.assigned_facility_id)
return is_at_facility(staff, facility)
## Physical attendance is separate from dispatch availability. A porter away
## with goods does not provide staff capacity to an empty warehouse.
func is_present(staff: StaffRuntime, facility: FacilityInstance) -> bool:
if _runtime == null or staff == null or facility == null \
or staff.assigned_facility_id != facility.instance_id \
or staff.state != StaffRuntime.State.AVAILABLE or not staff.can_take_shift():
return false
if _in_transit(staff.staff_id):
return false
if not _runtime.staff_service.requires_attendance(staff.staff_id):
return true
return is_at_facility(staff, facility)
func tick(absolute_minute: int) -> Dictionary:
if _runtime == null:
return {&"ok": false, &"reason": &"NOT_CONFIGURED"}
var moved: Array[StringName] = []
var waiting: Array[StringName] = []
for staff: StaffRuntime in _runtime.staff_service.get_staff_members():
if not _runtime.staff_service.requires_attendance(staff.staff_id) \
or staff.state != StaffRuntime.State.COMMUTING \
or not staff.can_take_shift() or not staff.current_task_id.is_empty():
continue
var shift: ShiftInstance = _runtime.staff_service.get_shift(staff.shift_id)
if shift == null or (not shift.contains_minute(absolute_minute) and not shift.is_commute_window(absolute_minute)):
continue
if _in_transit(staff.staff_id):
_wait(staff, absolute_minute, &"ELEVATOR_WAIT")
waiting.append(staff.staff_id)
continue
var facility: FacilityInstance = _runtime.facility_service.get_instance(shift.assigned_facility_id)
if facility == null:
_wait(staff, absolute_minute, WORKPLACE_MISSING)
waiting.append(staff.staff_id)
continue
if is_at_facility(staff, facility):
staff.absence_reason = &""
_wait(staff, absolute_minute, WAITING_SHIFT if shift.is_commute_window(absolute_minute) else &"COMMUTE_ARRIVED")
waiting.append(staff.staff_id)
continue
var access := find_facility_access_path(staff.current_cell, facility)
if not bool(access.get(&"reachable", false)):
_wait(staff, absolute_minute, ROUTE_BLOCKED)
waiting.append(staff.staff_id)
continue
staff.absence_reason = &""
var result: Dictionary = _runtime.advance_staff_commute(staff.staff_id, access[&"cell"], absolute_minute)
if bool(result.get(&"moved", false)):
moved.append(staff.staff_id)
else:
waiting.append(staff.staff_id)
return {&"ok": true, &"moved_staff_ids": moved, &"waiting_staff_ids": waiting}
func get_report() -> Dictionary:
var rows: Array[Dictionary] = []
if _runtime == null:
return {&"staff": rows}
for staff: StaffRuntime in _runtime.staff_service.get_staff_members():
if not _runtime.staff_service.requires_attendance(staff.staff_id):
continue
var shift: ShiftInstance = _runtime.staff_service.get_shift(staff.shift_id)
var facility: FacilityInstance = _runtime.facility_service.get_instance(staff.assigned_facility_id)
var reason := staff.absence_reason
var target := staff.current_cell
if _in_transit(staff.staff_id):
reason = &"ELEVATOR_WAIT"
if facility != null:
var access := find_facility_access_path(staff.current_cell, facility)
target = access.get(&"cell", facility.origin)
if staff.state == StaffRuntime.State.COMMUTING and reason.is_empty():
reason = WAITING_SHIFT if is_at_facility(staff, facility) else MOVING_TO_WORK
rows.append({
&"staff_id": staff.staff_id, &"state": StaffRuntime.State.keys()[staff.state],
&"reason": reason, &"facility_id": staff.assigned_facility_id,
&"current_cell": staff.current_cell, &"target_cell": target,
&"present": is_present(staff, facility),
&"late_minutes": shift.late_minutes if shift != null else 0,
})
return {&"staff": rows}
func find_facility_access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary:
if _runtime == null or facility == null:
return {&"reachable": false}
var candidates: Dictionary = {}
var occupied := facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
for cell: Vector3i in occupied:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
if _runtime.path_service.has_cell(cell + delta):
candidates[cell + delta] = true
var ordered: Array = candidates.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var best: Array[Vector3i] = []
var best_cost := 9223372036854775807
for cell: Vector3i in ordered:
var path: Array[Vector3i] = _runtime.path_service.find_path(start, cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
var cost: int = _runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1
if cost >= 0 and (best.is_empty() or cost < best_cost):
best = path
best_cost = cost
return {&"reachable": not best.is_empty(), &"path": best,
&"cell": best[-1] if not best.is_empty() else facility.origin,
&"steps": maxi(0, best.size() - 1)}
static func is_at_facility(staff: StaffRuntime, facility: FacilityInstance) -> bool:
if staff == null or facility == null:
return false
var occupied := facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
for cell: Vector3i in occupied:
if cell.z == staff.current_cell.z and cell.y == staff.current_cell.y \
and absi(cell.x - staff.current_cell.x) <= 1:
return true
return false
func _wait(staff: StaffRuntime, minute: int, reason: StringName) -> void:
if reason in [ROUTE_BLOCKED, WORKPLACE_MISSING]:
staff.absence_reason = reason
_runtime.advance_staff_commute(staff.staff_id, staff.current_cell, minute, reason)
func _in_transit(staff_id: StringName) -> bool:
return _runtime != null and _runtime.has_method("is_staff_in_transit") \
and bool(_runtime.is_staff_in_transit(staff_id))
@@ -0,0 +1 @@
uid://ddcf3c4qyyysc
+42 -5
View File
@@ -49,6 +49,7 @@ var _item_reserver := Callable()
var _item_releaser := Callable()
var _travel_estimator := Callable()
var _external_task_driver := Callable()
var _attendance_driver := Callable()
func configure_callbacks(
@@ -69,6 +70,17 @@ func configure_external_task_driver(driver: Callable = Callable()) -> void:
_external_task_driver = driver
## Runtime attendance uses the employee's real position. Component consumers
## and legacy one-off shifts retain their existing scheduling contract.
func configure_attendance_driver(driver: Callable = Callable()) -> void:
_attendance_driver = driver
func requires_attendance(staff_id: StringName) -> bool:
return _attendance_driver.is_valid() and _work_assignments.has(staff_id) \
and not String(_work_assignments[staff_id][&"facility_id"]).is_empty()
func is_externally_driven_task(task_id: StringName) -> bool:
return _external_task_driver.is_valid() and bool(_external_task_driver.call(task_id))
@@ -461,6 +473,17 @@ func set_work_assignment(staff_id: StringName, facility_id: StringName, template
if shift.cancelled or shift.end_at <= minute:
continue
var replacement := ShiftInstanceType.from_snapshot(shift.create_snapshot())
# Attendance can contain gaps, so clamping by scheduled duration alone
# cannot remove an already recorded paused-edit boundary minute. The
# current AVAILABLE employee recorded that minute; its replacement plan
# records it only if the employee is physically at the new workplace.
if requires_attendance(staff_id) and minute == current_minute \
and staff.state == StaffRuntimeType.State.AVAILABLE \
and staff.shift_id == shift.shift_instance_id \
and shift.contains_minute(minute) and shift.worked_minutes > 0:
replacement.worked_minutes -= 1
if replacement.worked_minutes == 0 and replacement.overtime_minutes == 0:
replacement.actual_started_at = -1
if replacement.start_at >= minute:
replacement.cancelled = true
else:
@@ -496,6 +519,8 @@ func set_work_assignment(staff_id: StringName, facility_id: StringName, template
staff.shift_id = &""
staff.assigned_facility_id = &""
staff.is_overtime = false
if String(staff.absence_reason).begins_with("COMMUTE_"):
staff.absence_reason = &""
staff.transition_to(StaffRuntimeType.State.OFF_DUTY, &"WORK_ASSIGNMENT_CHANGED", minute)
if new_shift != null:
_shifts[new_shift.shift_instance_id] = new_shift
@@ -1379,6 +1404,8 @@ func _update_shift_states(absolute_minute: int, only_staff: StringName = &"", re
task_reopened.emit(current_task.task_id, &"SHIFT_ENDED")
staff.current_task_id = &""
staff.is_overtime = false
if String(staff.absence_reason).begins_with("COMMUTE_"):
staff.absence_reason = &""
staff.shift_id = &""
if staff.assigned_facility_id == shift.assigned_facility_id:
staff.assigned_facility_id = &""
@@ -1410,16 +1437,26 @@ func _update_shift_states(absolute_minute: int, only_staff: StringName = &"", re
continue
if shift.contains_minute(absolute_minute):
staff.is_overtime = false
var attendance_required := requires_attendance(staff.staff_id)
var attended := not attendance_required or bool(_attendance_driver.call(staff, shift, absolute_minute))
if attendance_required and attended and String(staff.absence_reason).begins_with("COMMUTE_"):
staff.absence_reason = &""
if staff.current_task_id.is_empty() and staff.state in [
StaffRuntimeType.State.OFF_DUTY,
StaffRuntimeType.State.COMMUTING,
StaffRuntimeType.State.AVAILABLE,
]:
staff.transition_to(StaffRuntimeType.State.AVAILABLE, &"SHIFT_STARTED", absolute_minute)
shift_state_changed.emit(staff.staff_id, shift.shift_instance_id, staff.state)
if record_duty:
var next_state := StaffRuntimeType.State.AVAILABLE if attended else StaffRuntimeType.State.COMMUTING
if staff.state != next_state:
var reason := &"SHIFT_STARTED" if attended else (&"RETURN_TO_POST" if shift.actual_started_at >= 0 else &"SHIFT_COMMUTE")
staff.transition_to(next_state, reason, absolute_minute)
shift_state_changed.emit(staff.staff_id, shift.shift_instance_id, staff.state)
if record_duty and attended:
shift.record_duty_minute(absolute_minute)
if absolute_minute >= shift.start_at + 15 and staff.state == StaffRuntimeType.State.COMMUTING:
shift.mark_late(absolute_minute, DEFAULT_LATE_REASON)
if absolute_minute >= shift.start_at + 15 and staff.state == StaffRuntimeType.State.COMMUTING \
and (not attendance_required or shift.actual_started_at < 0):
var late_reason := staff.absence_reason if attendance_required and not staff.absence_reason.is_empty() else DEFAULT_LATE_REASON
shift.mark_late(absolute_minute, late_reason)
func _find_relevant_shift(staff_id: StringName, absolute_minute: int) -> ShiftInstance:
+6 -3
View File
@@ -403,7 +403,7 @@ func _access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary:
return {&"reachable": false}
var cells: Dictionary = {}
for cell: Vector3i in facility.occupied_cells:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
if _runtime.path_service.has_cell(cell + delta):
cells[cell + delta] = true
if cells.is_empty() and _runtime.path_service.has_cell(facility.origin):
@@ -414,10 +414,13 @@ func _access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary:
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var best: Array[Vector3i] = []
var best_cost := 9223372036854775807
for cell: Vector3i in ordered:
var path: Array[Vector3i] = _runtime.path_service.find_path(start, cell, [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
if not path.is_empty() and (best.is_empty() or path.size() < best.size()):
var cost: int = _runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1
if cost >= 0 and (best.is_empty() or cost < best_cost):
best = path
best_cost = cost
return {&"reachable": not best.is_empty(), &"cell": best[-1] if not best.is_empty() else facility.origin, &"steps": maxi(0, best.size() - 1)}
@@ -557,6 +560,6 @@ static func validate_crosslinks(requests: Dictionary, staff: StaffService, inven
static func _cell_touches_facility(cell: Vector3i, facility: FacilityInstance) -> bool:
for occupied: Vector3i in facility.occupied_cells:
if cell.z == occupied.z and absi(cell.x - occupied.x) + absi(cell.y - occupied.y) <= 1:
if cell.z == occupied.z and cell.y == occupied.y and absi(cell.x - occupied.x) <= 1:
return true
return cell == facility.origin
+678
View File
@@ -0,0 +1,678 @@
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)}
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:
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):
var waiting: Dictionary = _trips[actor_id]
if waiting[&"shaft_id"] == shaft_id and waiting[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED and cancel_actor(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
if not _exact_keys(trip, [&"actor_id", &"request_id", &"shaft_id", &"connector_id", &"from", &"to", &"enqueued_at", &"state"]):
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 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 boarded := _path.dequeue_elevator_boarding(connector_id, car[&"current_floor"], _minute, config[&"capacity"])
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
@@ -0,0 +1 @@
uid://cckw41o08w2xx
+39 -5
View File
@@ -44,11 +44,7 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
var w := rect.size.x
var top := rect.position.y
if id in [&"guest_elevator", &"service_elevator"]:
canvas.draw_rect(inner, Color("455855") if not back else Color("506277"))
canvas.draw_rect(Rect2(mid - p, top + p, 2 * p, rect.size.y - 2 * p), Color("243c3c"))
canvas.draw_rect(Rect2(inner.position.x + 2*p, top + 4*p, inner.size.x - 4*p, inner.size.y - 6*p), Color("84978b"))
canvas.draw_line(Vector2(mid, top+4*p), Vector2(mid, rect.end.y-3*p), INK, p)
canvas.draw_rect(Rect2(mid-2*p, top+2*p, 4*p, p), Color("f1c774"))
elevator_shaft(canvas, rect, p, back)
return
if id in [&"guest_corridor", &"service_corridor", &"staff_door"]:
if id == &"staff_door":
@@ -105,6 +101,44 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
table(canvas, Vector2(mid-6*p, floor_y-7*p), p)
## Static shaft rails remain when a car is stopped or the transport service is
## unavailable. The car itself is drawn once from its authoritative trip state.
static func elevator_shaft(canvas: CanvasItem, rect: Rect2, p: float, back: bool) -> void:
canvas.draw_rect(rect, INK)
canvas.draw_rect(rect.grow(-p), Color("3e514e") if not back else Color("404f63"))
canvas.draw_rect(Rect2(rect.position.x+2*p, rect.position.y, 2*p, rect.size.y), Color("83958a"))
canvas.draw_rect(Rect2(rect.end.x-4*p, rect.position.y, 2*p, rect.size.y), Color("83958a"))
canvas.draw_rect(Rect2(rect.get_center().x-p, rect.position.y, 2*p, rect.size.y), Color("283a39"))
for row: int in range(2):
var floor_y := rect.position.y + float(row+1)*rect.size.y*0.5
canvas.draw_rect(Rect2(rect.position.x, floor_y-4*p, rect.size.x, 3*p), Color("586c62") if not back else Color("526177"))
canvas.draw_rect(Rect2(rect.position.x, floor_y-p, rect.size.x, p), INK)
canvas.draw_rect(Rect2(rect.position.x+p, floor_y-7*p, p, 2*p), Color("dcc08b"))
canvas.draw_rect(Rect2(rect.end.x-2*p, floor_y-7*p, p, 2*p), Color("dcc08b"))
static func elevator_cabin(canvas: CanvasItem, rect: Rect2, p: float, back: bool, operational: bool) -> void:
canvas.draw_rect(rect, INK)
canvas.draw_rect(rect.grow(-p), Color("9daa8d") if not back else Color("879eaa"))
canvas.draw_rect(Rect2(rect.position+Vector2(2,3)*p,rect.size-Vector2(4,5)*p),Color("556e62") if not back else Color("4d6472"))
canvas.draw_rect(Rect2(rect.position.x+p,rect.end.y-3*p,rect.size.x-2*p,2*p),WOOD_LIGHT)
canvas.draw_rect(Rect2(rect.position.x+2*p,rect.position.y+p,rect.size.x-4*p,p),CREAM)
canvas.draw_rect(Rect2(rect.get_center().x-p,rect.position.y-p,2*p,p),Color("e2bd6c") if operational else Color("ba7157"))
static func elevator_gate(canvas: CanvasItem, rect: Rect2, p: float, doors_open: bool) -> void:
# The cutaway keeps passengers visible through the iron gate during travel.
var rail := Color("bfbb9a")
canvas.draw_rect(Rect2(rect.position.x,rect.position.y,2*p,rect.size.y),rail)
canvas.draw_rect(Rect2(rect.end.x-2*p,rect.position.y,2*p,rect.size.y),rail)
if doors_open:
return
var gate_y := rect.end.y-7*p
canvas.draw_rect(Rect2(rect.position.x+2*p,gate_y,rect.size.x-4*p,p),rail)
for x: int in range(int(rect.position.x+5*p),int(rect.end.x-3*p),maxi(int(6*p),1)):
canvas.draw_rect(Rect2(x,rect.position.y+4*p,p,rect.size.y-7*p),Color("a9aa91",0.8))
static func window(canvas: CanvasItem, at: Vector2, p: float) -> void:
canvas.draw_rect(Rect2(at, Vector2(7,9)*p), WOOD)
canvas.draw_rect(Rect2(at+Vector2.ONE*p, Vector2(5,6)*p), Color("9ab9b3"))
@@ -37,8 +37,6 @@ const SUPPORTED_KINDS: Array[StringName] = [
KIND_GUEST_CORRIDOR,
KIND_SERVICE_CORRIDOR,
KIND_STAFF_DOOR,
]
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = [
KIND_GUEST_ELEVATOR,
KIND_SERVICE_ELEVATOR,
]
@@ -50,6 +48,8 @@ const KNOWN_KINDS: Array[StringName] = [
KIND_SERVICE_ELEVATOR,
]
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = []
const VERTICAL_KINDS: Array[StringName] = [KIND_GUEST_ELEVATOR, KIND_SERVICE_ELEVATOR]
const REASON_UNSUPPORTED_VERTICAL_TOPOLOGY: StringName = &"UNSUPPORTED_VERTICAL_TOPOLOGY"
const QUARANTINE_TRANSACTION_ROLLBACK: StringName = &"TRANSACTION_ROLLBACK_UNCERTAIN"
const QUARANTINE_RESTORE_ROLLBACK: StringName = &"RESTORE_ROLLBACK_UNCERTAIN"
@@ -60,6 +60,7 @@ const QUARANTINE_REASONS: Array[StringName] = [
const ROUTE_ENTITY_PREFIX := "infra.route:"
const CONNECTOR_PREFIX := "infra.connector:"
const SHAFT_OWNER_PREFIX := "infra.shaft:"
const ADAPTER_PROJECT_REQUEST_PREFIX := "infra.project:"
const ADAPTER_UNPROJECT_REQUEST_PREFIX := "infra.unproject:"
const STAFF_ACCESS_ID: StringName = &"staff"
@@ -168,16 +169,6 @@ func project_facility(
var canonical_input: Dictionary = input_result[&"input"]
var definition_input: Dictionary = canonical_input[&"definition"]
var kind: StringName = definition_input[&"id"]
if kind in UNSUPPORTED_VERTICAL_KINDS:
return _failure(
CommandResultType.Code.VALIDATION_FAILED,
"Vertical facility projection is unsupported until ordinary adjacent walk edges can be suppressed safely.",
{
&"reason": REASON_UNSUPPORTED_VERTICAL_TOPOLOGY,
&"definition_id": kind,
&"unchanged": true,
}
)
if kind not in SUPPORTED_KINDS:
return _failure(CommandResultType.Code.INVALID_PAYLOAD, "Facility definition is not an infrastructure proxy.")
var request_id := StringName(request_id_value)
@@ -197,6 +188,7 @@ func project_facility(
return _failure(CommandResultType.Code.DUPLICATE_COMMAND_ID, "Facility instance is already projected.")
var record := _projection_from_project_input(canonical_input)
if _source.has_entity(record[&"route_entity_id"]) \
or _path.get_vertical_walk_blockers().has(StringName(SHAFT_OWNER_PREFIX + String(instance_id))) \
or (not StringName(record[&"connector_id"]).is_empty() and _path.has_connector(record[&"connector_id"])):
return _failure(CommandResultType.Code.DUPLICATE_COMMAND_ID, "Stable infrastructure ID collides with existing authoritative state.")
if not _dependencies_match_current_state():
@@ -244,6 +236,11 @@ func project_facility(
dependency_capture,
"Staff-door connector projection failed or callback tampering was detected."
)
if kind in VERTICAL_KINDS:
var shaft_cells: Array[Vector3i] = []
shaft_cells.assign(record[&"cells"])
if not _path.set_vertical_walk_blockers(StringName(SHAFT_OWNER_PREFIX + String(instance_id)), shaft_cells) or not _owned_state_matches(guard_state):
return _rollback_failure(owned_before, dependency_capture, "Elevator shaft projection failed or callback tampering was detected.")
var candidate_projections := _projections.duplicate(true)
candidate_projections[instance_id] = record.duplicate(true)
if not _dependencies_match_projection_map(candidate_projections) \
@@ -332,6 +329,9 @@ func unproject_facility(request_id_value: Variant, instance_id_value: Variant) -
dependency_capture,
"Staff-door connector removal failed or callback tampering was detected."
)
if record[&"kind"] in VERTICAL_KINDS:
if not _path.remove_vertical_walk_blockers(StringName(SHAFT_OWNER_PREFIX + String(instance_id))) or not _owned_state_matches(guard_state):
return _rollback_failure(owned_before, dependency_capture, "Elevator shaft unprojection failed or callback tampering was detected.")
var route_result := _adapter.remove_route(
StringName(ADAPTER_UNPROJECT_REQUEST_PREFIX + String(request_id)),
StringName(record[&"route_entity_id"])
@@ -977,6 +977,14 @@ func _projection_live_matches(record: Dictionary) -> bool:
if connector_id.is_empty() or not _path.has_connector(connector_id):
return false
return _path.get_connector(connector_id) == _expected_staff_connector(record)
if record[&"kind"] in VERTICAL_KINDS:
var blockers := _path.get_vertical_walk_blockers()
var owner_id := StringName(SHAFT_OWNER_PREFIX + String(record[&"instance_id"]))
if not blockers.has(owner_id):
return false
var cells: Array = (blockers[owner_id] as Array).duplicate()
cells.sort_custom(_cell_less)
return connector_id.is_empty() and cells == record[&"cells"]
return connector_id.is_empty()
@@ -1009,16 +1017,22 @@ static func _has_managed_namespace_collision(
) -> bool:
var expected_routes: Dictionary = {}
var expected_connectors: Dictionary = {}
var expected_shafts: Dictionary = {}
for record_value: Variant in projections.values():
if not record_value is Dictionary:
return true
var record: Dictionary = record_value
expected_routes[record[&"route_entity_id"]] = true
if record[&"kind"] in VERTICAL_KINDS:
expected_shafts[StringName(SHAFT_OWNER_PREFIX + String(record[&"instance_id"]))] = true
if not StringName(record[&"connector_id"]).is_empty():
expected_connectors[record[&"connector_id"]] = true
for entity_id: StringName in source_service.get_entity_ids(source_service.CATEGORY_ROUTE):
if String(entity_id).begins_with(ROUTE_ENTITY_PREFIX) and not expected_routes.has(entity_id):
return true
for owner_id: Variant in path_service.get_vertical_walk_blockers():
if String(owner_id).begins_with(SHAFT_OWNER_PREFIX) and not expected_shafts.has(owner_id):
return true
var path_snapshot := path_service.create_snapshot()
var connector_rows: Variant = path_snapshot.get(&"connectors", null)
if not connector_rows is Array:
+159 -6
View File
@@ -17,7 +17,10 @@ const CONGESTION_COST_PER_EXCESS := 75
const MINIMUM_SPEED_MULTIPLIER := 0.25
const STUCK_THRESHOLD_MINUTES := 30
const ELEVATOR_STARVATION_MINUTES := 60
const SNAPSHOT_VERSION := 3
const 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
@@ -74,6 +77,8 @@ var last_path_cost := 0
var _passable_cells: Dictionary = {}
var _connectors: Dictionary = {}
var _adjacency: Dictionary = {}
var _vertical_walk_blockers: Dictionary = {}
var _vertical_walk_blocked_cells: Dictionary = {}
var _path_cache: Dictionary = {}
var _cell_traffic: Dictionary = {}
var _actor_cells: Dictionary = {}
@@ -124,6 +129,79 @@ func rebuild_graph(passable_cells: Variant) -> bool:
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 _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,
@@ -995,6 +1073,28 @@ func get_connector(connector_id: StringName) -> Dictionary:
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
func get_step_connector(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> StringName:
return StringName(get_edge_report(from_cell, to_cell, connector_access).get(&"connector_id", &""))
func get_neighbors(cell: Vector3i) -> Array:
if not _adjacency.has(cell):
return []
@@ -1179,6 +1279,9 @@ func create_snapshot() -> Dictionary:
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,
@@ -1189,14 +1292,18 @@ func create_snapshot() -> Dictionary:
&"actor_progress": saved_progress,
&"known_elevator_request_ids": saved_known_requests,
&"active_elevator_units": saved_active_units,
&"vertical_walk_blockers": saved_vertical_blockers,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
var version := int(_dictionary_value(snapshot, &"schema_version", -1))
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, ELEVATOR_INDEX_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
last_error = "Unsupported PathService snapshot version: %d" % version
return false
if version < 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
var raw_connectors: Variant = _dictionary_value(snapshot, &"connectors", null)
if not raw_connectors is Array:
last_error = "PathService snapshot connectors must be an Array."
@@ -1210,7 +1317,9 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
var next_active_units: Dictionary = connector_result[&"active_units"]
if version == LEGACY_SNAPSHOT_VERSION:
var changed := next_connectors != _connectors
var changed := next_connectors != _connectors or not _vertical_walk_blockers.is_empty()
_vertical_walk_blockers.clear()
_rebuild_vertical_walk_blocked_cells()
_connectors = next_connectors
_cell_traffic.clear()
_actor_cells.clear()
@@ -1256,7 +1365,7 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
last_error = progress_result[&"error"]
return false
var next_progress: Dictionary = progress_result[&"progress"]
if version == SNAPSHOT_VERSION:
if version >= ELEVATOR_INDEX_SNAPSHOT_VERSION:
var raw_known_requests: Variant = _dictionary_value(
snapshot,
&"known_elevator_request_ids",
@@ -1277,6 +1386,13 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
return false
next_known_requests = index_result[&"known_request_ids"]
next_active_units = index_result[&"active_units"]
var next_vertical_blockers: Dictionary = {}
if version >= 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"]
if not _snapshot_densities_match(next_traffic, next_actor_cells):
last_error = "PathService snapshot density does not match actor projections."
return false
@@ -1290,6 +1406,8 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
return false
_connectors = next_connectors
_vertical_walk_blockers = next_vertical_blockers
_rebuild_vertical_walk_blocked_cells()
_cell_traffic = next_traffic
_actor_cells = next_actor_cells
_actor_progress = next_progress
@@ -1320,7 +1438,7 @@ func _rebuild_derived_graph() -> void:
var cell: Vector3i = cell_value
for direction in _CARDINAL_DIRECTIONS:
var neighbor := cell + direction
if _passable_cells.has(neighbor):
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(cell, neighbor):
_append_edge(
cell,
neighbor,
@@ -1587,7 +1705,7 @@ func _route_exists_with_overrides(
var neighbor_set: Dictionary = {}
for direction: Vector3i in _CARDINAL_DIRECTIONS:
var neighbor := current + direction
if _passable_cells.has(neighbor):
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(current, neighbor):
neighbor_set[neighbor] = true
for connector_id: StringName in connector_ids:
var connector: Dictionary = _connectors[connector_id]
@@ -2080,6 +2198,41 @@ func _normalize_snapshot_elevator_indexes(
}
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: