Files
TheTree_mock/scripts/construction/construction_service.gd
T

1540 lines
55 KiB
GDScript

class_name ConstructionService
extends RefCounted
signal job_created(job: ConstructionJob)
signal job_changed(job: ConstructionJob)
signal facility_completed(instance: Dictionary)
signal facility_demolished(instance_id: StringName, refund: Dictionary)
const PlacementReportType = preload("res://scripts/construction/placement_report.gd")
const ConstructionJobType = preload("res://scripts/construction/construction_job.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const SNAPSHOT_VERSION := 2
const LEGACY_SNAPSHOT_VERSION := 1
const DEFAULT_BUILD_MINUTES := 60
const DEMOLITION_MATERIAL_REFUND_BASIS_POINTS := 2_500
const MAX_PROGRESS := 10_000
const MAX_WORK_RATE := 1_000_000
const MAX_WORK_UNITS := 9_000_000_000_000_000
const MAX_BUILD_MINUTES := 10_000_000
const MAX_JOBS := 20_000
const MAX_FACILITIES := 20_000
const MAX_SEQUENCE := 99_999_999
const DEFERRED_JOB_CHANGED: StringName = &"JOB_CHANGED"
const DEFERRED_FACILITY_COMPLETED: StringName = &"FACILITY_COMPLETED"
var default_build_minutes := DEFAULT_BUILD_MINUTES
var current_minute := 0
var _world: Variant
var _resource_state: Variant
var _jobs: Dictionary = {}
var _facilities: Dictionary = {}
var _next_job_sequence := 1
var _next_facility_sequence := 1
var _last_progress_minutes: Dictionary = {}
var _mutation_in_progress := false
var _quarantined := false
var _quarantine_reason := ""
var _deferred_notifications: Array[Dictionary] = []
var _notification_staging_active := false
var _notification_commit_active := false
func _init(world: Variant = null, resource_state: Variant = null) -> void:
_world = world
_resource_state = resource_state
func configure(world: Variant, resource_state: Variant) -> ConstructionService:
_world = world
_resource_state = resource_state
return self
func validate_placement(
definition: Variant,
origin: Vector3i,
rotation: int = 0
) -> PlacementReport:
var report := PlacementReportType.new()
if not _definition_is_valid(definition):
report.add_error(
PlacementReportType.ERROR_INVALID_DEFINITION,
"The facility definition is missing or invalid."
)
return report.finalize()
var footprint: Vector2i = _definition_value(definition, &"footprint", Vector2i.ZERO)
var expected_zone := int(_definition_value(definition, &"zone", 0))
var expected_layer := int(_definition_value(definition, &"layer", origin.z))
report.total_cost = _normalize_cost(_definition_value(definition, &"construction_cost", {}))
report.build_time = _definition_build_minutes(definition)
var missing_world_methods := _missing_methods(
_world,
[&"cells_for_footprint", &"is_in_bounds", &"get_zone", &"get_occupant", &"is_excavated", &"validate_support"]
)
if not missing_world_methods.is_empty():
report.add_error(
PlacementReportType.ERROR_MISSING_DEPENDENCY,
"World placement services are unavailable.",
[],
{&"methods": missing_world_methods}
)
if _resource_state == null:
report.add_error(
PlacementReportType.ERROR_MISSING_DEPENDENCY,
"The resource state is unavailable."
)
if missing_world_methods.is_empty():
report.cells = _world_cells_for_footprint(
origin,
footprint,
_normalize_rotation(rotation),
expected_layer
)
if report.cells.is_empty():
report.add_error(
PlacementReportType.ERROR_INVALID_DEFINITION,
"The footprint produced no world cells."
)
_validate_cell_constraints(report, origin, expected_zone, expected_layer)
_validate_support(report, definition)
_validate_cost(report)
return report.finalize()
func start_construction(
definition: Variant,
origin: Vector3i,
rotation: int = 0,
build_minutes: int = 0,
created_at: int = -1
) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
if _jobs.size() >= MAX_JOBS or _next_job_sequence > MAX_SEQUENCE:
return _failure("CONSTRUCTION_LIMIT", "Construction job capacity has been reached.")
if build_minutes > MAX_BUILD_MINUTES:
return _failure("INVALID_BUILD_TIME", "Construction build time exceeds the supported limit.")
_mutation_in_progress = true
var report := validate_placement(definition, origin, rotation)
if not report.can_build:
_mutation_in_progress = false
return _failure("INVALID_PLACEMENT", "Facility placement is invalid.", {
&"placement_report": report.to_dictionary(),
})
var job_id := StringName("construction_%08d" % _next_job_sequence)
var resource_snapshot: Variant = _capture_resource_state()
var world_snapshot := _capture_world_state(report.cells)
if not _spend_cost(report.total_cost):
var payment_rolled_back := _restore_resource_state(resource_snapshot)
_mutation_in_progress = false
if not payment_rolled_back:
_quarantine("Construction payment failed after mutating resources and could not be rolled back.")
return _failure("PAYMENT_FAILED", "Construction resources changed before the command was applied.", {
&"cost": report.total_cost.duplicate(true),
&"rolled_back": payment_rolled_back,
&"rollback_failed": not payment_rolled_back,
})
if not _world_occupy(report.cells, job_id, &"construction"):
var world_rolled_back := _restore_world_state(world_snapshot)
var resources_rolled_back := _restore_resource_state(resource_snapshot)
var rolled_back := world_rolled_back and resources_rolled_back
_mutation_in_progress = false
if not rolled_back:
_quarantine("Construction reservation failed and transaction rollback was incomplete.")
return _failure("RESERVATION_FAILED", "World reservation failed.", {
&"job_id": job_id,
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
&"world_rolled_back": world_rolled_back,
&"resources_rolled_back": resources_rolled_back,
})
var effective_minutes := build_minutes if build_minutes > 0 else report.build_time
var effective_created_at := current_minute if created_at < 0 else created_at
var definition_id := StringName(_definition_value(definition, &"id", &""))
var expected_layer := int(_definition_value(definition, &"layer", origin.z))
var job := ConstructionJobType.new().configure(
job_id,
definition_id,
origin,
rotation,
expected_layer,
report.cells,
report.total_cost,
effective_minutes,
effective_created_at
)
_jobs[job_id] = job
_last_progress_minutes[job_id] = -1
_next_job_sequence += 1
_mutation_in_progress = false
job_created.emit(job)
return CommandResultType.succeeded(0, {
&"job_id": job_id,
&"placement_report": report.to_dictionary(),
})
func get_job(job_id: StringName) -> ConstructionJob:
return _jobs.get(job_id) as ConstructionJob
func get_jobs() -> Array[ConstructionJob]:
var result: Array[ConstructionJob] = []
var ids := _sorted_string_name_keys(_jobs)
for job_id: StringName in ids:
result.append(_jobs[job_id])
return result
func get_facility(instance_id: StringName) -> Dictionary:
if not _facilities.has(instance_id):
return {}
return (_facilities[instance_id] as Dictionary).duplicate(true)
func get_facilities() -> Array[Dictionary]:
var result: Array[Dictionary] = []
var ids := _sorted_string_name_keys(_facilities)
for instance_id: StringName in ids:
result.append((_facilities[instance_id] as Dictionary).duplicate(true))
return result
func assign_builders(job_id: StringName, builder_ids: Array[StringName]) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
var job := get_job(job_id)
if job == null:
return _not_found("Construction job", job_id)
if job.is_terminal():
return _invalid_state("A terminal construction job cannot receive builders.", job_id)
job.assigned_builder_ids.assign(builder_ids)
_emit_job_changed(job)
return CommandResultType.succeeded(0, {&"job_id": job_id})
func advance_one_minute(
absolute_minute: int = -1,
work_rates: Dictionary = {},
defer_notifications := false
) -> Array[CommandResult]:
if _notification_staging_active or _notification_commit_active:
return [_notification_unavailable_result(&"NOTIFICATION_REENTRY", "Construction notification dispatch is already active.")]
if has_deferred_notifications():
return [_notification_unavailable_result(
&"DEFERRED_NOTIFICATIONS_PENDING",
"Deferred construction notifications must be committed or discarded before another advance."
)]
if not _mutation_allowed():
return [_unavailable_result()]
_notification_staging_active = defer_notifications
if absolute_minute < 0:
current_minute += 1
else:
current_minute = maxi(absolute_minute, current_minute)
var results: Array[CommandResult] = []
for job_id: StringName in _sorted_string_name_keys(_jobs):
var job: ConstructionJob = _jobs[job_id]
if job.is_terminal():
continue
var raw_rate: Variant = work_rates.get(job_id, ConstructionJobType.NORMAL_WORK_RATE)
if not raw_rate is int:
results.append(_failure("INVALID_WORK_RATE", "Construction work rate must be an integer."))
continue
results.append(progress_job(job_id, int(raw_rate), current_minute))
_notification_staging_active = false
return results
func has_deferred_notifications() -> bool:
return not _deferred_notifications.is_empty()
func commit_deferred_notifications() -> bool:
if _notification_staging_active or _notification_commit_active or _mutation_in_progress:
return false
if _deferred_notifications.is_empty():
return true
_notification_commit_active = true
var prepared: Array[Dictionary] = []
for notification: Dictionary in _deferred_notifications:
var kind: StringName = notification.get(&"kind", &"")
match kind:
DEFERRED_JOB_CHANGED:
var saved_job: Variant = notification.get(&"payload", null)
if not saved_job is Dictionary:
_notification_commit_active = false
return false
var job := ConstructionJobType.from_snapshot(saved_job.duplicate(true))
if job == null:
_notification_commit_active = false
return false
prepared.append({&"kind": kind, &"payload": job})
DEFERRED_FACILITY_COMPLETED:
var saved_instance: Variant = notification.get(&"payload", null)
if not saved_instance is Dictionary:
_notification_commit_active = false
return false
prepared.append({&"kind": kind, &"payload": saved_instance.duplicate(true)})
_:
_notification_commit_active = false
return false
_deferred_notifications.clear()
for notification: Dictionary in prepared:
match StringName(notification[&"kind"]):
DEFERRED_JOB_CHANGED:
job_changed.emit(notification[&"payload"])
DEFERRED_FACILITY_COMPLETED:
facility_completed.emit((notification[&"payload"] as Dictionary).duplicate(true))
_notification_commit_active = false
return true
func discard_deferred_notifications() -> bool:
if _notification_staging_active or _notification_commit_active or _mutation_in_progress:
return false
_deferred_notifications.clear()
return true
func _emit_job_changed(job: ConstructionJob) -> void:
if _notification_staging_active:
_deferred_notifications.append({
&"kind": DEFERRED_JOB_CHANGED,
&"payload": job.create_snapshot().duplicate(true),
})
return
job_changed.emit(job)
func _emit_facility_completed(instance: Dictionary) -> void:
var payload := instance.duplicate(true)
if _notification_staging_active:
_deferred_notifications.append({
&"kind": DEFERRED_FACILITY_COMPLETED,
&"payload": payload,
})
return
facility_completed.emit(payload)
func progress_job(
job_id: StringName,
work_rate_basis_points: int = ConstructionJob.NORMAL_WORK_RATE,
absolute_minute: int = -1
) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
var job := get_job(job_id)
if job == null:
return _not_found("Construction job", job_id)
if job.is_terminal():
return _invalid_state("A completed or cancelled job cannot progress.", job_id)
var minute := current_minute if absolute_minute < 0 else absolute_minute
if minute < 0:
return _failure("INVALID_MINUTE", "Construction progress minute cannot be negative.")
if work_rate_basis_points <= 0 or work_rate_basis_points > MAX_WORK_RATE:
return _failure("INVALID_WORK_RATE", "Construction work rate is outside the supported range.")
if minute <= int(_last_progress_minutes.get(job_id, -1)):
var replay := CommandResultType.succeeded(0, {
&"job_id": job_id,
&"progress": job.progress,
&"state": ConstructionJobType.State.keys()[job.state],
})
replay.is_replay = true
return replay
if job.work_units_done > MAX_WORK_UNITS - work_rate_basis_points:
return _failure("WORK_LIMIT", "Construction work accumulator limit was reached.")
_mutation_in_progress = true
var ready_to_complete := job.advance_one_minute(work_rate_basis_points, minute)
_last_progress_minutes[job_id] = minute
if ready_to_complete:
var completed := _complete_job(job, minute)
_mutation_in_progress = false
return completed
_mutation_in_progress = false
_emit_job_changed(job)
return CommandResultType.succeeded(0, {
&"job_id": job_id,
&"progress": job.progress,
&"state": ConstructionJobType.State.keys()[job.state],
})
func cancel_job(job_id: StringName, absolute_minute: int = -1) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
var job := get_job(job_id)
if job == null:
return _not_found("Construction job", job_id)
if job.is_terminal():
return _invalid_state("A completed or cancelled job cannot be cancelled.", job_id)
var minute := current_minute if absolute_minute < 0 else absolute_minute
if minute < 0:
return _failure("INVALID_MINUTE", "Construction cancellation minute cannot be negative.")
var refund := job.refundable_cost()
var resource_snapshot: Variant = _capture_resource_state()
var world_snapshot := _capture_world_state(job.reserved_cells)
_mutation_in_progress = true
if not _world_release(job.reserved_cells, job.job_id):
var release_rolled_back := _restore_world_state(world_snapshot)
_mutation_in_progress = false
if not release_rolled_back:
_quarantine("Construction cancellation release failed and world state could not be restored.")
return _failure("RELEASE_FAILED", "The construction reservation could not be released.", {
&"job_id": job_id,
&"rolled_back": release_rolled_back,
&"rollback_failed": not release_rolled_back,
})
if not _refund_cost(refund):
var world_rolled_back := _restore_world_state(world_snapshot)
var resources_rolled_back := _restore_resource_state(resource_snapshot)
var rolled_back := world_rolled_back and resources_rolled_back
_mutation_in_progress = false
if not rolled_back:
_quarantine("Construction cancellation refund failed and rollback was incomplete.")
return _failure("REFUND_FAILED", "The cancellation refund failed.", {
&"job_id": job_id,
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
&"world_rolled_back": world_rolled_back,
&"resources_rolled_back": resources_rolled_back,
})
job.mark_cancelled(minute)
_mutation_in_progress = false
_emit_job_changed(job)
return CommandResultType.succeeded(0, {
&"job_id": job_id,
&"refund": refund,
&"progress": job.progress,
})
func demolish_facility(
instance_id: StringName,
material_refund_basis_points: int = DEMOLITION_MATERIAL_REFUND_BASIS_POINTS
) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
if not _facilities.has(instance_id):
return _not_found("Facility instance", instance_id)
var instance: Dictionary = _facilities[instance_id]
var cells: Array[Vector3i] = []
cells.assign(instance.get(&"occupied_cells", []))
var refund: Dictionary = {}
var construction_cost: Dictionary = instance.get(&"construction_cost", {})
var materials := int(construction_cost.get(&"materials", 0))
if materials > 0:
refund[&"materials"] = materials * clampi(material_refund_basis_points, 0, 10_000) / 10_000
var resource_snapshot: Variant = _capture_resource_state()
var world_snapshot := _capture_world_state(cells)
_mutation_in_progress = true
if not _world_release(cells, instance_id):
var release_rolled_back := _restore_world_state(world_snapshot)
_mutation_in_progress = false
if not release_rolled_back:
_quarantine("Facility demolition release failed and world state could not be restored.")
return _failure("RELEASE_FAILED", "The facility occupancy could not be released.", {
&"instance_id": instance_id,
&"rolled_back": release_rolled_back,
&"rollback_failed": not release_rolled_back,
})
if not _refund_cost(refund):
var world_rolled_back := _restore_world_state(world_snapshot)
var resources_rolled_back := _restore_resource_state(resource_snapshot)
var rolled_back := world_rolled_back and resources_rolled_back
_mutation_in_progress = false
if not rolled_back:
_quarantine("Facility demolition refund failed and rollback was incomplete.")
return _failure("REFUND_FAILED", "The demolition refund failed.", {
&"instance_id": instance_id,
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
&"world_rolled_back": world_rolled_back,
&"resources_rolled_back": resources_rolled_back,
})
_facilities.erase(instance_id)
# A completed construction job owns its live facility. Once demolition has
# committed, retire both records so snapshots cannot retain an orphaned
# COMPLETED job. Sequence counters and emitted domain events retain history.
var source_job_id := StringName(instance.get(&"source_job_id", &""))
_jobs.erase(source_job_id)
_last_progress_minutes.erase(source_job_id)
_mutation_in_progress = false
facility_demolished.emit(instance_id, refund)
return CommandResultType.succeeded(0, {
&"instance_id": instance_id,
&"refund": refund,
})
func create_snapshot() -> Dictionary:
if _notification_staging_active or _notification_commit_active or has_deferred_notifications():
return {}
var saved_jobs: Array[Dictionary] = []
for job_id: StringName in _sorted_string_name_keys(_jobs):
var job: ConstructionJob = _jobs[job_id]
var saved_job := job.create_snapshot()
saved_job[&"last_progress_at"] = int(_last_progress_minutes.get(job_id, -1))
saved_jobs.append(saved_job)
var saved_facilities: Array[Dictionary] = []
for instance_id: StringName in _sorted_string_name_keys(_facilities):
saved_facilities.append(_serialize_facility(_facilities[instance_id]))
return {
&"schema_version": SNAPSHOT_VERSION,
&"default_build_minutes": default_build_minutes,
&"current_minute": current_minute,
&"next_job_sequence": _next_job_sequence,
&"next_facility_sequence": _next_facility_sequence,
&"jobs": saved_jobs,
&"facilities": saved_facilities,
&"quarantined": _quarantined,
&"quarantine_reason": _quarantine_reason,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
if _mutation_in_progress or _notification_staging_active \
or _notification_commit_active or has_deferred_notifications():
return false
var parsed := _parse_snapshot(snapshot)
if not bool(parsed.get(&"ok", false)):
return false
if not _validate_restored_state(parsed):
return false
_jobs = parsed[&"jobs"]
_facilities = parsed[&"facilities"]
_last_progress_minutes = parsed[&"last_progress_minutes"]
default_build_minutes = parsed[&"default_build_minutes"]
current_minute = parsed[&"current_minute"]
_next_job_sequence = parsed[&"next_job_sequence"]
_next_facility_sequence = parsed[&"next_facility_sequence"]
_quarantined = parsed[&"quarantined"]
_quarantine_reason = parsed[&"quarantine_reason"]
return true
func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
var raw_version: Variant = snapshot.get(&"schema_version", null)
if not raw_version is int or int(raw_version) not in [LEGACY_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
return {&"ok": false}
var version := int(raw_version)
var top_level_keys: Array[StringName] = [
&"schema_version", &"default_build_minutes", &"current_minute",
&"next_job_sequence", &"next_facility_sequence", &"jobs", &"facilities",
]
if version == SNAPSHOT_VERSION:
top_level_keys.append_array([&"quarantined", &"quarantine_reason"])
if not _has_exact_keys(snapshot, top_level_keys):
return {&"ok": false}
for counter_key: StringName in [
&"default_build_minutes", &"current_minute", &"next_job_sequence", &"next_facility_sequence",
]:
if not snapshot[counter_key] is int:
return {&"ok": false}
var restored_default := int(snapshot[&"default_build_minutes"])
var restored_minute := int(snapshot[&"current_minute"])
var job_sequence := int(snapshot[&"next_job_sequence"])
var facility_sequence := int(snapshot[&"next_facility_sequence"])
if (
restored_default < 1
or restored_default > MAX_BUILD_MINUTES
or restored_minute < 0
or job_sequence < 1
or job_sequence > MAX_SEQUENCE + 1
or facility_sequence < 1
or facility_sequence > MAX_SEQUENCE + 1
):
return {&"ok": false}
var saved_jobs: Variant = snapshot[&"jobs"]
var saved_facilities: Variant = snapshot[&"facilities"]
if (
not saved_jobs is Array
or not saved_facilities is Array
or saved_jobs.size() > MAX_JOBS
or saved_facilities.size() > MAX_FACILITIES
):
return {&"ok": false}
var quarantined := false
var quarantine_reason := ""
if version == SNAPSHOT_VERSION:
if not snapshot[&"quarantined"] is bool or not snapshot[&"quarantine_reason"] is String:
return {&"ok": false}
quarantined = snapshot[&"quarantined"]
quarantine_reason = snapshot[&"quarantine_reason"]
if quarantined == quarantine_reason.is_empty():
return {&"ok": false}
var restored_jobs: Dictionary = {}
var progress_minutes: Dictionary = {}
for job_data: Variant in saved_jobs:
if not job_data is Dictionary or not _validate_saved_job(job_data, version):
return {&"ok": false}
var construction_data: Dictionary = job_data.duplicate(true)
construction_data.erase(&"last_progress_at")
var job := ConstructionJobType.from_snapshot(construction_data)
if job == null or restored_jobs.has(job.job_id):
return {&"ok": false}
restored_jobs[job.job_id] = job
var last_progress := -1
if version == SNAPSHOT_VERSION:
last_progress = int(job_data[&"last_progress_at"])
elif job.work_units_done > 0:
last_progress = maxi(job.started_at, restored_minute)
progress_minutes[job.job_id] = last_progress
var restored_facilities: Dictionary = {}
for facility_data: Variant in saved_facilities:
if not facility_data is Dictionary or not _validate_saved_facility(facility_data):
return {&"ok": false}
var instance := _deserialize_facility(facility_data)
var instance_id: StringName = instance[&"instance_id"]
if restored_facilities.has(instance_id):
return {&"ok": false}
restored_facilities[instance_id] = instance
return {
&"ok": true,
&"jobs": restored_jobs,
&"facilities": restored_facilities,
&"last_progress_minutes": progress_minutes,
&"default_build_minutes": restored_default,
&"current_minute": restored_minute,
&"next_job_sequence": job_sequence,
&"next_facility_sequence": facility_sequence,
&"quarantined": quarantined,
&"quarantine_reason": quarantine_reason,
}
func _validate_saved_job(saved: Dictionary, version: int) -> bool:
var keys: Array[StringName] = [
&"job_id", &"definition_id", &"origin", &"rotation", &"layer", &"reserved_cells",
&"progress", &"state", &"state_reason", &"assigned_builder_ids", &"construction_cost",
&"build_minutes", &"work_units_done", &"created_at", &"started_at", &"completed_at",
&"cancelled_at", &"facility_instance_id",
]
if version == SNAPSHOT_VERSION:
keys.append(&"last_progress_at")
if not _has_exact_keys(saved, keys):
return false
for text_key: StringName in [
&"job_id", &"definition_id", &"state", &"state_reason", &"facility_instance_id",
]:
if not saved[text_key] is String:
return false
if String(saved[&"job_id"]).is_empty() or String(saved[&"definition_id"]).is_empty():
return false
if _parse_saved_cell(saved[&"origin"]) == null:
return false
var cells: Variant = _parse_saved_cells(saved[&"reserved_cells"])
if cells == null or cells.is_empty():
return false
var origin: Vector3i = _parse_saved_cell(saved[&"origin"])
for integer_key: StringName in [
&"rotation", &"layer", &"progress", &"build_minutes", &"work_units_done",
&"created_at", &"started_at", &"completed_at", &"cancelled_at",
]:
if not saved[integer_key] is int:
return false
if version == SNAPSHOT_VERSION and not saved[&"last_progress_at"] is int:
return false
if int(saved[&"rotation"]) not in [0, 90, 180, 270] or int(saved[&"layer"]) not in [0, 1]:
return false
if origin.z != int(saved[&"layer"]) or not cells.has(origin):
return false
for cell: Vector3i in cells:
if cell.z != int(saved[&"layer"]):
return false
if not _valid_cost(saved[&"construction_cost"]):
return false
if not saved[&"assigned_builder_ids"] is Array:
return false
var builder_set: Dictionary = {}
for builder_id: Variant in saved[&"assigned_builder_ids"]:
if not builder_id is String or String(builder_id).is_empty() or builder_set.has(builder_id):
return false
builder_set[builder_id] = true
var progress := int(saved[&"progress"])
var build_minutes := int(saved[&"build_minutes"])
var work_done := int(saved[&"work_units_done"])
if (
progress < 0
or progress > MAX_PROGRESS
or build_minutes < 1
or build_minutes > MAX_BUILD_MINUTES
or work_done < 0
or work_done > MAX_WORK_UNITS
or progress != mini(MAX_PROGRESS, work_done / build_minutes)
or int(saved[&"created_at"]) < 0
or int(saved[&"started_at"]) < -1
or int(saved[&"completed_at"]) < -1
or int(saved[&"cancelled_at"]) < -1
):
return false
if work_done == 0 and int(saved[&"started_at"]) != -1:
return false
if work_done > 0 and int(saved[&"started_at"]) < 0:
return false
if version == SNAPSHOT_VERSION:
var last_progress := int(saved[&"last_progress_at"])
if last_progress < -1:
return false
if work_done == 0 and last_progress != -1:
return false
if work_done > 0 and last_progress < int(saved[&"started_at"]):
return false
var state := String(saved[&"state"])
if state not in ConstructionJobType.State.keys():
return false
var facility_id := String(saved[&"facility_instance_id"])
match state:
"COMPLETED":
if progress != MAX_PROGRESS or int(saved[&"completed_at"]) < 0 or int(saved[&"cancelled_at"]) != -1 or facility_id.is_empty() or not String(saved[&"state_reason"]).is_empty():
return false
"CANCELLED":
if int(saved[&"cancelled_at"]) < 0 or int(saved[&"completed_at"]) != -1 or not facility_id.is_empty() or not String(saved[&"state_reason"]).is_empty():
return false
"PLANNED":
if work_done != 0 or progress != 0 or int(saved[&"started_at"]) != -1 or not String(saved[&"state_reason"]).is_empty():
return false
"BUILDING":
if work_done <= 0 or progress >= MAX_PROGRESS or not String(saved[&"state_reason"]).is_empty():
return false
"BLOCKED":
if String(saved[&"state_reason"]).is_empty():
return false
_:
if int(saved[&"completed_at"]) != -1 or int(saved[&"cancelled_at"]) != -1 or not facility_id.is_empty():
return false
if state not in ["COMPLETED", "CANCELLED"] and (
int(saved[&"completed_at"]) != -1
or int(saved[&"cancelled_at"]) != -1
or not facility_id.is_empty()
):
return false
return true
func _validate_saved_facility(saved: Dictionary) -> bool:
var keys: Array[StringName] = [
&"instance_id", &"definition_id", &"origin", &"rotation", &"layer", &"occupied_cells",
&"construction_cost", &"source_job_id", &"state", &"completed_at",
]
if not _has_exact_keys(saved, keys):
return false
for text_key: StringName in [&"instance_id", &"definition_id", &"source_job_id", &"state"]:
if not saved[text_key] is String or String(saved[text_key]).is_empty():
return false
if String(saved[&"state"]) != "ACTIVE" or _parse_saved_cell(saved[&"origin"]) == null:
return false
var cells: Variant = _parse_saved_cells(saved[&"occupied_cells"])
if cells == null or cells.is_empty():
return false
var origin: Vector3i = _parse_saved_cell(saved[&"origin"])
for integer_key: StringName in [&"rotation", &"layer", &"completed_at"]:
if not saved[integer_key] is int:
return false
if (
int(saved[&"rotation"]) not in [0, 90, 180, 270]
or int(saved[&"layer"]) not in [0, 1]
or int(saved[&"completed_at"]) < 0
):
return false
if origin.z != int(saved[&"layer"]) or not cells.has(origin):
return false
for cell: Vector3i in cells:
if cell.z != int(saved[&"layer"]):
return false
return _valid_cost(saved[&"construction_cost"])
func _validate_restored_state(parsed: Dictionary) -> bool:
var restored_jobs: Dictionary = parsed[&"jobs"]
var restored_facilities: Dictionary = parsed[&"facilities"]
var progress_minutes: Dictionary = parsed[&"last_progress_minutes"]
if (
(not restored_jobs.is_empty() or not restored_facilities.is_empty())
and _missing_methods(_world, [&"is_in_bounds", &"get_occupant"]).size() > 0
):
return false
var job_high_water := 0
var facility_high_water := 0
for job_id: Variant in restored_jobs:
if not job_id is StringName or not progress_minutes.has(job_id):
return false
var job: ConstructionJob = restored_jobs[job_id]
if not _strict_bool_call(_world, &"is_in_bounds", [job.origin]):
return false
for cell: Vector3i in job.reserved_cells:
if not _strict_bool_call(_world, &"is_in_bounds", [cell]):
return false
var sequence := _sequence_from_id(String(job_id), "construction_")
if sequence < 1:
return false
job_high_water = maxi(job_high_water, sequence)
if job.state == ConstructionJobType.State.COMPLETED:
if not restored_facilities.has(job.facility_instance_id):
return false
elif job.state != ConstructionJobType.State.CANCELLED:
for cell: Vector3i in job.reserved_cells:
if _world_occupant(cell) != job.job_id:
return false
for instance_id: Variant in restored_facilities:
if not instance_id is StringName:
return false
var sequence := _sequence_from_id(String(instance_id), "facility_")
if sequence < 1:
return false
facility_high_water = maxi(facility_high_water, sequence)
var instance: Dictionary = restored_facilities[instance_id]
if not _strict_bool_call(_world, &"is_in_bounds", [instance[&"origin"]]):
return false
var source_id: StringName = instance[&"source_job_id"]
if not restored_jobs.has(source_id):
return false
var source_job: ConstructionJob = restored_jobs[source_id]
if (
source_job.state != ConstructionJobType.State.COMPLETED
or source_job.facility_instance_id != instance_id
or source_job.definition_id != instance[&"definition_id"]
or source_job.origin != instance[&"origin"]
or source_job.rotation != int(instance[&"rotation"])
or source_job.layer != int(instance[&"layer"])
or source_job.reserved_cells != instance[&"occupied_cells"]
or source_job.construction_cost != instance[&"construction_cost"]
or source_job.completed_at != int(instance[&"completed_at"])
):
return false
for cell: Vector3i in instance[&"occupied_cells"]:
if _world_occupant(cell) != instance_id:
return false
return (
job_high_water < int(parsed[&"next_job_sequence"])
and facility_high_water < int(parsed[&"next_facility_sequence"])
)
static func _parse_saved_cell(value: Variant) -> Variant:
if not value is Array or value.size() != 3:
return null
if not value[0] is int or not value[1] is int or not value[2] is int:
return null
return Vector3i(int(value[0]), int(value[1]), int(value[2]))
static func _parse_saved_cells(value: Variant) -> Variant:
if not value is Array:
return null
var result: Array[Vector3i] = []
var seen: Dictionary = {}
for raw_cell: Variant in value:
var cell: Variant = _parse_saved_cell(raw_cell)
if cell == null or seen.has(cell):
return null
seen[cell] = true
result.append(cell)
return result
static func _sequence_from_id(value: String, prefix: String) -> int:
if not value.begins_with(prefix) or value.length() != prefix.length() + 8:
return -1
var suffix := value.substr(prefix.length())
if not suffix.is_valid_int():
return -1
return int(suffix)
func _validate_cell_constraints(
report: PlacementReport,
origin: Vector3i,
expected_zone: int,
expected_layer: int
) -> void:
if origin.z != expected_layer:
report.layer_mismatch_cells.append(origin)
if expected_zone == 2 and origin.y != 0:
report.zone_mismatch_cells.append(origin)
for cell: Vector3i in report.cells:
var in_bounds := _strict_bool_call(_world, &"is_in_bounds", [cell])
if not in_bounds:
report.out_of_bounds_cells.append(cell)
if cell.z != expected_layer and not report.layer_mismatch_cells.has(cell):
report.layer_mismatch_cells.append(cell)
if not in_bounds:
continue
var zone_mismatch := false
if expected_zone == 2:
zone_mismatch = cell.y < 0
elif expected_zone != 0:
zone_mismatch = not _zone_matches(expected_zone, _world.call(&"get_zone", cell))
if zone_mismatch and not report.zone_mismatch_cells.has(cell):
report.zone_mismatch_cells.append(cell)
var occupant: Variant = _world.call(&"get_occupant", cell)
if not _occupant_is_empty(occupant):
report.occupied_cells.append(cell)
if cell.y < 0 and not _strict_bool_call(_world, &"is_excavated", [cell]):
report.unexcavated_cells.append(cell)
if not report.out_of_bounds_cells.is_empty():
report.add_error(
PlacementReportType.ERROR_OUT_OF_BOUNDS,
"One or more footprint cells are outside the current map bounds.",
report.out_of_bounds_cells
)
if not report.zone_mismatch_cells.is_empty():
report.add_error(
PlacementReportType.ERROR_ZONE_MISMATCH,
"The facility is not permitted in one or more map zones.",
report.zone_mismatch_cells,
{&"expected_zone": expected_zone}
)
if not report.layer_mismatch_cells.is_empty():
report.add_error(
PlacementReportType.ERROR_LAYER_MISMATCH,
"The footprint layer differs from the facility definition.",
report.layer_mismatch_cells,
{&"expected_layer": expected_layer}
)
if not report.occupied_cells.is_empty():
report.add_error(
PlacementReportType.ERROR_OVERLAP,
"One or more footprint cells are already occupied or reserved.",
report.occupied_cells
)
if not report.unexcavated_cells.is_empty():
report.add_error(
PlacementReportType.ERROR_NOT_EXCAVATED,
"Underground facilities require every footprint cell to be excavated first.",
report.unexcavated_cells
)
func _validate_support(report: PlacementReport, definition: Variant) -> void:
if report.cells.is_empty():
return
var weight_per_cell := maxi(int(_definition_value(definition, &"weight_per_cell", 1)), 1)
var support_result: Variant = _call_by_supported_arity(
_world,
&"validate_support",
[report.cells, weight_per_cell]
)
var supported := true
var reason := "The structure does not have sufficient support."
if support_result is bool:
supported = support_result
if not supported:
report.unsupported_cells.assign(report.cells)
elif support_result is Array:
for cell: Variant in support_result:
if cell is Vector3i:
report.unsupported_cells.append(cell)
supported = report.unsupported_cells.is_empty()
elif support_result is Dictionary:
var raw_ok: Variant = support_result.get(&"ok", support_result.get("ok", null))
supported = raw_ok if raw_ok is bool else false
reason = String(support_result.get(&"reason", support_result.get("reason", reason)))
var unsupported: Variant = support_result.get(
&"unsupported_cells",
support_result.get("unsupported_cells", [])
)
if unsupported is Array:
for cell: Variant in unsupported:
if cell is Vector3i:
report.unsupported_cells.append(cell)
if not supported and report.unsupported_cells.is_empty():
report.unsupported_cells.assign(report.cells)
elif support_result is Object and support_result.has_method(&"is_success"):
var raw_success: Variant = support_result.call(&"is_success")
supported = raw_success if raw_success is bool else false
if not supported:
report.unsupported_cells.assign(report.cells)
if not supported:
report.add_error(
PlacementReportType.ERROR_UNSUPPORTED,
reason,
report.unsupported_cells
)
func _validate_cost(report: PlacementReport) -> void:
if _resource_state == null:
return
if _can_afford(report.total_cost):
return
for resource_name: Variant in report.total_cost:
var required := int(report.total_cost[resource_name])
var available := _resource_amount(StringName(resource_name))
if available < required:
report.missing_resources[StringName(resource_name)] = required - available
report.add_error(
PlacementReportType.ERROR_INSUFFICIENT_RESOURCES,
"Available resources do not cover the complete construction cost.",
[],
{&"missing": report.missing_resources.duplicate(true)}
)
func _complete_job(job: ConstructionJob, absolute_minute: int) -> CommandResult:
if _facilities.size() >= MAX_FACILITIES or _next_facility_sequence > MAX_SEQUENCE:
job.mark_blocked("Facility capacity has been reached.")
_emit_job_changed(job)
return _failure("FACILITY_LIMIT", job.state_reason, {&"job_id": job.job_id})
var instance_id := StringName("facility_%08d" % _next_facility_sequence)
var world_snapshot := _capture_world_state(job.reserved_cells)
if not _world_release(job.reserved_cells, job.job_id):
var release_rolled_back := _restore_world_state(world_snapshot)
job.mark_blocked("The construction reservation could not be released for completion.")
if not release_rolled_back:
_quarantine("Construction completion release failed and world state could not be restored.")
_emit_job_changed(job)
return _failure("RELEASE_FAILED", job.state_reason, {
&"job_id": job.job_id,
&"rolled_back": release_rolled_back,
&"rollback_failed": not release_rolled_back,
})
if not _world_occupy(job.reserved_cells, instance_id, &"facility"):
var rolled_back := _restore_world_state(world_snapshot)
job.mark_blocked("Facility occupancy failed during construction completion.")
if not rolled_back:
_quarantine("Facility occupancy failed and the construction reservation could not be restored.")
_emit_job_changed(job)
return _failure("FACILITY_OCCUPANCY_FAILED", job.state_reason, {
&"job_id": job.job_id,
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
})
var instance := {
&"instance_id": instance_id,
&"definition_id": job.definition_id,
&"origin": job.origin,
&"rotation": job.rotation,
&"layer": job.layer,
&"occupied_cells": job.reserved_cells.duplicate(),
&"construction_cost": job.construction_cost.duplicate(true),
&"source_job_id": job.job_id,
&"state": &"ACTIVE",
&"completed_at": absolute_minute,
}
_facilities[instance_id] = instance
_next_facility_sequence += 1
job.mark_completed(instance_id, absolute_minute)
_emit_job_changed(job)
_emit_facility_completed(instance)
return CommandResultType.succeeded(0, {
&"job_id": job.job_id,
&"instance_id": instance_id,
&"progress": job.progress,
})
func _world_cells_for_footprint(
origin: Vector3i,
footprint: Vector2i,
rotation: int,
layer: int
) -> Array[Vector3i]:
var argument_count := _method_argument_count(_world, &"cells_for_footprint")
var returned: Variant
if argument_count <= 2:
var rotated_footprint := footprint
if rotation == 90 or rotation == 270:
rotated_footprint = Vector2i(footprint.y, footprint.x)
returned = _world.callv(&"cells_for_footprint", [origin, rotated_footprint])
else:
returned = _call_by_supported_arity(
_world,
&"cells_for_footprint",
[origin, footprint, rotation, layer]
)
var cells: Array[Vector3i] = []
var seen: Dictionary = {}
if not returned is Array or returned.size() != footprint.x * footprint.y:
return cells
for cell: Variant in returned:
if not cell is Vector3i or seen.has(cell):
return []
seen[cell] = true
cells.append(cell)
return cells
func _world_occupy(cells: Array[Vector3i], occupant_id: StringName, kind: StringName) -> bool:
if _world == null or not _world.has_method(&"occupy"):
return false
var argument_count := _method_argument_count(_world, &"occupy")
var arguments: Array = [occupant_id, cells, kind]
var returned: Variant = _world.callv(&"occupy", arguments.slice(0, argument_count))
return _operation_succeeded(returned)
func _world_release(cells: Array[Vector3i], occupant_id: StringName) -> bool:
if _world == null or not _world.has_method(&"release"):
return false
var argument_count := _method_argument_count(_world, &"release")
var arguments: Array = [occupant_id] if argument_count == 1 else [occupant_id, cells]
var returned: Variant = _world.callv(&"release", arguments.slice(0, argument_count))
return _operation_succeeded(returned)
func _definition_build_minutes(definition: Variant) -> int:
var configured := int(_definition_value(definition, &"build_minutes", 0))
return configured if configured > 0 else maxi(default_build_minutes, 1)
func _definition_is_valid(definition: Variant) -> bool:
if definition == null:
return false
if definition is Object and definition.has_method(&"is_valid"):
var valid_result: Variant = definition.call(&"is_valid")
if not valid_result is bool or not valid_result:
return false
var raw_id: Variant = _definition_value(definition, &"id", null)
var footprint: Variant = _definition_value(definition, &"footprint", null)
var cost: Variant = _definition_value(definition, &"construction_cost", null)
var minutes: Variant = _definition_value(definition, &"build_minutes", default_build_minutes)
return (
(raw_id is String or raw_id is StringName)
and not String(raw_id).is_empty()
and footprint is Vector2i
and footprint.x > 0
and footprint.y > 0
and _valid_cost(cost)
and minutes is int
and int(minutes) > 0
and int(minutes) <= MAX_BUILD_MINUTES
)
func _definition_value(source: Variant, property_name: StringName, fallback: Variant) -> Variant:
if source is Dictionary:
return source.get(property_name, source.get(String(property_name), fallback))
if source == null or not source.has_method(&"get_property_list"):
return fallback
for property: Dictionary in source.get_property_list():
if StringName(property.get(&"name", &"")) == property_name:
return source.get(property_name)
return fallback
func _can_afford(cost: Dictionary) -> bool:
if _resource_state == null:
return false
if _resource_state.has_method(&"can_afford"):
return _operation_succeeded(_resource_state.call(&"can_afford", cost))
for resource_name: Variant in cost:
if _resource_amount(StringName(resource_name)) < int(cost[resource_name]):
return false
return true
func _spend_cost(cost: Dictionary) -> bool:
if _resource_state == null:
return false
if _resource_state.has_method(&"spend"):
return _operation_succeeded(_resource_state.call(&"spend", cost))
if not _can_afford(cost):
return false
var deltas: Dictionary = {}
for resource_name: Variant in cost:
deltas[StringName(resource_name)] = -int(cost[resource_name])
if _resource_state.has_method(&"apply_resource_deltas"):
return _operation_succeeded(_resource_state.call(&"apply_resource_deltas", deltas))
return false
func _refund_cost(refund: Dictionary) -> bool:
if refund.is_empty():
return true
if _resource_state == null:
return false
if _resource_state.has_method(&"apply_resource_deltas"):
return _operation_succeeded(_resource_state.call(&"apply_resource_deltas", refund))
if _resource_state.has_method(&"add_resource"):
for resource_name: Variant in refund:
var returned: Variant = _resource_state.call(
&"add_resource",
StringName(resource_name),
int(refund[resource_name])
)
if not returned is int and not (returned is bool and returned):
return false
return true
return false
func _resource_amount(resource_name: StringName) -> int:
if _resource_state == null:
return 0
if _resource_state.has_method(&"get_resource"):
var returned: Variant = _resource_state.call(&"get_resource", resource_name)
return returned if returned is int else 0
var resources: Variant = _definition_value(_resource_state, &"resources", {})
if resources is Dictionary:
var amount: Variant = resources.get(resource_name, resources.get(String(resource_name), 0))
return amount if amount is int else 0
return 0
func _capture_resource_state() -> Variant:
var captured := {
&"amounts": {
&"gold": _resource_amount(&"gold"),
&"materials": _resource_amount(&"materials"),
},
&"has_native": false,
&"native_snapshot": null,
}
if _resource_state != null and _resource_state.has_method(&"create_snapshot"):
captured[&"has_native"] = true
captured[&"native_snapshot"] = _resource_state.call(&"create_snapshot")
return captured
func _restore_resource_state(snapshot: Variant) -> bool:
if not snapshot is Dictionary or _resource_state == null:
return false
var expected: Variant = snapshot.get(&"amounts", null)
if not expected is Dictionary:
return false
if bool(snapshot.get(&"has_native", false)) and _resource_state.has_method(&"restore_snapshot"):
_operation_succeeded(_resource_state.call(&"restore_snapshot", snapshot.get(&"native_snapshot", null)))
elif _resource_state.has_method(&"apply_resource_deltas"):
var deltas: Dictionary = {}
for resource_name: StringName in [&"gold", &"materials"]:
var expected_amount: Variant = expected.get(resource_name, null)
if not expected_amount is int:
return false
deltas[resource_name] = int(expected_amount) - _resource_amount(resource_name)
_operation_succeeded(_resource_state.call(&"apply_resource_deltas", deltas))
else:
return false
return _resource_amounts_match(expected)
func _capture_world_state(cells: Array[Vector3i]) -> Dictionary:
var occupants: Array[Dictionary] = []
for cell: Vector3i in cells:
var occupant := _world_occupant(cell)
var kind := &""
if _world != null and _world.has_method(&"get_occupant_kind"):
var raw_kind: Variant = _world.call(&"get_occupant_kind", cell)
if raw_kind is String or raw_kind is StringName:
kind = StringName(raw_kind)
if kind.is_empty() and not occupant.is_empty():
kind = &"construction" if String(occupant).begins_with("construction_") else &"facility"
occupants.append({&"cell": cell, &"occupant_id": occupant, &"kind": kind})
var captured := {
&"occupants": occupants,
&"has_native": false,
&"native_snapshot": null,
}
if _world != null and _world.has_method(&"create_snapshot"):
captured[&"has_native"] = true
captured[&"native_snapshot"] = _world.call(&"create_snapshot")
return captured
func _restore_world_state(snapshot: Variant) -> bool:
if not snapshot is Dictionary or _world == null:
return false
var occupants: Variant = snapshot.get(&"occupants", null)
if not occupants is Array:
return false
if bool(snapshot.get(&"has_native", false)) and _world.has_method(&"restore_snapshot"):
_operation_succeeded(_world.call(&"restore_snapshot", snapshot.get(&"native_snapshot", null)))
else:
var cells: Array[Vector3i] = []
var ids_to_release: Dictionary = {}
for entry: Variant in occupants:
if not entry is Dictionary or not entry.get(&"cell", null) is Vector3i:
return false
var cell: Vector3i = entry[&"cell"]
cells.append(cell)
var current_id := _world_occupant(cell)
if not current_id.is_empty():
ids_to_release[current_id] = true
for occupant_id: Variant in ids_to_release:
_world_release(cells, StringName(occupant_id))
var groups: Dictionary = {}
for entry: Dictionary in occupants:
var expected_id := StringName(entry[&"occupant_id"])
if expected_id.is_empty():
continue
if not groups.has(expected_id):
groups[expected_id] = {&"cells": [], &"kind": StringName(entry[&"kind"])}
groups[expected_id][&"cells"].append(entry[&"cell"])
for expected_id: Variant in groups:
var restored_cells: Array[Vector3i] = []
restored_cells.assign(groups[expected_id][&"cells"])
_world_occupy(restored_cells, StringName(expected_id), groups[expected_id][&"kind"])
return _world_state_matches(occupants)
func _world_state_matches(occupants: Variant) -> bool:
if not occupants is Array:
return false
for entry: Variant in occupants:
if not entry is Dictionary or not entry.get(&"cell", null) is Vector3i:
return false
if _world_occupant(entry[&"cell"]) != StringName(entry.get(&"occupant_id", &"")):
return false
return true
func _world_occupant(cell: Vector3i) -> StringName:
if _world == null or not _world.has_method(&"get_occupant"):
return &""
var returned: Variant = _world.call(&"get_occupant", cell)
if returned is String or returned is StringName:
return StringName(returned)
return &""
func _resource_amounts_match(expected: Dictionary) -> bool:
for resource_name: StringName in [&"gold", &"materials"]:
var amount: Variant = expected.get(resource_name, null)
if not amount is int or _resource_amount(resource_name) != int(amount):
return false
return true
func _serialize_facility(instance: Dictionary) -> Dictionary:
var serialized := instance.duplicate(true)
serialized[&"instance_id"] = String(instance.get(&"instance_id", &""))
serialized[&"definition_id"] = String(instance.get(&"definition_id", &""))
serialized[&"source_job_id"] = String(instance.get(&"source_job_id", &""))
serialized[&"state"] = String(instance.get(&"state", &"ACTIVE"))
var origin: Vector3i = instance.get(&"origin", Vector3i.ZERO)
serialized[&"origin"] = [origin.x, origin.y, origin.z]
var saved_cells: Array[Array] = []
for cell: Vector3i in instance.get(&"occupied_cells", []):
saved_cells.append([cell.x, cell.y, cell.z])
serialized[&"occupied_cells"] = saved_cells
return serialized
func _deserialize_facility(saved: Dictionary) -> Dictionary:
var instance := saved.duplicate(true)
instance[&"instance_id"] = StringName(saved.get(&"instance_id", ""))
instance[&"definition_id"] = StringName(saved.get(&"definition_id", ""))
instance[&"source_job_id"] = StringName(saved.get(&"source_job_id", ""))
instance[&"state"] = StringName(saved.get(&"state", "ACTIVE"))
var origin: Variant = saved.get(&"origin", [])
if origin is Array and origin.size() == 3:
instance[&"origin"] = Vector3i(int(origin[0]), int(origin[1]), int(origin[2]))
else:
instance[&"origin"] = Vector3i.ZERO
var cells: Array[Vector3i] = []
var saved_cells: Variant = saved.get(&"occupied_cells", [])
if saved_cells is Array:
for cell: Variant in saved_cells:
if cell is Array and cell.size() == 3:
cells.append(Vector3i(int(cell[0]), int(cell[1]), int(cell[2])))
instance[&"occupied_cells"] = cells
instance[&"construction_cost"] = _normalize_cost(saved.get(&"construction_cost", {}))
return instance
func _not_found(entity_name: String, entity_id: StringName) -> CommandResult:
return CommandResultType.failed(
CommandResultType.Code.NOT_FOUND,
0,
"%s '%s' was not found." % [entity_name, entity_id],
{&"id": entity_id}
)
func _invalid_state(message: String, entity_id: StringName) -> CommandResult:
return CommandResultType.failed(
CommandResultType.Code.VALIDATION_FAILED,
0,
message,
{&"id": entity_id}
)
func _failure(code: StringName, message: String, details: Dictionary = {}) -> CommandResult:
var merged := details.duplicate(true)
merged[&"code"] = code
return CommandResultType.failed(
CommandResultType.Code.TRANSACTION_APPLY_FAILED,
0,
message,
merged
)
func _notification_unavailable_result(code: StringName, message: String) -> CommandResult:
return _failure(code, message)
func is_quarantined() -> bool:
return _quarantined
func get_quarantine_reason() -> String:
return _quarantine_reason
func _mutation_allowed() -> bool:
return (
not _mutation_in_progress
and not _notification_commit_active
and not _quarantined
and (_notification_staging_active or not has_deferred_notifications())
)
func _unavailable_result() -> CommandResult:
var code: StringName = &"SERVICE_QUARANTINED" if _quarantined else &"REENTRANT_MUTATION"
var message := (
"Construction service is quarantined: %s" % _quarantine_reason
if _quarantined
else "A reentrant construction mutation was rejected."
)
return _failure(code, message, {&"quarantine_reason": _quarantine_reason})
func _quarantine(reason: String) -> void:
_quarantined = true
_quarantine_reason = reason
static func _normalize_cost(source: Variant) -> Dictionary:
var normalized: Dictionary = {}
if not _valid_cost(source):
return normalized
for resource_name: StringName in [&"gold", &"materials"]:
normalized[resource_name] = int(source[resource_name])
return normalized
static func _valid_cost(source: Variant) -> bool:
if not source is Dictionary or source.size() != 2:
return false
for resource_name: StringName in [&"gold", &"materials"]:
if not source.has(resource_name):
return false
var amount: Variant = source[resource_name]
if not amount is int or int(amount) < 0:
return false
return true
static func _has_exact_keys(source: Dictionary, expected: Array) -> bool:
if source.size() != expected.size():
return false
for key: Variant in expected:
if not source.has(key):
return false
return true
static func _normalize_rotation(value: int) -> int:
var normalized := value % 360
if normalized < 0:
normalized += 360
return (normalized / 90) * 90
static func _zone_matches(expected_zone: int, actual_zone: Variant) -> bool:
if actual_zone is int:
return int(actual_zone) == expected_zone
var normalized := String(actual_zone).to_upper()
match expected_zone:
1:
return normalized == "ABOVE_GROUND"
2:
return normalized == "BOUNDARY"
3:
return normalized == "UNDERGROUND"
return false
static func _occupant_is_empty(occupant: Variant) -> bool:
if occupant == null:
return true
match typeof(occupant):
TYPE_BOOL:
return not bool(occupant)
TYPE_INT, TYPE_FLOAT:
return float(occupant) == 0.0
TYPE_STRING, TYPE_STRING_NAME:
return String(occupant).is_empty()
return false
static func _operation_succeeded(returned: Variant) -> bool:
if returned is bool:
return returned
if returned is Object and returned.has_method(&"is_success"):
var result: Variant = returned.call(&"is_success")
return result if result is bool else false
return false
static func _strict_bool_call(target: Variant, method_name: StringName, arguments: Array) -> bool:
if target == null or not target.has_method(method_name):
return false
var returned: Variant = target.callv(method_name, arguments)
return returned if returned is bool else false
static func _missing_methods(target: Variant, methods: Array[StringName]) -> Array[String]:
var missing: Array[String] = []
if target == null:
for method_name: StringName in methods:
missing.append(String(method_name))
return missing
for method_name: StringName in methods:
if not target.has_method(method_name):
missing.append(String(method_name))
return missing
static func _call_by_supported_arity(target: Variant, method_name: StringName, arguments: Array) -> Variant:
if target == null or not target.has_method(method_name):
return null
var argument_count := mini(arguments.size(), _method_argument_count(target, method_name))
return target.callv(method_name, arguments.slice(0, argument_count))
static func _method_argument_count(target: Variant, method_name: StringName) -> int:
if target == null or not target.has_method(method_name):
return 0
for method: Dictionary in target.get_method_list():
if StringName(method.get(&"name", &"")) != method_name:
continue
var declared_arguments: Variant = method.get(&"args", [])
if declared_arguments is Array:
return declared_arguments.size()
return 0
static func _sorted_string_name_keys(source: Dictionary) -> Array[StringName]:
var as_strings: Array[String] = []
for key: Variant in source:
as_strings.append(String(key))
as_strings.sort()
var result: Array[StringName] = []
for value: String in as_strings:
result.append(StringName(value))
return result