Files
TheTree_mock/scripts/construction/excavation_service.gd

1035 lines
36 KiB
GDScript

class_name ExcavationService
extends RefCounted
## Deterministic underground excavation queue.
##
## Excavation is owned per spatial X/Y cell. Completing one job unlocks both
## FRONT and BACK through WorldService.set_excavated(). A job can be paused and
## resumed without losing work, and every new cell must remain connected to the
## boundary through already excavated cells.
signal job_created(job: Dictionary)
signal job_changed(job: Dictionary)
signal excavation_completed(report: Dictionary)
const CommandResultType = preload("res://scripts/core/command_result.gd")
const SNAPSHOT_VERSION := 2
const LEGACY_SNAPSHOT_VERSION := 1
const MAX_PROGRESS := 10_000
const NORMAL_WORK_RATE := 10_000
const MAX_WORK_RATE := 1_000_000
const MAX_WORK_UNITS := 9_000_000_000_000_000
const MAX_HARDNESS := 3
const MAX_JOBS := 20_000
const MAX_SEQUENCE := 99_999_999
const STATE_ACTIVE: StringName = &"ACTIVE"
const STATE_PAUSED: StringName = &"PAUSED"
const STATE_COMPLETED: StringName = &"COMPLETED"
const ERROR_OUT_OF_BOUNDS: StringName = &"OUT_OF_BOUNDS"
const ERROR_NOT_UNDERGROUND: StringName = &"NOT_UNDERGROUND"
const ERROR_ALREADY_EXCAVATED: StringName = &"ALREADY_EXCAVATED"
const ERROR_ALREADY_RESERVED: StringName = &"ALREADY_RESERVED"
const ERROR_NO_EXCAVATED_NEIGHBOR: StringName = &"NO_EXCAVATED_NEIGHBOR"
const ERROR_NO_ENTRANCE_CONNECTION: StringName = &"NO_ENTRANCE_CONNECTION"
const ERROR_SUPPORT_REQUIRED: StringName = &"SUPPORT_REQUIRED"
const ERROR_INSUFFICIENT_RESOURCES: StringName = &"INSUFFICIENT_RESOURCES"
const ERROR_MISSING_DEPENDENCY: StringName = &"MISSING_DEPENDENCY"
const HARDNESS_RULES := {
0: {&"gold": 20, &"materials": 1, &"minutes": 30},
1: {&"gold": 50, &"materials": 1, &"minutes": 60},
2: {&"gold": 100, &"materials": 2, &"minutes": 120},
3: {&"gold": 200, &"materials": 4, &"minutes": 240},
}
var current_minute := 0
var last_error := ""
var _world: Variant
var _resource_state: Variant
var _hardness: Dictionary = {}
var _jobs: Dictionary = {}
var _cell_jobs: Dictionary = {}
var _next_job_sequence := 1
var _external_mutation_in_progress := false
var _quarantined := false
var _quarantine_reason := ""
func _init(world: Variant = null, resource_state: Variant = null) -> void:
_world = world
_resource_state = resource_state
func configure(world: Variant, resource_state: Variant) -> ExcavationService:
_world = world
_resource_state = resource_state
return self
func set_hardness(cell_or_position: Variant, hardness: int) -> bool:
if not _mutation_allowed():
return false
var position: Variant = _spatial_position(cell_or_position)
if position == null or hardness < 0 or hardness > MAX_HARDNESS:
return _fail("Excavation hardness or position is invalid.")
var front := Vector3i(position.x, position.y, 0)
if not _world_ready() or not _call_bool(_world, &"is_in_bounds", [front]) or position.y >= 0:
return _fail("Hardness can only be assigned to an in-bounds underground cell.")
if _cell_jobs.has(position) or _call_bool(_world, &"is_excavated", [front]):
return _fail("Hardness is immutable after excavation has started.")
if hardness == 0:
_hardness.erase(position)
else:
_hardness[position] = hardness
last_error = ""
return true
func get_hardness(cell_or_position: Variant) -> int:
var position: Variant = _spatial_position(cell_or_position)
if position == null:
return -1
return int(_hardness.get(position, 0))
func get_rule(hardness: int) -> Dictionary:
if not HARDNESS_RULES.has(hardness):
return {}
return (HARDNESS_RULES[hardness] as Dictionary).duplicate(true)
func validate_excavation(cell: Vector3i) -> Dictionary:
var position := Vector2i(cell.x, cell.y)
var errors: Array[StringName] = []
var hardness := get_hardness(position)
var rule := get_rule(hardness)
var report := {
&"ok": false,
&"position": position,
&"cells": [
Vector3i(position.x, position.y, 0),
Vector3i(position.x, position.y, 1),
],
&"hardness": hardness,
&"cost": _rule_cost(rule),
&"build_minutes": int(rule.get(&"minutes", 0)),
&"errors": errors,
&"missing_resources": {},
&"support_required": false,
&"entrance_connected": false,
}
if _quarantined or not _world_ready():
_add_error(errors, ERROR_MISSING_DEPENDENCY)
report[&"errors"] = errors
return report
var front := Vector3i(position.x, position.y, 0)
if not _call_bool(_world, &"is_in_bounds", [front]):
_add_error(errors, ERROR_OUT_OF_BOUNDS)
if position.y >= 0:
_add_error(errors, ERROR_NOT_UNDERGROUND)
if errors.is_empty() and _call_bool(_world, &"is_excavated", [front]):
_add_error(errors, ERROR_ALREADY_EXCAVATED)
if _cell_jobs.has(position):
_add_error(errors, ERROR_ALREADY_RESERVED)
if errors.is_empty():
var excavated_neighbors := _excavated_neighbors(position)
if excavated_neighbors.is_empty():
_add_error(errors, ERROR_NO_EXCAVATED_NEIGHBOR)
else:
var connected := false
for neighbor: Vector2i in excavated_neighbors:
if _has_boundary_connection(neighbor):
connected = true
break
report[&"entrance_connected"] = connected
if not connected:
_add_error(errors, ERROR_NO_ENTRANCE_CONNECTION)
if errors.is_empty() and hardness >= 2:
var hard_component := _prospective_hard_span(position)
var support_required := hard_component.size() >= 4
report[&"support_required"] = support_required
if support_required and not _component_has_support(hard_component):
_add_error(errors, ERROR_SUPPORT_REQUIRED)
var missing: Dictionary = {}
if _resource_state == null:
_add_error(errors, ERROR_MISSING_DEPENDENCY)
else:
for resource_name: StringName in [&"gold", &"materials"]:
var required := int(report[&"cost"].get(resource_name, 0))
var available := _resource_amount(resource_name)
if available < required:
missing[resource_name] = required - available
if not missing.is_empty():
_add_error(errors, ERROR_INSUFFICIENT_RESOURCES)
report[&"missing_resources"] = missing
report[&"errors"] = errors
report[&"ok"] = errors.is_empty()
return report
func start_excavation(cell: Vector3i, 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("EXCAVATION_LIMIT", "Excavation job capacity has been reached.")
var report := validate_excavation(cell)
if not bool(report[&"ok"]):
return _failure("INVALID_EXCAVATION", "Excavation cannot start.", {&"report": report})
var resource_snapshot: Variant = _capture_resource_state()
var cost: Dictionary = report[&"cost"]
_external_mutation_in_progress = true
if not _spend_cost(cost):
var rolled_back := _restore_resource_state(resource_snapshot, cost)
_external_mutation_in_progress = false
if not rolled_back:
_quarantine("Excavation payment failed after mutating resources and could not be rolled back.")
return _failure("PAYMENT_FAILED", "Excavation resources changed before payment.", {
&"cost": cost,
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
})
_external_mutation_in_progress = false
var job_id := StringName("excavation_%08d" % _next_job_sequence)
var position: Vector2i = report[&"position"]
var job := {
&"job_id": job_id,
&"position": position,
&"hardness": int(report[&"hardness"]),
&"cost": cost.duplicate(true),
&"build_minutes": int(report[&"build_minutes"]),
&"work_units_done": 0,
&"progress": 0,
&"state": STATE_ACTIVE,
&"state_reason": &"",
&"created_at": current_minute if created_at < 0 else created_at,
&"started_at": -1,
&"completed_at": -1,
&"last_progress_at": -1,
}
if _cell_jobs.has(position):
_external_mutation_in_progress = true
var rolled_back := _restore_resource_state(resource_snapshot, cost)
_external_mutation_in_progress = false
if not rolled_back:
_quarantine("Excavation payment rollback failed after a concurrent reservation.")
return _failure("RESERVATION_FAILED", "The excavation cell was reserved concurrently.", {
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
})
_jobs[job_id] = job
_cell_jobs[position] = job_id
_next_job_sequence += 1
last_error = ""
job_created.emit(job.duplicate(true))
return _success({&"job_id": job_id, &"report": report})
func get_job(job_id: StringName) -> Dictionary:
if not _jobs.has(job_id):
return {}
return (_jobs[job_id] as Dictionary).duplicate(true)
func get_jobs() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for job_id: StringName in _sorted_job_ids():
result.append((_jobs[job_id] as Dictionary).duplicate(true))
return result
func pause_job(job_id: StringName, reason: StringName = &"PLAYER_PAUSED") -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
if not _jobs.has(job_id):
return _failure("NOT_FOUND", "Excavation job does not exist.")
var job: Dictionary = _jobs[job_id]
if job[&"state"] == STATE_COMPLETED:
return _failure("TERMINAL_STATE", "Completed excavation cannot be paused.")
if job[&"state"] == STATE_PAUSED:
var replay := _success({&"job_id": job_id, &"progress": job[&"progress"]})
replay.is_replay = true
return replay
job[&"state"] = STATE_PAUSED
job[&"state_reason"] = reason
job_changed.emit(job.duplicate(true))
return _success({&"job_id": job_id, &"progress": job[&"progress"]})
func resume_job(job_id: StringName) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
if not _jobs.has(job_id):
return _failure("NOT_FOUND", "Excavation job does not exist.")
var job: Dictionary = _jobs[job_id]
if job[&"state"] == STATE_COMPLETED:
return _failure("TERMINAL_STATE", "Completed excavation cannot resume.")
if job[&"state"] == STATE_ACTIVE:
var replay := _success({&"job_id": job_id, &"progress": job[&"progress"]})
replay.is_replay = true
return replay
if not _can_resume(job):
return _failure("EXCAVATION_DISCONNECTED", "The excavation route no longer reaches an entrance.")
job[&"state"] = STATE_ACTIVE
job[&"state_reason"] = &""
job_changed.emit(job.duplicate(true))
return _success({&"job_id": job_id, &"progress": job[&"progress"]})
func advance_one_minute(
absolute_minute: int = -1,
work_rates: Dictionary = {}
) -> Array[CommandResult]:
if not _mutation_allowed():
return [_unavailable_result()]
if absolute_minute < 0:
current_minute += 1
else:
current_minute = maxi(current_minute, absolute_minute)
var results: Array[CommandResult] = []
for job_id: StringName in _sorted_job_ids():
var job: Dictionary = _jobs[job_id]
if job[&"state"] != STATE_ACTIVE:
continue
var rate := int(work_rates.get(job_id, NORMAL_WORK_RATE))
results.append(progress_job(job_id, rate, current_minute))
return results
func progress_job(
job_id: StringName,
work_rate_basis_points: int = NORMAL_WORK_RATE,
absolute_minute: int = -1
) -> CommandResult:
if not _mutation_allowed():
return _unavailable_result()
if not _jobs.has(job_id):
return _failure("NOT_FOUND", "Excavation job does not exist.")
var job: Dictionary = _jobs[job_id]
if job[&"state"] == STATE_COMPLETED:
return _failure("TERMINAL_STATE", "Completed excavation cannot progress.")
if job[&"state"] == STATE_PAUSED:
return _failure("PAUSED", "Paused excavation must be resumed first.")
if work_rate_basis_points <= 0 or work_rate_basis_points > MAX_WORK_RATE:
return _failure("INVALID_WORK_RATE", "Excavation work rate must be positive.")
if not _can_resume(job):
job[&"state"] = STATE_PAUSED
job[&"state_reason"] = &"ENTRANCE_DISCONNECTED"
job_changed.emit(job.duplicate(true))
return _failure("EXCAVATION_DISCONNECTED", "Excavation lost its entrance connection.")
var minute := current_minute if absolute_minute < 0 else absolute_minute
if minute < 0:
return _failure("INVALID_MINUTE", "Excavation progress minute cannot be negative.")
if minute <= int(job[&"last_progress_at"]):
var replay := _success({&"job_id": job_id, &"progress": job[&"progress"], &"state": job[&"state"]})
replay.is_replay = true
return replay
if int(job[&"started_at"]) < 0:
job[&"started_at"] = minute
job[&"last_progress_at"] = minute
job[&"work_units_done"] = mini(
MAX_WORK_UNITS,
int(job[&"work_units_done"]) + work_rate_basis_points
)
job[&"progress"] = mini(
MAX_PROGRESS,
int(job[&"work_units_done"]) / int(job[&"build_minutes"])
)
if int(job[&"progress"]) < MAX_PROGRESS:
job_changed.emit(job.duplicate(true))
return _success({&"job_id": job_id, &"progress": job[&"progress"], &"state": STATE_ACTIVE})
return _complete_job(job_id, minute)
func validate_invariants() -> Dictionary:
var errors: Array[Dictionary] = []
if _next_job_sequence < 1 or _next_job_sequence > MAX_SEQUENCE + 1:
errors.append({&"code": &"INVALID_SEQUENCE"})
if _jobs.size() > MAX_JOBS or _cell_jobs.size() > _jobs.size():
errors.append({&"code": &"JOB_LIMIT"})
var seen_positions: Dictionary = {}
var high_water := 0
for job_id: StringName in _sorted_job_ids():
var job: Variant = _jobs[job_id]
var parsed := _validate_job(job, job_id)
if not bool(parsed[&"ok"]):
errors.append({&"code": &"INVALID_JOB", &"job_id": job_id})
continue
var position: Vector2i = job[&"position"]
if seen_positions.has(position):
errors.append({&"code": &"DUPLICATE_JOB_CELL", &"job_id": job_id})
seen_positions[position] = true
var sequence := _sequence_from_job_id(job_id)
if sequence <= 0:
errors.append({&"code": &"INVALID_JOB_ID", &"job_id": job_id})
else:
high_water = maxi(high_water, sequence)
if job[&"state"] == STATE_COMPLETED:
if _cell_jobs.has(position):
errors.append({&"code": &"COMPLETED_CELL_RESERVED", &"job_id": job_id})
if _world_ready() and not _call_bool(_world, &"is_excavated", [Vector3i(position.x, position.y, 0)]):
errors.append({&"code": &"COMPLETED_CELL_NOT_EXCAVATED", &"job_id": job_id})
else:
if _cell_jobs.get(position, &"") != job_id:
errors.append({&"code": &"MISSING_CELL_RESERVATION", &"job_id": job_id})
if _world_ready() and _call_bool(_world, &"is_excavated", [Vector3i(position.x, position.y, 0)]):
errors.append({&"code": &"RESERVED_CELL_ALREADY_EXCAVATED", &"job_id": job_id})
if job[&"state"] == STATE_ACTIVE and not _can_resume(job):
errors.append({&"code": &"ACTIVE_JOB_DISCONNECTED", &"job_id": job_id})
var rule := get_rule(int(job[&"hardness"]))
if (
int(job[&"hardness"]) != int(_hardness.get(position, 0))
or job[&"cost"] != _rule_cost(rule)
or int(job[&"build_minutes"]) != int(rule.get(&"minutes", 0))
):
errors.append({&"code": &"JOB_RULE_MISMATCH", &"job_id": job_id})
if high_water >= _next_job_sequence:
errors.append({&"code": &"SEQUENCE_NOT_ABOVE_HIGH_WATER"})
for position: Variant in _hardness:
if (
not position is Vector2i
or not _hardness[position] is int
or int(_hardness[position]) < 1
or int(_hardness[position]) > MAX_HARDNESS
or position.y >= 0
or (_world_ready() and not _call_bool(_world, &"is_in_bounds", [Vector3i(position.x, position.y, 0)]))
):
errors.append({&"code": &"INVALID_HARDNESS"})
if _quarantined and _quarantine_reason.is_empty():
errors.append({&"code": &"INVALID_QUARANTINE"})
if not _quarantined and not _quarantine_reason.is_empty():
errors.append({&"code": &"INVALID_QUARANTINE"})
return {&"ok": errors.is_empty(), &"errors": errors}
func create_snapshot() -> Dictionary:
var hardness_entries: Array[Dictionary] = []
var hardness_positions: Array[Vector2i] = []
for position: Variant in _hardness:
hardness_positions.append(position)
hardness_positions.sort_custom(_position_less)
for position: Vector2i in hardness_positions:
hardness_entries.append({&"position": [position.x, position.y], &"hardness": _hardness[position]})
var jobs: Array[Dictionary] = []
for job_id: StringName in _sorted_job_ids():
jobs.append(_serialize_job(_jobs[job_id]))
return {
&"schema_version": SNAPSHOT_VERSION,
&"current_minute": current_minute,
&"next_job_sequence": _next_job_sequence,
&"hardness": hardness_entries,
&"jobs": jobs,
&"quarantined": _quarantined,
&"quarantine_reason": _quarantine_reason,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
if _external_mutation_in_progress:
last_error = "Excavation state cannot be restored during an external mutation."
return false
var parsed := _parse_snapshot(snapshot)
if not bool(parsed[&"ok"]):
last_error = String(parsed[&"error"])
return false
var previous_hardness := _hardness
var previous_jobs := _jobs
var previous_cell_jobs := _cell_jobs
var previous_minute := current_minute
var previous_sequence := _next_job_sequence
var previous_quarantined := _quarantined
var previous_quarantine_reason := _quarantine_reason
_hardness = parsed[&"hardness"]
_jobs = parsed[&"jobs"]
_cell_jobs = parsed[&"cell_jobs"]
current_minute = parsed[&"current_minute"]
_next_job_sequence = parsed[&"next_job_sequence"]
_quarantined = parsed[&"quarantined"]
_quarantine_reason = parsed[&"quarantine_reason"]
var invariants := validate_invariants()
if not bool(invariants[&"ok"]):
_hardness = previous_hardness
_jobs = previous_jobs
_cell_jobs = previous_cell_jobs
current_minute = previous_minute
_next_job_sequence = previous_sequence
_quarantined = previous_quarantined
_quarantine_reason = previous_quarantine_reason
last_error = "Excavation snapshot violates live world invariants."
return false
last_error = ""
return true
func _complete_job(job_id: StringName, minute: int) -> CommandResult:
var job: Dictionary = _jobs[job_id]
var position: Vector2i = job[&"position"]
var front := Vector3i(position.x, position.y, 0)
var world_snapshot := _capture_world_state()
_external_mutation_in_progress = true
var committed := _call_bool(_world, &"set_excavated", [front, true])
_external_mutation_in_progress = false
if not committed:
_external_mutation_in_progress = true
var rolled_back := _restore_world_state(world_snapshot, front)
_external_mutation_in_progress = false
job[&"state"] = STATE_PAUSED
job[&"state_reason"] = &"WORLD_COMMIT_FAILED"
if not rolled_back:
_quarantine("World excavation commit and rollback both failed.")
job_changed.emit(job.duplicate(true))
return _failure("WORLD_COMMIT_FAILED", "The world rejected completed excavation.", {
&"rolled_back": rolled_back,
&"rollback_failed": not rolled_back,
})
job[&"progress"] = MAX_PROGRESS
job[&"state"] = STATE_COMPLETED
job[&"state_reason"] = &""
job[&"completed_at"] = minute
_cell_jobs.erase(position)
var report := {
&"job_id": job_id,
&"position": position,
&"cells": [front, Vector3i(position.x, position.y, 1)],
&"hardness": job[&"hardness"],
&"completed_at": minute,
}
job_changed.emit(job.duplicate(true))
excavation_completed.emit(report.duplicate(true))
return _success(report)
func _can_resume(job: Dictionary) -> bool:
var position: Vector2i = job[&"position"]
if not _world_ready() or _call_bool(_world, &"is_excavated", [Vector3i(position.x, position.y, 0)]):
return false
for neighbor: Vector2i in _excavated_neighbors(position):
if _has_boundary_connection(neighbor):
return true
return false
func _excavated_neighbors(position: Vector2i) -> Array[Vector2i]:
var result: Array[Vector2i] = []
for neighbor: Vector2i in _neighbors(position):
var front := Vector3i(neighbor.x, neighbor.y, 0)
if _call_bool(_world, &"is_in_bounds", [front]) and _call_bool(_world, &"is_excavated", [front]):
result.append(neighbor)
return result
func _has_boundary_connection(start: Vector2i) -> bool:
if start.y == 0:
return true
var queue: Array[Vector2i] = [start]
var visited := {start: true}
var index := 0
while index < queue.size():
var current := queue[index]
index += 1
for neighbor: Vector2i in _neighbors(current):
if visited.has(neighbor):
continue
var front := Vector3i(neighbor.x, neighbor.y, 0)
if not _call_bool(_world, &"is_in_bounds", [front]) or not _call_bool(_world, &"is_excavated", [front]):
continue
if neighbor.y == 0:
return true
visited[neighbor] = true
queue.append(neighbor)
return false
func _prospective_hard_span(position: Vector2i) -> Array[Vector2i]:
var horizontal := _hard_run(position, Vector2i.RIGHT)
var vertical := _hard_run(position, Vector2i.DOWN)
if vertical.size() > horizontal.size():
return vertical
return horizontal
func _hard_run(position: Vector2i, direction: Vector2i) -> Array[Vector2i]:
var result: Array[Vector2i] = [position]
for sign_value: int in [-1, 1]:
var cursor := position + direction * sign_value
while get_hardness(cursor) >= 2:
var front := Vector3i(cursor.x, cursor.y, 0)
if not _call_bool(_world, &"is_in_bounds", [front]) or not _call_bool(_world, &"is_excavated", [front]):
break
result.append(cursor)
cursor += direction * sign_value
result.sort_custom(_position_less)
return result
func _component_has_support(component: Array[Vector2i]) -> bool:
if not _world.has_method(&"get_support_capacity") or not _world.has_method(&"is_support_anchor"):
return false
var candidates: Dictionary = {}
for position: Vector2i in component:
candidates[position] = true
for neighbor: Vector2i in _neighbors(position):
candidates[neighbor] = true
for position: Variant in candidates:
for layer: int in [0, 1]:
var cell := Vector3i(position.x, position.y, layer)
if not _call_bool(_world, &"is_in_bounds", [cell]):
continue
if _call_bool(_world, &"is_support_anchor", [cell]) or _call_int(_world, &"get_support_capacity", [cell], 0) > 0:
return true
return false
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, &"error": "Unsupported excavation snapshot version."}
var version := int(raw_version)
var top_level_keys: Array[StringName] = [
&"schema_version", &"current_minute", &"next_job_sequence", &"hardness", &"jobs",
]
if version == SNAPSHOT_VERSION:
top_level_keys.append_array([&"quarantined", &"quarantine_reason"])
if not _has_exact_keys(snapshot, top_level_keys):
return {&"ok": false, &"error": "Excavation snapshot schema is not exact."}
var minute: Variant = snapshot.get(&"current_minute", null)
var sequence: Variant = snapshot.get(&"next_job_sequence", null)
var hardness_values: Variant = snapshot.get(&"hardness", null)
var job_values: Variant = snapshot.get(&"jobs", null)
if (
not minute is int
or int(minute) < 0
or not sequence is int
or int(sequence) < 1
or int(sequence) > MAX_SEQUENCE + 1
):
return {&"ok": false, &"error": "Excavation snapshot counters are invalid."}
if not hardness_values is Array or not job_values is Array or job_values.size() > MAX_JOBS:
return {&"ok": false, &"error": "Excavation snapshot collections are invalid."}
var quarantined := false
var quarantine_reason := ""
if version == SNAPSHOT_VERSION:
var raw_quarantined: Variant = snapshot[&"quarantined"]
var raw_reason: Variant = snapshot[&"quarantine_reason"]
if not raw_quarantined is bool or not raw_reason is String:
return {&"ok": false, &"error": "Excavation quarantine state is invalid."}
quarantined = raw_quarantined
quarantine_reason = raw_reason
if quarantined == quarantine_reason.is_empty():
return {&"ok": false, &"error": "Excavation quarantine state is inconsistent."}
var hardness: Dictionary = {}
for value: Variant in hardness_values:
if not value is Dictionary or not _has_exact_keys(value, [&"position", &"hardness"]):
return {&"ok": false, &"error": "Hardness entry is invalid."}
var position: Variant = _parse_position(value.get(&"position", null))
var amount: Variant = value.get(&"hardness", null)
if position == null or not amount is int or int(amount) < 1 or int(amount) > MAX_HARDNESS or hardness.has(position):
return {&"ok": false, &"error": "Hardness entry is invalid."}
hardness[position] = int(amount)
var jobs: Dictionary = {}
var cell_jobs: Dictionary = {}
for value: Variant in job_values:
if not value is Dictionary or not _validate_serialized_job(value, version):
return {&"ok": false, &"error": "Excavation job entry is invalid."}
var job := _deserialize_job(value, version, int(minute))
var job_id := StringName(job.get(&"job_id", &""))
if job_id.is_empty() or jobs.has(job_id) or not bool(_validate_job(job, job_id)[&"ok"]):
return {&"ok": false, &"error": "Excavation job entry is invalid."}
var position: Vector2i = job[&"position"]
if cell_jobs.has(position):
return {&"ok": false, &"error": "Excavation jobs overlap."}
jobs[job_id] = job
if job[&"state"] != STATE_COMPLETED:
cell_jobs[position] = job_id
return {
&"ok": true,
&"current_minute": int(minute),
&"next_job_sequence": int(sequence),
&"hardness": hardness,
&"jobs": jobs,
&"cell_jobs": cell_jobs,
&"quarantined": quarantined,
&"quarantine_reason": quarantine_reason,
}
func _validate_job(value: Variant, expected_id: StringName) -> Dictionary:
if not value is Dictionary:
return {&"ok": false}
var required: Array[StringName] = [
&"job_id", &"position", &"hardness", &"cost", &"build_minutes",
&"work_units_done", &"progress", &"state", &"state_reason", &"created_at",
&"started_at", &"completed_at", &"last_progress_at",
]
if not _has_exact_keys(value, required):
return {&"ok": false}
if not value[&"job_id"] is StringName or value[&"job_id"] != expected_id or not value[&"position"] is Vector2i:
return {&"ok": false}
for integer_key: StringName in [
&"hardness", &"build_minutes", &"work_units_done", &"progress", &"created_at",
&"started_at", &"completed_at", &"last_progress_at",
]:
if not value[integer_key] is int:
return {&"ok": false}
if int(value[&"hardness"]) < 0 or int(value[&"hardness"]) > MAX_HARDNESS:
return {&"ok": false}
if not _valid_cost(value[&"cost"]) or int(value[&"build_minutes"]) <= 0:
return {&"ok": false}
if (
int(value[&"work_units_done"]) < 0
or int(value[&"work_units_done"]) > MAX_WORK_UNITS
or int(value[&"progress"]) < 0
or int(value[&"progress"]) > MAX_PROGRESS
):
return {&"ok": false}
var expected_progress := mini(MAX_PROGRESS, int(value[&"work_units_done"]) / int(value[&"build_minutes"]))
if int(value[&"progress"]) != expected_progress:
return {&"ok": false}
if not value[&"state"] is StringName or not value[&"state_reason"] is StringName:
return {&"ok": false}
var state: StringName = value[&"state"]
if state not in [STATE_ACTIVE, STATE_PAUSED, STATE_COMPLETED]:
return {&"ok": false}
if (
int(value[&"created_at"]) < 0
or int(value[&"started_at"]) < -1
or int(value[&"completed_at"]) < -1
or int(value[&"last_progress_at"]) < -1
):
return {&"ok": false}
if int(value[&"work_units_done"]) == 0 and (int(value[&"started_at"]) != -1 or int(value[&"last_progress_at"]) != -1):
return {&"ok": false}
if int(value[&"work_units_done"]) > 0 and (
int(value[&"started_at"]) < 0
or int(value[&"last_progress_at"]) < int(value[&"started_at"])
):
return {&"ok": false}
if state == STATE_COMPLETED and (int(value[&"progress"]) != MAX_PROGRESS or int(value[&"completed_at"]) < 0):
return {&"ok": false}
if state != STATE_COMPLETED and int(value[&"completed_at"]) != -1:
return {&"ok": false}
return {&"ok": true}
func _validate_serialized_job(value: Dictionary, version: int) -> bool:
var required: Array[StringName] = [
&"job_id", &"position", &"hardness", &"cost", &"build_minutes",
&"work_units_done", &"progress", &"state", &"state_reason", &"created_at",
&"started_at", &"completed_at",
]
if version == SNAPSHOT_VERSION:
required.append(&"last_progress_at")
if not _has_exact_keys(value, required):
return false
if not value[&"job_id"] is String or String(value[&"job_id"]).is_empty():
return false
if _parse_position(value[&"position"]) == null:
return false
if not value[&"state"] is String or not value[&"state_reason"] is String:
return false
for integer_key: StringName in [
&"hardness", &"build_minutes", &"work_units_done", &"progress", &"created_at",
&"started_at", &"completed_at",
]:
if not value[integer_key] is int:
return false
if version == SNAPSHOT_VERSION and not value[&"last_progress_at"] is int:
return false
return _valid_cost(value[&"cost"])
func _serialize_job(job: Dictionary) -> Dictionary:
var result := job.duplicate(true)
var position: Vector2i = job[&"position"]
result[&"job_id"] = String(job[&"job_id"])
result[&"position"] = [position.x, position.y]
result[&"state"] = String(job[&"state"])
result[&"state_reason"] = String(job[&"state_reason"])
return result
func _deserialize_job(value: Dictionary, version: int, snapshot_minute: int) -> Dictionary:
var result := value.duplicate(true)
result[&"job_id"] = StringName(value[&"job_id"])
result[&"position"] = _parse_position(value[&"position"])
result[&"state"] = StringName(value[&"state"])
result[&"state_reason"] = StringName(value[&"state_reason"])
if version == LEGACY_SNAPSHOT_VERSION:
result[&"last_progress_at"] = -1
if int(result[&"work_units_done"]) > 0:
result[&"last_progress_at"] = maxi(int(result[&"started_at"]), snapshot_minute)
return result
func _world_ready() -> bool:
return (
_world != null
and _world.has_method(&"is_in_bounds")
and _world.has_method(&"is_excavated")
and _world.has_method(&"set_excavated")
)
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)
if returned is int:
return returned
return 0
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 _resource_state.has_method(&"apply_resource_deltas"):
return false
var deltas: Dictionary = {}
for resource_name: Variant in cost:
if _resource_amount(StringName(resource_name)) < int(cost[resource_name]):
return false
deltas[StringName(resource_name)] = -int(cost[resource_name])
return _operation_succeeded(_resource_state.call(&"apply_resource_deltas", deltas))
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, cost: Dictionary) -> bool:
if not snapshot is Dictionary or _resource_state == null:
return false
var attempted := false
if (
bool(snapshot.get(&"has_native", false))
and _resource_state.has_method(&"restore_snapshot")
):
attempted = true
_operation_succeeded(_resource_state.call(&"restore_snapshot", snapshot.get(&"native_snapshot", null)))
elif _resource_state.has_method(&"apply_resource_deltas"):
attempted = true
var expected: Variant = snapshot.get(&"amounts", null)
if not expected is Dictionary:
return false
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))
if not attempted:
return false
return _resource_amounts_match(snapshot.get(&"amounts", {}))
func _capture_world_state() -> Dictionary:
var captured := {
&"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, front: Vector3i) -> bool:
if _world == null:
return false
if (
snapshot is Dictionary
and bool(snapshot.get(&"has_native", false))
and _world.has_method(&"restore_snapshot")
):
_operation_succeeded(_world.call(&"restore_snapshot", snapshot.get(&"native_snapshot", null)))
elif _world.has_method(&"set_excavated"):
_call_bool(_world, &"set_excavated", [front, false])
else:
return false
return not _call_bool(_world, &"is_excavated", [front])
func _resource_amounts_match(expected: Variant) -> bool:
if not expected is Dictionary:
return false
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 _rule_cost(rule: Dictionary) -> Dictionary:
return {
&"gold": int(rule.get(&"gold", 0)),
&"materials": int(rule.get(&"materials", 0)),
}
func _sorted_job_ids() -> Array[StringName]:
var result: Array[StringName] = []
for job_id: Variant in _jobs:
result.append(StringName(job_id))
result.sort_custom(func(left: StringName, right: StringName) -> bool:
return String(left) < String(right)
)
return result
func _sequence_from_job_id(job_id: StringName) -> int:
var text := String(job_id)
if not text.begins_with("excavation_") or text.length() != 19:
return -1
var suffix := text.substr(11)
if not suffix.is_valid_int():
return -1
return int(suffix)
static func _spatial_position(value: Variant) -> Variant:
if value is Vector2i:
return value
if value is Vector3i:
return Vector2i(value.x, value.y)
return null
static func _parse_position(value: Variant) -> Variant:
if not value is Array or value.size() != 2 or not value[0] is int or not value[1] is int:
return null
return Vector2i(int(value[0]), int(value[1]))
static func _valid_cost(value: Variant) -> bool:
if not value is Dictionary or not _has_exact_keys(value, [&"gold", &"materials"]):
return false
return (
value[&"gold"] is int
and value[&"materials"] is int
and int(value[&"gold"]) >= 0
and int(value[&"materials"]) >= 0
)
static func _has_exact_keys(value: Dictionary, expected: Array) -> bool:
if value.size() != expected.size():
return false
for key: Variant in expected:
if not value.has(key):
return false
return true
static func _call_bool(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 _call_int(
target: Variant,
method_name: StringName,
arguments: Array,
fallback: int
) -> int:
if target == null or not target.has_method(method_name):
return fallback
var returned: Variant = target.callv(method_name, arguments)
return returned if returned is int else fallback
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 _neighbors(position: Vector2i) -> Array[Vector2i]:
return [
position + Vector2i.LEFT,
position + Vector2i.RIGHT,
position + Vector2i.UP,
position + Vector2i.DOWN,
]
static func _position_less(left: Vector2i, right: Vector2i) -> bool:
if left.y != right.y:
return left.y < right.y
return left.x < right.x
static func _add_error(errors: Array[StringName], code: StringName) -> void:
if not errors.has(code):
errors.append(code)
func _success(details: Dictionary = {}) -> CommandResult:
return CommandResultType.succeeded(0, details)
func is_quarantined() -> bool:
return _quarantined
func get_quarantine_reason() -> String:
return _quarantine_reason
func _mutation_allowed() -> bool:
if _external_mutation_in_progress:
last_error = "A reentrant excavation mutation was rejected."
return false
if _quarantined:
last_error = "Excavation service is quarantined: %s" % _quarantine_reason
return false
return true
func _unavailable_result() -> CommandResult:
var code: StringName = &"SERVICE_QUARANTINED" if _quarantined else &"REENTRANT_MUTATION"
return _failure(code, last_error, {&"quarantine_reason": _quarantine_reason})
func _quarantine(reason: String) -> void:
_quarantined = true
_quarantine_reason = reason
last_error = reason
func _failure(code: StringName, message: String, details: Dictionary = {}) -> CommandResult:
last_error = message
var merged := details.duplicate(true)
merged[&"code"] = code
return CommandResultType.failed(CommandResultType.Code.VALIDATION_FAILED, 0, message, merged)
func _fail(message: String) -> bool:
last_error = message
return false