Files
TheTree_mock/scripts/construction/facility_resize_preview.gd
T
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
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.
2026-09-30 23:52:24 +09:00

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