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.
130 lines
7.1 KiB
GDScript
130 lines
7.1 KiB
GDScript
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
|