class_name LegacyStairService extends RefCounted ## Owns only the stairs materialized once from pre-policy vertical WALK edges. ## Their original route score and access remain unchanged. Demolished landings ## permanently remove their compatibility connector; new floors need stairs. const PathType = preload("res://scripts/world/path_service.gd") const SNAPSHOT_VERSION := 1 const CONNECTOR_PREFIX := "legacy.stairs:" const CONNECTOR_COST := 100 const MAX_STAIRS := 131_072 const MAX_COORDINATE_ABS := 100_000 var last_error := "" var _path: PathService var _stairs: Dictionary = {} var _migration_closed := false var _transaction_active := false var _quarantined := false ## Bind before or after Path restoration. restore_snapshot performs the exact ## namespace and topology crosslink checks after all dependencies are loaded. func configure(path_value: Variant) -> bool: if _transaction_active or _path != null: return _fail("Legacy stairs are already configured or transacting.") if not path_value is PathService: return _fail("Legacy stairs require PathService.") _path = path_value last_error = "" return true func migrate_existing_vertical_walks() -> bool: if not _available(): return false if _path.get_vertical_walk_requires_connector(): if not _live_matches(_stairs, true): return _fail("Current legacy stairs do not match Path ownership or landings.") _migration_closed = true last_error = "" return true if _migration_closed or not _stairs.is_empty(): return _fail("Legacy vertical WALK migration cannot be run again.") if not _live_matches({}, false): return _fail("Legacy stair connector namespace contains unowned state.") var capture := _capture_path() if capture.is_empty(): return false var blocked := _shaft_cells() var next_stairs: Dictionary = {} for from_cell: Vector3i in capture[&"cells"]: for raw_edge: Variant in _path.get_neighbors(from_cell): var edge: Dictionary = raw_edge var to_cell: Vector3i = edge[&"to"] if edge[&"movement_type"] != PathType.MOVEMENT_WALK \ or not StringName(edge[&"connector_id"]).is_empty() \ or to_cell != from_cell + Vector3i(0, 1, 0) \ or blocked.has(from_cell) or blocked.has(to_cell): continue if not _valid_cell(from_cell) or not _valid_cell(to_cell): return _fail("Legacy vertical WALK endpoints exceed the supported coordinate range.") var stair_id := _stable_id(from_cell) next_stairs[stair_id] = {&"id": stair_id, &"from": from_cell, &"to": to_cell} if next_stairs.size() > MAX_STAIRS: return _fail("Legacy stair migration exceeds the supported capacity.") return _apply_candidate(next_stairs, capture) ## Called after authoritative cells/shafts change. Missing endpoints and new ## shafts retire records; no adjacency scan is ever performed here. func prune_invalid() -> bool: if not _available(): return false if not _live_matches(_stairs, true, false): return _fail("Legacy stair ownership or vertical policy was altered.") var blocked := _shaft_cells() var next_stairs: Dictionary = {} for stair_id: StringName in _sorted_ids(_stairs): var record: Dictionary = _stairs[stair_id] if _landing_is_valid(record, blocked): next_stairs[stair_id] = record.duplicate(true) if next_stairs == _stairs: last_error = "" return true var capture := _capture_path() if capture.is_empty(): return false return _apply_candidate(next_stairs, capture) func create_snapshot() -> Dictionary: if _path == null: return {} var rows: Array[Dictionary] = [] for stair_id: StringName in _sorted_ids(_stairs): var record: Dictionary = _stairs[stair_id] rows.append({&"id": String(stair_id), &"from": record[&"from"], &"to": record[&"to"]}) return {&"schema_version": SNAPSHOT_VERSION, &"stairs": rows} func restore_snapshot(data: Dictionary) -> bool: if _path == null or _transaction_active: return _fail("Legacy stair restore requires an idle configured service.") var parsed := _parse_snapshot(data) if not bool(parsed[&"ok"]): return _fail(String(parsed[&"error"])) var next_stairs: Dictionary = parsed[&"stairs"] if not _live_matches(next_stairs, true): return _fail("Legacy stair snapshot does not exactly match Path connectors and landings.") _stairs = next_stairs.duplicate(true) _migration_closed = true _quarantined = false last_error = "" return true func validate_invariants() -> Dictionary: var ok := _path != null and not _quarantined and _live_matches(_stairs, true) return {&"ok": ok, &"stair_count": _stairs.size(), &"quarantined": _quarantined} ## Assemble one Path snapshot so migration remains linear in graph size and ## preserves every unrelated connector, actor, traffic row, and shaft owner. func _apply_candidate(next_stairs: Dictionary, capture: Dictionary) -> bool: var before: Dictionary = capture[&"snapshot"] var next_snapshot := before.duplicate(true) var connectors: Array = [] for raw_connector: Dictionary in before[&"connectors"]: if not String(raw_connector[&"id"]).begins_with(CONNECTOR_PREFIX): connectors.append(raw_connector.duplicate(true)) for stair_id: StringName in _sorted_ids(next_stairs): connectors.append(_expected_connector(next_stairs[stair_id])) connectors.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"id"]) < String(right[&"id"])) next_snapshot[&"connectors"] = connectors next_snapshot[&"vertical_walk_requires_connector"] = true next_snapshot[&"graph_revision"] = int(before[&"graph_revision"]) + 1 var verifier := PathType.new() if not verifier.restore_snapshot(next_snapshot) or not verifier.rebuild_graph(capture[&"cells"]): return _fail("Legacy stair candidate Path snapshot is invalid: %s" % verifier.last_error) var expected := verifier.create_snapshot() var stairs_before := _stairs.duplicate(true) var closed_before := _migration_closed var path_before := _path _transaction_active = true var applied := _path.restore_snapshot(expected.duplicate(true)) if not applied or _path != path_before or not _transaction_active or _stairs != stairs_before \ or _migration_closed != closed_before \ or _path.create_snapshot() != expected \ or not _live_matches(next_stairs, true): _path = path_before var restored := false for _attempt: int in range(2): _path.rebuild_graph(capture[&"cells"]) _path.restore_snapshot(before.duplicate(true)) _path.rebuild_graph(capture[&"cells"]) if _path.create_snapshot() == before: restored = true break _stairs = stairs_before _migration_closed = closed_before _transaction_active = false _quarantined = not restored return _fail("Legacy stair transaction failed; exact rollback %s." % ("restored" if restored else "is uncertain")) _stairs = next_stairs.duplicate(true) _migration_closed = true _transaction_active = false last_error = "" return true func _live_matches(stairs: Dictionary, require_policy: bool, require_landings := true) -> bool: if _path == null or (require_policy and not _path.get_vertical_walk_requires_connector()): return false var seen: Dictionary = {} for raw_connector: Dictionary in _path.create_snapshot().get(&"connectors", []): var stair_id := StringName(raw_connector[&"id"]) if not String(stair_id).begins_with(CONNECTOR_PREFIX): continue if not stairs.has(stair_id) or _path.get_connector(stair_id) != _expected_connector(stairs[stair_id]): return false seen[stair_id] = true if seen.size() != stairs.size(): return false if require_landings: var blocked := _shaft_cells() for record: Dictionary in stairs.values(): if not _landing_is_valid(record, blocked): return false return true func _landing_is_valid(record: Dictionary, blocked: Dictionary) -> bool: return _path.has_cell(record[&"from"]) and _path.has_cell(record[&"to"]) \ and not blocked.has(record[&"from"]) and not blocked.has(record[&"to"]) func _shaft_cells() -> Dictionary: var result: Dictionary = {} for cells: Array in _path.get_vertical_walk_blockers().values(): for cell: Vector3i in cells: result[cell] = true return result func _capture_path() -> Dictionary: var snapshot := _path.create_snapshot() var cells: Array[Vector3i] = [] for row: Dictionary in snapshot.get(&"cell_traffic", []): var cell: Vector3i = row[&"cell"] if not _path.has_cell(cell): _fail("Legacy stair migration requires the materialized Path graph.") return {} cells.append(cell) return {&"snapshot": snapshot, &"cells": cells} static func _parse_snapshot(data: Dictionary) -> Dictionary: if not _keys_match(data, [&"schema_version", &"stairs"]) \ or typeof(data.get(&"schema_version")) != TYPE_INT \ or data[&"schema_version"] != SNAPSHOT_VERSION \ or not data.get(&"stairs") is Array \ or data[&"stairs"].size() > MAX_STAIRS: return {&"ok": false, &"error": "Legacy stair snapshot fields or version are invalid."} var stairs: Dictionary = {} var previous_id := "" for raw: Variant in data[&"stairs"]: if not raw is Dictionary or not _keys_match(raw, [&"id", &"from", &"to"]) \ or typeof(raw.get(&"id")) != TYPE_STRING \ or not _valid_cell(raw.get(&"from")) or not _valid_cell(raw.get(&"to")): return {&"ok": false, &"error": "Legacy stair record types are invalid."} var lower: Vector3i = raw[&"from"] var upper: Vector3i = raw[&"to"] var stair_id := StringName(raw[&"id"]) if upper != lower + Vector3i(0, 1, 0) or stair_id != _stable_id(lower) \ or (not previous_id.is_empty() and String(stair_id) <= previous_id): return {&"ok": false, &"error": "Legacy stair geometry, stable ID, or order is invalid."} stairs[stair_id] = {&"id": stair_id, &"from": lower, &"to": upper} previous_id = String(stair_id) return {&"ok": true, &"stairs": stairs} static func _expected_connector(record: Dictionary) -> Dictionary: var lower: Vector3i = record[&"from"] return { &"id": record[&"id"], &"from": lower, &"to": record[&"to"], &"cost": CONNECTOR_COST, &"bidirectional": true, &"access_id": &"", &"enabled": true, &"movement_type": PathType.MOVEMENT_CONNECTOR, &"dynamic_cost": 0, &"operational_state": PathType.CONNECTOR_OPERATIONAL, &"cabin_capacity": 8, &"queue_capacity": 256, &"cabin_state": PathType.CABIN_IDLE, &"current_floor": lower.y, &"travel_minutes_per_floor": 1, &"request_queue": [], } static func _stable_id(lower: Vector3i) -> StringName: return StringName("%s%d:%d:%d" % [CONNECTOR_PREFIX, lower.z, lower.x, lower.y]) static func _valid_cell(value: Variant) -> bool: return value is Vector3i and value.z in [PathType.FRONT, PathType.BACK] \ and absi(value.x) <= MAX_COORDINATE_ABS and absi(value.y) <= MAX_COORDINATE_ABS static func _keys_match(value: Dictionary, expected: Array[StringName]) -> bool: if value.size() != expected.size(): return false for key: Variant in value: if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(key) not in expected: return false return true static func _sorted_ids(value: Dictionary) -> Array[StringName]: var result: Array[StringName] = [] for stair_id: StringName in value: result.append(stair_id) result.sort_custom(func(left: StringName, right: StringName) -> bool: return String(left) < String(right)) return result func _available() -> bool: if _path == null or _transaction_active or _quarantined: return _fail("Legacy stairs require an idle, configured, available service.") return true func _fail(message: String) -> bool: last_error = message return false