extends RefCounted ## Compare actual guest/staff access queries on two isolated Runtime states. ## Reachability alone is not a relocation or in-flight transport migration. static func inspect(before: Variant, after: Variant, facility_id: StringName) -> Dictionary: var result := {&"ok": false, &"errors": [], &"guest_replans": [], &"checked_routes": 0} var old_room: FacilityInstance = before.facility_service.get_instance(facility_id) var new_room: FacilityInstance = after.facility_service.get_instance(facility_id) if old_room == null or new_room == null: result[&"errors"].append(&"FACILITY_REQUIRED") return result _check(result, before.get_facility_access_report(facility_id), after.get_facility_access_report(facility_id), &"ENTRANCE_ACCESS_LOST", &"") for guest: GuestRuntime in before.guest_service.get_guests(): if guest.target_facility_id != facility_id: continue var restored: GuestRuntime = after.guest_service.get_guest(guest.guest_id) if restored == null: result[&"errors"].append({&"code": &"GUEST_MISSING", &"id": guest.guest_id}) continue var old_route: Dictionary = before._find_facility_access_path(guest.current_cell, old_room) var new_route: Dictionary = after._find_facility_access_path(restored.current_cell, new_room) _check(result, old_route, new_route, &"GUEST_ACCESS_LOST", guest.guest_id) if restored.state == GuestRuntime.State.MOVING and new_route[&"reachable"]: var target: Vector3i = after._path_handle_target(restored.path_handle, restored.current_cell) # A new best destination must be applied explicitly to the saved route. if target != new_route[&"target_cell"]: result[&"guest_replans"].append({&"id": guest.guest_id, &"old_target": target, &"target": new_route[&"target_cell"]}) for worker: StaffRuntime in before.staff_service.get_staff_members(): var assignment: Dictionary = before.staff_service.get_work_assignment(worker.staff_id) var rest: Dictionary = before.get_staff_rest_report(worker.staff_id) var resting_here := StringName(rest.get(&"facility_id", &"")) == facility_id if worker.assigned_facility_id != facility_id and StringName(assignment.get(&"facility_id", &"")) != facility_id and not resting_here: continue var restored: StaffRuntime = after.staff_service.get_staff(worker.staff_id) if restored == null: result[&"errors"].append({&"code": &"STAFF_MISSING", &"id": worker.staff_id}) continue _check(result, before.staff_commute.find_facility_access_path(worker.current_cell, old_room), after.staff_commute.find_facility_access_path(restored.current_cell, new_room), &"STAFF_ACCESS_LOST", worker.staff_id) var access_cells := {} for cell: Vector3i in new_room.occupied_cells: for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]: access_cells[cell + offset] = true for task: StaffTask in before.staff_service.get_tasks(): if task.is_terminal() or (task.target_entity_id != facility_id and task.rescue_facility_id != facility_id): continue var restored: StaffTask = after.staff_service.get_task(task.task_id) if restored == null or restored.create_snapshot() != task.create_snapshot(): result[&"errors"].append({&"code": &"TASK_STATE_CHANGED", &"id": task.task_id}) continue if not access_cells.has(task.target_cell): result[&"errors"].append({&"code": &"TASK_DESTINATION_CHANGED", &"id": task.task_id}) continue if task.assigned_staff_id.is_empty(): continue var old_worker: StaffRuntime = before.staff_service.get_staff(task.assigned_staff_id) var new_worker: StaffRuntime = after.staff_service.get_staff(task.assigned_staff_id) if old_worker == null or new_worker == null: result[&"errors"].append({&"code": &"STAFF_MISSING", &"id": task.assigned_staff_id}) continue _check(result, {&"reachable":_task_reachable(before, old_worker, task)}, {&"reachable":_task_reachable(after, new_worker, restored)}, &"TASK_ACCESS_LOST", task.task_id) result[&"ok"] = result[&"errors"].is_empty() and result[&"guest_replans"].is_empty() return result static func _check(result: Dictionary, old_route: Dictionary, new_route: Dictionary, code: StringName, actor_id: StringName) -> void: result[&"checked_routes"] += 1 if old_route.get(&"reachable", false) and not new_route.get(&"reachable", false): result[&"errors"].append({&"code": code, &"id": actor_id}) ## Apply only to an isolated candidate. Validate the entire batch before changes. ## Position, movement credit, service request, wallet and transport stay intact. static func retarget_guests(candidate: Variant, report: Dictionary) -> Dictionary: var result := {&"ok": false, &"errors": [], &"changed": []} if not report.get(&"errors", []).is_empty(): result[&"errors"].append(&"ACCESS_LOSS") return result var updates: Array = [] var seen := {} for plan: Dictionary in report.get(&"guest_replans", []): var id := StringName(plan.get(&"id", &"")) var guest: GuestRuntime = candidate.guest_service.get_guest(id) if guest == null or seen.has(id) or guest.state != GuestRuntime.State.MOVING or not guest.path_handle is Dictionary or not plan.get(&"target") is Vector3i: result[&"errors"].append(&"INVALID_REPLAN") return result seen[id] = true if candidate._path_handle_target(guest.path_handle, guest.current_cell) != plan.get(&"old_target"): result[&"errors"].append(&"STALE_REPLAN") return result if not candidate.elevators.get_actor_trip(candidate._guest_path_actor_id(id)).is_empty(): result[&"errors"].append(&"TRANSPORT_REPLAN_REQUIRED") return result if candidate.path_service.find_path(guest.current_cell, plan[&"target"]).is_empty(): result[&"errors"].append(&"TARGET_UNREACHABLE") return result updates.append({&"guest": guest, &"target": plan[&"target"]}) for update: Dictionary in updates: var guest: GuestRuntime = update[&"guest"] var handle: Dictionary = guest.path_handle.duplicate(true) handle[&"target_cell"] = update[&"target"] handle[&"graph_revision"] = candidate.path_service.graph_revision guest.path_handle = handle result[&"changed"].append(guest.guest_id) result[&"ok"] = true return result static func _task_reachable(runtime: Variant, worker: StaffRuntime, task: StaffTask) -> bool: if task.task_type in [&"rescue_pickup", &"rescue_transport"]: return not runtime.path_service.find_backyard_path(worker.current_cell, task.target_cell, true).is_empty() return not runtime.path_service.find_path(worker.current_cell, task.target_cell, [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]).is_empty()