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