extends RefCounted ## Read-only geometry preflight. A successful preview is NOT a migration commit: ## Runtime ownership, routes, jobs, actor positions and backups need their own gate. const DIMENSIONS := { &"premium_room": {&"old": Vector2i(3, 2), &"new": Vector2i(4, 1), &"layer": 0}, &"staff_dormitory": {&"old": Vector2i(4, 2), &"new": Vector2i(2, 1), &"layer": 1}, &"black_market": {&"old": Vector2i(3, 2), &"new": Vector2i(4, 2), &"layer": 0}, } static func inspect(world: WorldService, instance: Dictionary, weight_per_cell: int, entity_weights: Dictionary = {}) -> Dictionary: var result := {&"geometry_ok": false, &"ready_to_apply": false, &"errors": [], &"added": [], &"removed": [], &"target_cells": [], &"affected_entities": [], &"remaining_gates": [&"DEFINITION_WEIGHT_VALIDATION", &"RUNTIME_CROSSLINKS", &"ROUTES_AND_ACTORS", &"CONSTRUCTION_JOBS", &"BACKUP_AND_ATOMIC_COMMIT"]} if typeof(instance.get(&"instance_id")) not in [TYPE_STRING, TYPE_STRING_NAME] or typeof(instance.get(&"definition_id")) not in [TYPE_STRING, TYPE_STRING_NAME]: result[&"errors"].append(&"INVALID_ID") return result var id := StringName(instance.get(&"instance_id", &"")) var definition_id := StringName(instance.get(&"definition_id", &"")) if world == null or id.is_empty() or not DIMENSIONS.has(definition_id) or weight_per_cell <= 0: result[&"errors"].append(&"INVALID_INPUT") return result var origin: Variant = instance.get(&"origin") var rotation: Variant = instance.get(&"rotation") var cells: Variant = instance.get(&"occupied_cells") if not origin is Vector3i or typeof(rotation) != TYPE_INT or rotation not in [0, 90, 180, 270] or not cells is Array: result[&"errors"].append(&"INVALID_GEOMETRY") return result var specification: Dictionary = DIMENSIONS[definition_id] if origin.z != specification[&"layer"]: result[&"errors"].append(&"LAYER_MISMATCH") return result var old_size: Vector2i = specification[&"old"] var new_size: Vector2i = specification[&"new"] if rotation in [90, 270]: old_size = Vector2i(old_size.y, old_size.x) new_size = Vector2i(new_size.y, new_size.x) var expected_old := world.cells_for_footprint(origin, old_size) var target := world.cells_for_footprint(origin, new_size) if not _same_cells(cells, world.get_entity_cells(id)) or (not _same_cells(cells, expected_old) and not _same_cells(cells, target)): result[&"errors"].append(&"SOURCE_OWNERSHIP_OR_SHAPE_MISMATCH") return result result[&"target_cells"] = target.duplicate() for cell: Vector3i in target: if cell not in cells: result[&"added"].append(cell) if not world.is_in_bounds(cell): result[&"errors"].append({&"code": &"OUT_OF_BOUNDS", &"cell": cell}) elif cell.y < 0 and not world.is_excavated(cell): result[&"errors"].append({&"code": &"NOT_EXCAVATED", &"cell": cell}) if definition_id == &"black_market" and cell.y >= 0: result[&"errors"].append({&"code": &"ZONE_MISMATCH", &"cell": cell}) var other := world.get_occupant(cell) if not other.is_empty() and other != id: result[&"errors"].append({&"code": &"OVERLAP", &"cell": cell, &"entity_id": other}) for cell: Vector3i in cells: if cell not in target: result[&"removed"].append(cell) if not result[&"errors"].is_empty(): return result var candidate := WorldService.new() if not candidate.restore_snapshot(world.create_snapshot()) or not candidate.release(id) or not candidate.occupy(id, target, world.get_entity_kind(id)): result[&"errors"].append(&"CANDIDATE_REJECTED") return result if not candidate.validate_support(target, weight_per_cell, entity_weights)[&"ok"]: result[&"errors"].append(&"TARGET_UNSUPPORTED") # Recheck every previously supported occupied component; removal may split # a component even when the resized facility itself remains anchored. for row: Dictionary in world.create_snapshot()[&"occupancies"]: if row[&"entity_id"] == id: continue if world.validate_support(row[&"cells"], int(entity_weights.get(row[&"entity_id"], 1)), entity_weights)[&"ok"] and not candidate.validate_support(row[&"cells"], int(entity_weights.get(row[&"entity_id"], 1)), entity_weights)[&"ok"]: result[&"affected_entities"].append(row[&"entity_id"]) result[&"errors"].append({&"code": &"SUPPORT_LOSS", &"entity_id": row[&"entity_id"]}) result[&"geometry_ok"] = result[&"errors"].is_empty() return result static func _same_cells(a: Array, b: Array) -> bool: if a.size() != b.size(): return false var seen := {} for value: Variant in a: if not value is Vector3i or seen.has(value) or value not in b: return false seen[value] = true return true ## Build a validated construction/world candidate without touching live state. ## Other Runtime services and disk backup still need validation before commit. static func prepare_construction_candidate(world: WorldService, construction: ConstructionService, owner_id: StringName, weight: int, weights: Dictionary = {}) -> Dictionary: var source := construction.create_snapshot() var result := {&"ok": false, &"ready_to_apply": false, &"errors": []} var validator := ConstructionService.new(world) if source.is_empty() or not validator.restore_snapshot(source): result[&"errors"].append(&"INVALID_SOURCE_CONSTRUCTION") return result var instance := construction.get_facility(owner_id) var source_job_id: StringName = instance.get(&"source_job_id", &"") if instance.is_empty(): for job: ConstructionJob in construction.get_jobs(): if job.job_id != owner_id or job.is_terminal(): continue source_job_id = job.job_id instance = {&"instance_id": job.job_id, &"definition_id": job.definition_id, &"origin": job.origin, &"rotation": job.rotation, &"occupied_cells": job.reserved_cells.duplicate()} break if instance.is_empty(): result[&"errors"].append(&"MIGRATABLE_OWNER_REQUIRED") return result var preview := inspect(world, instance, weight, weights) result[&"preview"] = preview if not preview[&"geometry_ok"]: result[&"errors"] = preview[&"errors"].duplicate(true) return result var target: Array = preview[&"target_cells"] var candidate_world := WorldService.new() if not candidate_world.restore_snapshot(world.create_snapshot()): result[&"errors"].append(&"CANDIDATE_WORLD_REJECTED") return result if not _same_cells(world.get_entity_cells(owner_id), target) and (not candidate_world.release(owner_id) or not candidate_world.occupy(owner_id, target, world.get_entity_kind(owner_id))): result[&"errors"].append(&"CANDIDATE_WORLD_REJECTED") return result var encoded: Array = [] for cell: Vector3i in target: encoded.append([cell.x, cell.y, cell.z]) var candidate := source.duplicate(true) for saved: Dictionary in candidate[&"jobs"]: if StringName(saved[&"job_id"]) == source_job_id: saved[&"reserved_cells"] = encoded.duplicate(true) for saved: Dictionary in candidate[&"facilities"]: if StringName(saved[&"instance_id"]) == owner_id: saved[&"occupied_cells"] = encoded.duplicate(true) var restored := ConstructionService.new(candidate_world) if not restored.restore_snapshot(candidate): result[&"errors"].append(&"CANDIDATE_CONSTRUCTION_REJECTED") return result result[&"ok"] = true result[&"world"] = candidate_world.create_snapshot() result[&"construction"] = restored.create_snapshot() return result