Files
TheTree_mock/scripts/authority/authority_service.gd
T

1376 lines
50 KiB
GDScript

class_name AuthorityService
extends RefCounted
## Deterministic Authority aggregate. World visibility, path finding and
## financial/sanction application are explicit callback boundaries so this
## service never scans the map or mutates another domain directly.
signal heat_changed(old_value: int, new_value: int, source_id: StringName)
signal incident_created(incident: AuthorityIncident)
signal incident_detector_added(incident_id: StringName, detector_id: StringName)
signal evidence_registered(evidence: AuthorityEvidence)
signal evidence_discovered(evidence: AuthorityEvidence, method: StringName)
signal inspection_scheduled(inspection: AuthorityInspection)
signal inspection_state_changed(inspection_id: StringName, old_state: int, new_state: int, reason: StringName)
signal resolution_applied(inspection_id: StringName, resolution: Dictionary)
const HeatStateType = preload("res://scripts/authority/heat_state.gd")
const IncidentType = preload("res://scripts/authority/incident.gd")
const EvidenceType = preload("res://scripts/authority/evidence.gd")
const InspectionType = preload("res://scripts/authority/inspection.gd")
const CommandResultType = preload("res://scripts/core/command_result.gd")
const SNAPSHOT_VERSION := 1
const INSPECTION_EVALUATION_MINUTE := 360
const DEFAULT_HEAT_DECAY := 5
const DEFAULT_ACTIVE_CASE_FLOOR := 20
var current_minute := 0
var last_error := ""
var _state: HeatState = HeatStateType.new()
var _incidents: Dictionary = {}
var _evidence: Dictionary = {}
var _inspections: Dictionary = {}
var _scored_incident_points: Dictionary = {}
var _applied_resolutions: Dictionary = {}
var _evaluated_inspection_days: Dictionary = {}
var _next_inspection_sequence := 1
var _next_inspector_sequence := 1
var _line_of_sight := Callable()
var _path_query := Callable()
var _resolution_applier := Callable()
var _inspection_roll := Callable()
func configure_callbacks(
line_of_sight: Callable = Callable(),
path_query: Callable = Callable(),
resolution_applier: Callable = Callable(),
inspection_roll: Callable = Callable()
) -> AuthorityService:
_line_of_sight = line_of_sight
_path_query = path_query
_resolution_applier = resolution_applier
_inspection_roll = inspection_roll
return self
func get_authority_state() -> HeatState:
return _state
func get_incident(incident_id: StringName) -> AuthorityIncident:
return _incidents.get(incident_id) as AuthorityIncident
func get_incidents() -> Array[AuthorityIncident]:
var result: Array[AuthorityIncident] = []
for incident_id: StringName in _sorted_keys(_incidents):
result.append(_incidents[incident_id])
return result
func get_evidence(evidence_id: StringName) -> AuthorityEvidence:
return _evidence.get(evidence_id) as AuthorityEvidence
func get_all_evidence() -> Array[AuthorityEvidence]:
var result: Array[AuthorityEvidence] = []
for evidence_id: StringName in _sorted_keys(_evidence):
result.append(_evidence[evidence_id])
return result
func get_inspection(inspection_id: StringName) -> AuthorityInspection:
return _inspections.get(inspection_id) as AuthorityInspection
func get_inspections() -> Array[AuthorityInspection]:
var result: Array[AuthorityInspection] = []
for inspection_id: StringName in _sorted_keys(_inspections):
result.append(_inspections[inspection_id])
return result
func get_resolution(resolution_id: StringName) -> Dictionary:
if not _applied_resolutions.has(resolution_id):
return {}
return (_applied_resolutions[resolution_id] as Dictionary).duplicate(true)
func apply_heat_delta(
delta: int,
source_id: StringName,
absolute_minute: int,
reason: StringName = &""
) -> CommandResult:
var details := _state.apply_heat_delta(delta, source_id, absolute_minute, reason)
if not bool(details.get(&"ok", false)):
return _invalid("Heat delta requires a non-negative minute.", details)
if int(details[&"old_value"]) != int(details[&"new_value"]):
heat_changed.emit(int(details[&"old_value"]), int(details[&"new_value"]), source_id)
last_error = ""
return _success(details)
func set_suspicion(value: int) -> CommandResult:
var old_value := _state.suspicion
_state.set_suspicion(value)
last_error = ""
return _success({&"old_value": old_value, &"new_value": _state.suspicion})
func create_incident(
incident_id: StringName,
incident_type: StringName,
absolute_minute: int,
detector_id: StringName,
options: Dictionary = {}
) -> CommandResult:
var incident := IncidentType.new().configure(
incident_id,
incident_type,
absolute_minute,
options
)
return report_incident(incident, detector_id)
## incident_id is the authoritative deduplication key. A second detector is
## retained as audit metadata, but Heat and evidence are never added again.
func report_incident(value: Variant, detector_id: StringName) -> CommandResult:
if value == null or not value is AuthorityIncident:
return _invalid("AuthorityIncident is missing or invalid.")
var incoming: AuthorityIncident = value
if not incoming.is_valid() or detector_id.is_empty():
return _invalid("AuthorityIncident fails schema validation or has no detector.")
if incoming.state != IncidentType.State.OPEN:
return _invalid("New Incident reports must begin in OPEN state.")
if not incoming.evidence_ids.is_empty():
return _invalid("New Incident reports cannot arrive with pre-attached Evidence.")
if _incidents.has(incoming.incident_id):
var existing := get_incident(incoming.incident_id)
var detector_added := existing.add_detector(detector_id)
if detector_added:
incident_detector_added.emit(existing.incident_id, detector_id)
var replay := _success({
&"incident_id": existing.incident_id,
&"detector_added": detector_added,
&"heat_applied": 0,
&"canonical_severity": existing.severity,
})
replay.is_replay = true
last_error = ""
return replay
incoming.add_detector(detector_id)
_incidents[incoming.incident_id] = incoming
if incoming.state == IncidentType.State.OPEN:
_state.add_active_case(incoming.incident_id)
var heat_result := _state.apply_heat_delta(
incoming.heat_delta,
incoming.incident_id,
incoming.created_at,
&"INCIDENT"
)
if int(heat_result[&"old_value"]) != int(heat_result[&"new_value"]):
heat_changed.emit(
int(heat_result[&"old_value"]),
int(heat_result[&"new_value"]),
incoming.incident_id
)
incident_created.emit(incoming)
last_error = ""
return _success({
&"incident_id": incoming.incident_id,
&"detector_added": true,
&"heat_applied": int(heat_result[&"applied_delta"]),
})
func dismiss_incident(
incident_id: StringName,
reason: StringName = &"DISMISSED"
) -> CommandResult:
var incident := get_incident(incident_id)
if incident == null:
return _not_found("AuthorityIncident", incident_id)
if not incident.dismiss(reason):
return _invalid("Only an open Incident can be dismissed.")
_state.remove_active_case(incident_id)
last_error = ""
return _success({&"incident_id": incident_id, &"state": &"DISMISSED"})
func register_evidence(value: Variant) -> CommandResult:
if value == null or not value is AuthorityEvidence:
return _invalid("AuthorityEvidence is missing or invalid.")
var evidence: AuthorityEvidence = value
if not evidence.is_valid():
return _invalid("AuthorityEvidence fails schema validation.")
if evidence.is_discovered():
return _invalid("Live Evidence must enter as latent and use the discovery boundary.")
if _evidence.has(evidence.evidence_id):
return _duplicate("AuthorityEvidence", evidence.evidence_id)
if not evidence.incident_id.is_empty() and not _incidents.has(evidence.incident_id):
return _invalid("AuthorityEvidence references an unknown Incident.", {
&"incident_id": evidence.incident_id,
})
_evidence[evidence.evidence_id] = evidence
if not evidence.incident_id.is_empty():
get_incident(evidence.incident_id).add_evidence(evidence.evidence_id)
evidence_registered.emit(evidence)
if evidence.is_discovered():
_recompute_authority_evidence_points(current_minute)
last_error = ""
return _success({&"evidence_id": evidence.evidence_id})
func create_evidence(
evidence_id: StringName,
source: StringName,
severity: int,
options: Dictionary = {}
) -> CommandResult:
return register_evidence(EvidenceType.new().configure(evidence_id, source, severity, options))
## No visibility callback means no line-of-sight proof. Reaching the cell,
## trusted records/testimony and an already-known illegal facility are the only
## callback-free discovery bases.
func attempt_discover_evidence(
evidence_id: StringName,
inspector_id: StringName,
inspector_cell: Vector3i,
absolute_minute: int,
context: Dictionary = {}
) -> CommandResult:
var evidence := get_evidence(evidence_id)
if evidence == null:
return _not_found("AuthorityEvidence", evidence_id)
if inspector_id.is_empty() or absolute_minute < 0:
return _invalid("Evidence discovery requires an inspector and non-negative minute.")
if evidence.is_expired(absolute_minute):
return _invalid("Evidence has expired.", {&"reason": &"EVIDENCE_EXPIRED"})
if evidence.is_discovered():
var replay := _success({
&"evidence_id": evidence_id,
&"discovery_method": EvidenceType.DiscoveryMethod.keys()[evidence.discovery_method],
&"evidence_points": _state.evidence_points,
})
replay.is_replay = true
last_error = ""
return replay
var method := EvidenceType.DiscoveryMethod.LATENT
var reason: StringName = &"NO_VISIBILITY_PROOF"
if inspector_cell == evidence.location_cell:
method = EvidenceType.DiscoveryMethod.REACHED
elif bool(_value(context, &"trusted_source", false)):
method = EvidenceType.DiscoveryMethod.TRUSTED_SOURCE
elif not evidence.facility_id.is_empty() \
and _state.known_illegal_facility_ids.has(evidence.facility_id):
method = EvidenceType.DiscoveryMethod.KNOWN_ILLEGAL
elif inspector_cell.z != evidence.location_cell.z:
reason = &"DIFFERENT_LAYER"
elif not _line_of_sight.is_valid():
reason = &"LINE_OF_SIGHT_UNAVAILABLE"
else:
var visibility_context := context.duplicate(true)
visibility_context[&"evidence_id"] = evidence.evidence_id
visibility_context[&"facility_id"] = evidence.facility_id
var visible := bool(_line_of_sight.call(
inspector_cell,
evidence.location_cell,
visibility_context
))
if visible:
method = EvidenceType.DiscoveryMethod.LINE_OF_SIGHT
else:
reason = &"LINE_OF_SIGHT_BLOCKED"
if method == EvidenceType.DiscoveryMethod.LATENT:
return _invalid("Evidence is not discoverable from the inspector's position.", {
&"reason": reason,
&"evidence_id": evidence_id,
})
if not evidence.mark_discovered(inspector_id, absolute_minute, method):
return _invalid("Evidence discovery transition failed.")
if bool(_value(context, &"illegal_confirmed", false)) and not evidence.facility_id.is_empty():
_state.add_known_illegal_facility(evidence.facility_id)
_recompute_authority_evidence_points(absolute_minute)
evidence_discovered.emit(evidence, StringName(EvidenceType.DiscoveryMethod.keys()[method]))
last_error = ""
return _success({
&"evidence_id": evidence_id,
&"discovery_method": StringName(EvidenceType.DiscoveryMethod.keys()[method]),
&"evidence_points": _state.evidence_points,
})
func advance_time(
absolute_minute: int,
daily_heat_decay: int = DEFAULT_HEAT_DECAY,
active_case_floor: int = DEFAULT_ACTIVE_CASE_FLOOR
) -> CommandResult:
if absolute_minute < current_minute or daily_heat_decay < 0:
return _invalid("Authority time cannot move backwards and decay cannot be negative.")
var old_day := current_minute / 1_440
var new_day := absolute_minute / 1_440
var decay_records: Array[Dictionary] = []
for day_index: int in range(old_day + 1, new_day + 1):
var old_heat := _state.heat
var decay := _state.apply_daily_decay(day_index, daily_heat_decay, active_case_floor)
decay_records.append(decay)
if old_heat != _state.heat:
heat_changed.emit(old_heat, _state.heat, &"daily_decay")
current_minute = absolute_minute
_recompute_authority_evidence_points(absolute_minute)
last_error = ""
return _success({
&"current_minute": current_minute,
&"decays": decay_records,
&"evidence_points": _state.evidence_points,
})
func evaluate_inspection(absolute_minute: int, terms: Dictionary = {}) -> CommandResult:
if absolute_minute < 0 or absolute_minute % 1_440 != INSPECTION_EVALUATION_MINUTE:
return _invalid("Inspection selection is evaluated only at 06:00.")
var day_index := absolute_minute / 1_440
if _evaluated_inspection_days.has(day_index):
var replay := _success((_evaluated_inspection_days[day_index] as Dictionary).duplicate(true))
replay.is_replay = true
last_error = ""
return replay
if absolute_minute < _state.next_inspection_window:
return _invalid("Inspection evaluation is outside the next Window.", {
&"next_inspection_window": _state.next_inspection_window,
})
var base_chance := int(_value(terms, &"base_chance", 5))
var heat_weight := int(_value(terms, &"heat_weight", 1))
var suspicion_weight := int(_value(terms, &"suspicion_weight", 1))
var overdue_bonus := int(_value(
terms,
&"overdue_case_bonus",
20 if not _state.active_case_ids.is_empty() else 0
))
var reputation_protection := int(_value(terms, &"reputation_protection", 0))
var chance := clampi(
base_chance
+ _state.heat * heat_weight
+ _state.suspicion * suspicion_weight
+ overdue_bonus
- reputation_protection,
0,
95
)
var roll := -1
if _has_key(terms, &"roll"):
roll = clampi(int(_value(terms, &"roll", 99)), 0, 99)
elif _inspection_roll.is_valid():
roll = clampi(int(_inspection_roll.call(day_index, chance, terms.duplicate(true))), 0, 99)
else:
roll = _stable_percent("inspection:%d:%d:%d:%d" % [
day_index,
_state.heat,
_state.suspicion,
_state.active_case_ids.size(),
])
var selected := roll < chance
var details := {
&"day_index": day_index,
&"chance": chance,
&"roll": roll,
&"selected": selected,
&"inspection_id": &"",
}
if selected and not _has_open_inspection():
var scheduled := schedule_inspection(absolute_minute, terms)
if not scheduled.is_success():
return scheduled
details[&"inspection_id"] = scheduled.details[&"inspection_id"]
elif selected:
details[&"selected"] = false
details[&"reason"] = &"INSPECTION_ALREADY_ACTIVE"
_state.next_inspection_window = (day_index + 1) * 1_440 + INSPECTION_EVALUATION_MINUTE
_state.revision += 1
_evaluated_inspection_days[day_index] = details.duplicate(true)
last_error = ""
return _success(details)
func schedule_inspection(
scheduled_at: int,
options: Dictionary = {}
) -> CommandResult:
if scheduled_at < 0:
return _invalid("Inspection schedule requires a non-negative minute.")
var inspection_id := StringName(_value(options, &"inspection_id", &""))
var generated_inspection_id := inspection_id.is_empty()
if generated_inspection_id:
inspection_id = StringName("inspection_%08d" % _next_inspection_sequence)
if _inspections.has(inspection_id):
return _duplicate("AuthorityInspection", inspection_id)
var inspector_id := StringName(_value(options, &"inspector_id", &""))
var generated_inspector_id := inspector_id.is_empty()
if generated_inspector_id:
inspector_id = StringName("authority_inspector_%08d" % _next_inspector_sequence)
var inspection := InspectionType.new().configure(
inspection_id,
inspector_id,
scheduled_at,
options
)
if not inspection.is_valid():
return _invalid("AuthorityInspection fails schema validation.")
_inspections[inspection_id] = inspection
if generated_inspection_id:
_next_inspection_sequence += 1
if generated_inspector_id:
_next_inspector_sequence += 1
inspection_scheduled.emit(inspection)
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"inspector_id": inspector_id,
})
func start_inspection(inspection_id: StringName, absolute_minute: int) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.ARRIVING,
absolute_minute,
&"INSPECTION_STARTED"
)
func mark_inspector_arrived(inspection_id: StringName, absolute_minute: int) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.CHECK_IN,
absolute_minute,
&"ARRIVED"
)
func mark_inspection_blocked_entry(
inspection_id: StringName,
absolute_minute: int,
reason: StringName = &"BLOCKED_ENTRY"
) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.BLOCKED_ENTRY,
absolute_minute,
reason
)
func interrupt_inspection(
inspection_id: StringName,
absolute_minute: int,
reason: StringName = &"INTERRUPTED"
) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.INTERRUPTED,
absolute_minute,
reason
)
func mark_inspection_hostile(
inspection_id: StringName,
absolute_minute: int,
reason: StringName = &"HOSTILE"
) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.HOSTILE,
absolute_minute,
reason
)
func cancel_inspection(
inspection_id: StringName,
absolute_minute: int,
reason: StringName = &"CANCELLED"
) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.CANCELLED,
absolute_minute,
reason
)
func plan_patrol(
inspection_id: StringName,
candidates: Array,
absolute_minute: int,
time_budget_minutes: int = 120
) -> CommandResult:
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
if inspection.state != InspectionType.State.CHECK_IN:
return _invalid("Patrol can only be planned after CHECK_IN.")
if time_budget_minutes <= 0:
return _invalid("Patrol time budget must be positive.")
if absolute_minute < inspection.state_changed_at:
return _invalid("Patrol plan time cannot move backwards.")
var normalized: Array[Dictionary] = []
for raw_candidate: Variant in candidates:
if raw_candidate is Dictionary:
var waypoint := InspectionType.normalize_waypoint(raw_candidate)
if not waypoint.is_empty():
normalized.append(waypoint)
normalized.sort_custom(_waypoint_less)
var selected: Array[Dictionary] = []
var selected_ids: Dictionary = {}
var used_minutes := 0
for waypoint: Dictionary in normalized:
var waypoint_id: StringName = waypoint[&"waypoint_id"]
if selected_ids.has(waypoint_id):
continue
selected_ids[waypoint_id] = true
var cost := int(waypoint[&"travel_minutes"]) + int(waypoint[&"inspection_minutes"])
if used_minutes + cost > time_budget_minutes:
continue
selected.append(waypoint.duplicate(true))
used_minutes += cost
inspection.time_budget_minutes = time_budget_minutes
inspection.time_spent_minutes = 0
inspection.waypoint_route = selected
inspection.current_waypoint_index = 0
inspection.visited_waypoint_ids.clear()
inspection.inaccessible_waypoint_ids.clear()
inspection.revision += 1
var state_result := _change_inspection_state(
inspection_id,
InspectionType.State.PATROLLING,
absolute_minute,
&"PATROL_PLANNED"
)
if not state_result.is_success():
return state_result
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"waypoint_ids": _waypoint_ids(selected),
&"planned_minutes": used_minutes,
&"time_budget_minutes": time_budget_minutes,
})
## Moves only to the next chosen waypoint. It never scans other cells. A
## failed path immediately records a reason and starts a safe departure.
func advance_patrol(
inspection_id: StringName,
absolute_minute: int,
context: Dictionary = {}
) -> CommandResult:
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
if inspection.state != InspectionType.State.PATROLLING:
return _invalid("Inspection is not PATROLLING.")
if absolute_minute < inspection.state_changed_at:
return _invalid("Patrol time cannot move backwards.")
if inspection.current_waypoint_index >= inspection.waypoint_route.size():
return _change_inspection_state(
inspection_id,
InspectionType.State.REVIEWING,
absolute_minute,
&"PATROL_COMPLETE"
)
var waypoint: Dictionary = inspection.waypoint_route[inspection.current_waypoint_index]
var from_cell := inspection.entrance_cell
if inspection.current_waypoint_index > 0:
from_cell = inspection.waypoint_route[inspection.current_waypoint_index - 1][&"cell"]
var to_cell: Vector3i = waypoint[&"cell"]
var path_result := {
&"reachable": false,
&"reason": &"PATH_QUERY_UNAVAILABLE",
&"travel_minutes": int(waypoint[&"travel_minutes"]),
}
if _path_query.is_valid():
var path_context := context.duplicate(true)
path_context[&"inspection_id"] = inspection_id
path_context[&"waypoint_id"] = waypoint[&"waypoint_id"]
var raw_result: Variant = _path_query.call(from_cell, to_cell, path_context)
path_result = _normalize_path_result(raw_result, int(waypoint[&"travel_minutes"]))
if not bool(path_result[&"reachable"]):
var waypoint_id: StringName = waypoint[&"waypoint_id"]
inspection.add_inaccessible_waypoint(waypoint_id)
inspection.exit_reason = &"PATH_UNREACHABLE"
inspection.result_reason = StringName(path_result.get(&"reason", &"PATH_UNREACHABLE"))
var departure := _change_inspection_state(
inspection_id,
InspectionType.State.DEPARTING,
absolute_minute,
inspection.result_reason
)
if not departure.is_success():
return departure
return _success({
&"inspection_id": inspection_id,
&"reachable": false,
&"waypoint_id": waypoint_id,
&"exit_reason": inspection.exit_reason,
&"path_reason": inspection.result_reason,
})
var travel_minutes := maxi(int(path_result[&"travel_minutes"]), 0)
inspection.add_visited_waypoint(waypoint[&"waypoint_id"], travel_minutes)
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"reachable": true,
&"waypoint_id": waypoint[&"waypoint_id"],
&"cell": to_cell,
&"travel_minutes": travel_minutes,
})
func inspect_current_waypoint(
inspection_id: StringName,
evidence_ids: Array,
absolute_minute: int,
context: Dictionary = {}
) -> CommandResult:
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
if inspection.state != InspectionType.State.PATROLLING or inspection.current_waypoint_index <= 0:
return _invalid("Inspector must first reach a patrol waypoint.")
if absolute_minute < inspection.state_changed_at:
return _invalid("Evidence inspection time cannot move backwards.")
var waypoint: Dictionary = inspection.waypoint_route[inspection.current_waypoint_index - 1]
var inspector_cell: Vector3i = waypoint[&"cell"]
var discovered: Array[StringName] = []
var rejected: Array[Dictionary] = []
for evidence_id: StringName in _sorted_unique_names(evidence_ids):
var result := attempt_discover_evidence(
evidence_id,
inspection.inspector_id,
inspector_cell,
absolute_minute,
context
)
if result.is_success():
inspection.add_discovered_evidence(evidence_id)
discovered.append(evidence_id)
else:
rejected.append({
&"evidence_id": String(evidence_id),
&"message": result.message,
&"details": result.details.duplicate(true),
})
inspection.time_spent_minutes += int(waypoint[&"inspection_minutes"])
inspection.evidence_score = _calculate_inspection_evidence_score(inspection, absolute_minute)
inspection.revision += 1
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"waypoint_id": waypoint[&"waypoint_id"],
&"discovered_evidence_ids": discovered,
&"rejected": rejected,
&"evidence_score": inspection.evidence_score,
})
func finish_patrol(inspection_id: StringName, absolute_minute: int) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.REVIEWING,
absolute_minute,
&"PATROL_COMPLETE"
)
func begin_inspection_departure(
inspection_id: StringName,
absolute_minute: int,
reason: StringName = &"DEPARTING"
) -> CommandResult:
return _change_inspection_state(
inspection_id,
InspectionType.State.DEPARTING,
absolute_minute,
reason
)
func resolve_inspection(
inspection_id: StringName,
resolution_id: StringName,
absolute_minute: int,
options: Dictionary = {}
) -> CommandResult:
if resolution_id.is_empty():
return _invalid("Inspection resolution requires a resolution_id.")
if _applied_resolutions.has(resolution_id):
var existing: Dictionary = _applied_resolutions[resolution_id]
var replay := _success(existing.duplicate(true))
replay.is_replay = true
last_error = ""
return replay
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
if inspection.state != InspectionType.State.REVIEWING:
return _invalid("Inspection can only resolve from REVIEWING.")
if not inspection.resolution_id.is_empty():
return _invalid("Inspection already has a different resolution.")
inspection.evidence_score = _calculate_inspection_evidence_score(inspection, absolute_minute)
var outcome := _determine_resolution(inspection.evidence_score, options)
var fine_per_point := maxi(int(_value(options, &"fine_per_point", 10)), 0)
var fine_amount := 0
if outcome >= InspectionType.Result.FINE:
fine_amount = inspection.evidence_score * fine_per_point
var sanction := _sanction_for_result(outcome)
var payload := {
&"resolution_id": resolution_id,
&"inspection_id": inspection_id,
&"absolute_minute": absolute_minute,
&"result": StringName(InspectionType.Result.keys()[outcome]),
&"evidence_score": inspection.evidence_score,
&"fine_amount": fine_amount,
&"sanction": sanction,
&"evidence_ids": _names_to_strings(inspection.discovered_evidence_ids),
}
if absolute_minute < inspection.state_changed_at:
return _invalid("Inspection resolution time cannot move backwards.")
if _resolution_applier.is_valid():
var validation := _normalize_callback_result(
_resolution_applier.call(payload.duplicate(true), &"validate", null)
)
if not bool(validation[&"ok"]):
return _invalid("Inspection resolution validation failed.", validation)
var applied := _normalize_callback_result(
_resolution_applier.call(
payload.duplicate(true),
&"apply",
validation.get(&"token", null)
)
)
if not bool(applied[&"ok"]):
var rollback := _normalize_callback_result(_resolution_applier.call(
payload.duplicate(true),
&"rollback",
applied.get(&"token", validation.get(&"token", null))
))
var failure_details := applied.duplicate(true)
failure_details[&"rollback_ok"] = bool(rollback[&"ok"])
return _invalid("Inspection resolution application failed.", failure_details)
var transition := _change_inspection_state(
inspection_id,
InspectionType.State.RESOLVING,
absolute_minute,
&"RESOLUTION_STARTED"
)
if not transition.is_success():
return transition
if fine_amount > 0:
_state.add_fine_due(fine_amount)
inspection.result = outcome
inspection.resolution_id = resolution_id
inspection.result_reason = &"RESOLUTION_APPLIED"
inspection.revision += 1
_applied_resolutions[resolution_id] = payload.duplicate(true)
for evidence_id: StringName in inspection.discovered_evidence_ids:
var evidence := get_evidence(evidence_id)
if evidence == null or evidence.incident_id.is_empty():
continue
var incident := get_incident(evidence.incident_id)
if incident != null and incident.state == IncidentType.State.OPEN:
incident.resolve(resolution_id, &"INSPECTION_RESOLVED")
_state.remove_active_case(incident.incident_id)
_change_inspection_state(
inspection_id,
InspectionType.State.DEPARTING,
absolute_minute,
&"RESOLUTION_APPLIED"
)
resolution_applied.emit(inspection_id, payload.duplicate(true))
last_error = ""
return _success(payload)
func depart_inspection(inspection_id: StringName, absolute_minute: int) -> CommandResult:
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
var departure_reason := inspection.result_reason
if departure_reason.is_empty():
departure_reason = &"DEPARTED"
var result := _change_inspection_state(
inspection_id,
InspectionType.State.CLOSED,
absolute_minute,
departure_reason
)
if not result.is_success():
return result
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"state": &"CLOSED",
&"exit_reason": inspection.exit_reason,
&"result_reason": inspection.result_reason,
})
func validate_invariants() -> Dictionary:
return _validate_candidate_invariants(
_state,
_incidents,
_evidence,
_inspections,
_scored_incident_points,
_applied_resolutions,
current_minute
)
func create_snapshot() -> Dictionary:
var incidents: Array[Dictionary] = []
for incident: AuthorityIncident in get_incidents():
incidents.append(incident.create_snapshot())
var evidence_snapshots: Array[Dictionary] = []
for evidence: AuthorityEvidence in get_all_evidence():
evidence_snapshots.append(evidence.create_snapshot())
var inspections: Array[Dictionary] = []
for inspection: AuthorityInspection in get_inspections():
inspections.append(inspection.create_snapshot())
var scored_points: Array[Dictionary] = []
for score_key: StringName in _sorted_keys(_scored_incident_points):
scored_points.append({
&"score_key": String(score_key),
&"points": int(_scored_incident_points[score_key]),
})
var resolutions: Array[Dictionary] = []
for resolution_id: StringName in _sorted_keys(_applied_resolutions):
resolutions.append({
&"resolution_id": String(resolution_id),
&"payload": (_applied_resolutions[resolution_id] as Dictionary).duplicate(true),
})
var evaluated_days: Array[int] = []
for day_value: Variant in _evaluated_inspection_days.keys():
evaluated_days.append(int(day_value))
evaluated_days.sort()
var evaluations: Array[Dictionary] = []
for day_index: int in evaluated_days:
evaluations.append({
&"day_index": day_index,
&"result": (_evaluated_inspection_days[day_index] as Dictionary).duplicate(true),
})
return {
&"version": SNAPSHOT_VERSION,
&"current_minute": current_minute,
&"authority_state": _state.create_snapshot(),
&"incidents": incidents,
&"evidence": evidence_snapshots,
&"inspections": inspections,
&"scored_incident_points": scored_points,
&"applied_resolutions": resolutions,
&"evaluated_inspection_days": evaluations,
&"next_inspection_sequence": _next_inspection_sequence,
&"next_inspector_sequence": _next_inspector_sequence,
}
## Parses every object and cross-reference before replacing any live field.
func restore_snapshot(snapshot: Dictionary) -> bool:
var parsed := _parse_snapshot(snapshot)
if not bool(parsed.get(&"ok", false)):
last_error = String(parsed.get(&"error", "Invalid Authority snapshot."))
return false
_state = parsed[&"authority_state"]
_incidents = parsed[&"incidents"]
_evidence = parsed[&"evidence"]
_inspections = parsed[&"inspections"]
_scored_incident_points = parsed[&"scored_incident_points"]
_applied_resolutions = parsed[&"applied_resolutions"]
_evaluated_inspection_days = parsed[&"evaluated_inspection_days"]
current_minute = int(parsed[&"current_minute"])
_next_inspection_sequence = int(parsed[&"next_inspection_sequence"])
_next_inspector_sequence = int(parsed[&"next_inspector_sequence"])
last_error = ""
return true
func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
if int(_value(snapshot, &"version", 0)) != SNAPSHOT_VERSION:
return {&"ok": false, &"error": "Unsupported Authority snapshot version."}
for collection_key: StringName in [
&"incidents",
&"evidence",
&"inspections",
&"scored_incident_points",
&"applied_resolutions",
&"evaluated_inspection_days",
]:
if not _value(snapshot, collection_key, null) is Array:
return {&"ok": false, &"error": "Malformed Authority collection: %s" % collection_key}
var raw_state: Variant = _value(snapshot, &"authority_state", null)
if not raw_state is Dictionary:
return {&"ok": false, &"error": "Malformed AuthorityState snapshot."}
var next_state := HeatStateType.from_snapshot(raw_state)
if next_state == null:
return {&"ok": false, &"error": "Invalid AuthorityState snapshot."}
var next_incidents: Dictionary = {}
for raw_incident: Variant in _value(snapshot, &"incidents", []):
if not raw_incident is Dictionary:
return {&"ok": false, &"error": "Malformed Incident snapshot."}
var incident := IncidentType.from_snapshot(raw_incident)
if incident == null or next_incidents.has(incident.incident_id):
return {&"ok": false, &"error": "Invalid or duplicate Incident snapshot."}
next_incidents[incident.incident_id] = incident
var next_evidence: Dictionary = {}
for raw_evidence: Variant in _value(snapshot, &"evidence", []):
if not raw_evidence is Dictionary:
return {&"ok": false, &"error": "Malformed Evidence snapshot."}
var evidence := EvidenceType.from_snapshot(raw_evidence)
if evidence == null or next_evidence.has(evidence.evidence_id):
return {&"ok": false, &"error": "Invalid or duplicate Evidence snapshot."}
next_evidence[evidence.evidence_id] = evidence
var next_inspections: Dictionary = {}
for raw_inspection: Variant in _value(snapshot, &"inspections", []):
if not raw_inspection is Dictionary:
return {&"ok": false, &"error": "Malformed Inspection snapshot."}
var inspection := InspectionType.from_snapshot(raw_inspection)
if inspection == null or next_inspections.has(inspection.inspection_id):
return {&"ok": false, &"error": "Invalid or duplicate Inspection snapshot."}
next_inspections[inspection.inspection_id] = inspection
var next_scored_points: Dictionary = {}
for raw_score: Variant in _value(snapshot, &"scored_incident_points", []):
if not raw_score is Dictionary:
return {&"ok": false, &"error": "Malformed Evidence score snapshot."}
var score_key := StringName(_value(raw_score, &"score_key", &""))
var points := int(_value(raw_score, &"points", -1))
if score_key.is_empty() or points < 0 or next_scored_points.has(score_key):
return {&"ok": false, &"error": "Invalid or duplicate Evidence score snapshot."}
next_scored_points[score_key] = points
var next_resolutions: Dictionary = {}
for raw_resolution: Variant in _value(snapshot, &"applied_resolutions", []):
if not raw_resolution is Dictionary or not _value(raw_resolution, &"payload", null) is Dictionary:
return {&"ok": false, &"error": "Malformed resolution snapshot."}
var resolution_id := StringName(_value(raw_resolution, &"resolution_id", &""))
if resolution_id.is_empty() or next_resolutions.has(resolution_id):
return {&"ok": false, &"error": "Invalid or duplicate resolution snapshot."}
next_resolutions[resolution_id] = (_value(raw_resolution, &"payload", {}) as Dictionary).duplicate(true)
var next_evaluations: Dictionary = {}
for raw_evaluation: Variant in _value(snapshot, &"evaluated_inspection_days", []):
if not raw_evaluation is Dictionary or not _value(raw_evaluation, &"result", null) is Dictionary:
return {&"ok": false, &"error": "Malformed inspection evaluation snapshot."}
var day_index := int(_value(raw_evaluation, &"day_index", -1))
if day_index < 0 or next_evaluations.has(day_index):
return {&"ok": false, &"error": "Invalid or duplicate inspection evaluation day."}
next_evaluations[day_index] = (_value(raw_evaluation, &"result", {}) as Dictionary).duplicate(true)
var next_current_minute := int(_value(snapshot, &"current_minute", -1))
var next_inspection_sequence := int(_value(snapshot, &"next_inspection_sequence", 0))
var next_inspector_sequence := int(_value(snapshot, &"next_inspector_sequence", 0))
if next_current_minute < 0 or next_inspection_sequence < 1 or next_inspector_sequence < 1:
return {&"ok": false, &"error": "Authority snapshot counters are invalid."}
var invariant_report := _validate_candidate_invariants(
next_state,
next_incidents,
next_evidence,
next_inspections,
next_scored_points,
next_resolutions,
next_current_minute
)
if not bool(invariant_report[&"ok"]):
return {
&"ok": false,
&"error": "Authority snapshot violates invariants: %s" % [str(invariant_report[&"errors"])],
}
return {
&"ok": true,
&"authority_state": next_state,
&"incidents": next_incidents,
&"evidence": next_evidence,
&"inspections": next_inspections,
&"scored_incident_points": next_scored_points,
&"applied_resolutions": next_resolutions,
&"evaluated_inspection_days": next_evaluations,
&"current_minute": next_current_minute,
&"next_inspection_sequence": next_inspection_sequence,
&"next_inspector_sequence": next_inspector_sequence,
}
func _validate_candidate_invariants(
state: HeatState,
incidents: Dictionary,
evidence_by_id: Dictionary,
inspections: Dictionary,
scored_points: Dictionary,
resolutions: Dictionary,
reference_minute: int
) -> Dictionary:
var errors: Array[String] = []
if state == null or not state.is_valid():
errors.append("AuthorityState is invalid.")
return {&"ok": false, &"errors": errors}
if state.last_decay_day > reference_minute / 1_440:
errors.append("AuthorityState daily decay is ahead of current time.")
for incident_id: StringName in _sorted_keys(incidents):
var incident: AuthorityIncident = incidents[incident_id]
if incident == null or incident.incident_id != incident_id or not incident.is_valid():
errors.append("Invalid Incident: %s" % incident_id)
continue
if incident.detector_ids.is_empty():
errors.append("Incident has no reporting detector: %s" % incident_id)
var should_be_active := incident.state == IncidentType.State.OPEN
if state.active_case_ids.has(incident_id) != should_be_active:
errors.append("Incident active-case mismatch: %s" % incident_id)
for evidence_id: StringName in incident.evidence_ids:
if not evidence_by_id.has(evidence_id):
errors.append("Incident references missing Evidence: %s" % evidence_id)
if incident.state == IncidentType.State.RESOLVED and not resolutions.has(incident.resolution_id):
errors.append("Resolved Incident references missing resolution: %s" % incident_id)
for case_id: StringName in state.active_case_ids:
if not incidents.has(case_id):
errors.append("AuthorityState references missing active case: %s" % case_id)
for evidence_id: StringName in _sorted_keys(evidence_by_id):
var evidence: AuthorityEvidence = evidence_by_id[evidence_id]
if evidence == null or evidence.evidence_id != evidence_id or not evidence.is_valid():
errors.append("Invalid Evidence: %s" % evidence_id)
continue
if not evidence.incident_id.is_empty():
if not incidents.has(evidence.incident_id):
errors.append("Evidence references missing Incident: %s" % evidence_id)
elif not (incidents[evidence.incident_id] as AuthorityIncident).evidence_ids.has(evidence_id):
errors.append("Incident/Evidence reverse reference mismatch: %s" % evidence_id)
var expected_scores: Dictionary = {}
for evidence_id: StringName in _sorted_keys(evidence_by_id):
var scored_evidence: AuthorityEvidence = evidence_by_id[evidence_id]
if scored_evidence == null or not scored_evidence.is_discovered() \
or scored_evidence.is_expired(reference_minute):
continue
var expected_key := scored_evidence.incident_id
if expected_key.is_empty():
expected_key = StringName("evidence:%s" % evidence_id)
expected_scores[expected_key] = maxi(
int(expected_scores.get(expected_key, 0)),
scored_evidence.severity
)
for score_key: StringName in _sorted_keys(scored_points):
if int(scored_points[score_key]) < 0 or int(scored_points[score_key]) > 100:
errors.append("Invalid Evidence score: %s" % score_key)
if scored_points != expected_scores:
errors.append("Evidence score index does not match discovered, unexpired evidence.")
var score_sum := 0
for points: Variant in scored_points.values():
score_sum += int(points)
if score_sum != state.evidence_points:
errors.append("Authority evidence_points cache mismatch.")
for inspection_id: StringName in _sorted_keys(inspections):
var inspection: AuthorityInspection = inspections[inspection_id]
if inspection == null or inspection.inspection_id != inspection_id or not inspection.is_valid():
errors.append("Invalid Inspection: %s" % inspection_id)
continue
for evidence_id: StringName in inspection.discovered_evidence_ids:
if not evidence_by_id.has(evidence_id) or not (evidence_by_id[evidence_id] as AuthorityEvidence).is_discovered():
errors.append("Inspection references unavailable Evidence: %s" % evidence_id)
if not inspection.resolution_id.is_empty() and not resolutions.has(inspection.resolution_id):
errors.append("Inspection resolution reference is missing: %s" % inspection.resolution_id)
var expected_fines_due := 0
for resolution_id: StringName in _sorted_keys(resolutions):
var payload: Dictionary = resolutions[resolution_id]
if StringName(_value(payload, &"resolution_id", &"")) != resolution_id:
errors.append("Resolution payload ID mismatch: %s" % resolution_id)
var inspection_id := StringName(_value(payload, &"inspection_id", &""))
var fine_amount := int(_value(payload, &"fine_amount", -1))
if fine_amount < 0:
errors.append("Resolution has an invalid fine amount: %s" % resolution_id)
else:
expected_fines_due += fine_amount
if not inspections.has(inspection_id):
errors.append("Resolution references missing Inspection: %s" % resolution_id)
else:
var resolved_inspection: AuthorityInspection = inspections[inspection_id]
if resolved_inspection.resolution_id != resolution_id:
errors.append("Resolution/Inspection reverse reference mismatch: %s" % resolution_id)
if String(_value(payload, &"result", "")) != InspectionType.Result.keys()[resolved_inspection.result]:
errors.append("Resolution result cache mismatch: %s" % resolution_id)
if int(_value(payload, &"evidence_score", -1)) != resolved_inspection.evidence_score:
errors.append("Resolution Evidence score cache mismatch: %s" % resolution_id)
if expected_fines_due != state.fines_due:
errors.append("Authority fines_due cache mismatch.")
return {&"ok": errors.is_empty(), &"errors": errors}
func _recompute_authority_evidence_points(absolute_minute: int) -> void:
var next_scores: Dictionary = {}
for evidence_id: StringName in _sorted_keys(_evidence):
var evidence := get_evidence(evidence_id)
if evidence == null or not evidence.is_discovered() or evidence.is_expired(absolute_minute):
continue
var score_key := evidence.incident_id
if score_key.is_empty():
score_key = StringName("evidence:%s" % evidence.evidence_id)
next_scores[score_key] = maxi(int(next_scores.get(score_key, 0)), evidence.severity)
_scored_incident_points = next_scores
var total := 0
for points: Variant in _scored_incident_points.values():
total += int(points)
_state.set_evidence_points(total)
func _calculate_inspection_evidence_score(
inspection: AuthorityInspection,
absolute_minute: int
) -> int:
var incident_scores: Dictionary = {}
for evidence_id: StringName in inspection.discovered_evidence_ids:
var evidence := get_evidence(evidence_id)
if evidence == null or not evidence.is_discovered() or evidence.is_expired(absolute_minute):
continue
var score_key := evidence.incident_id
if score_key.is_empty():
score_key = StringName("evidence:%s" % evidence.evidence_id)
incident_scores[score_key] = maxi(int(incident_scores.get(score_key, 0)), evidence.severity)
var total := 0
for points: Variant in incident_scores.values():
total += int(points)
return total
func _change_inspection_state(
inspection_id: StringName,
new_state: AuthorityInspection.State,
absolute_minute: int,
reason: StringName
) -> CommandResult:
var inspection := get_inspection(inspection_id)
if inspection == null:
return _not_found("AuthorityInspection", inspection_id)
if absolute_minute < inspection.state_changed_at:
return _invalid("Inspection state time cannot move backwards.")
if not _transition_is_allowed(inspection.state, new_state):
return _invalid("Illegal Inspection state transition.", {
&"old_state": InspectionType.State.keys()[inspection.state],
&"new_state": InspectionType.State.keys()[new_state],
})
var old_state := inspection.state
if not inspection.set_state(new_state, absolute_minute, reason):
return _invalid("Inspection state transition did not change state.")
inspection_state_changed.emit(inspection_id, old_state, new_state, reason)
last_error = ""
return _success({
&"inspection_id": inspection_id,
&"old_state": StringName(InspectionType.State.keys()[old_state]),
&"new_state": StringName(InspectionType.State.keys()[new_state]),
&"reason": reason,
})
static func _transition_is_allowed(
old_state: AuthorityInspection.State,
new_state: AuthorityInspection.State
) -> bool:
match old_state:
InspectionType.State.SCHEDULED:
return new_state in [InspectionType.State.ARRIVING, InspectionType.State.CANCELLED]
InspectionType.State.ARRIVING:
return new_state in [
InspectionType.State.CHECK_IN,
InspectionType.State.BLOCKED_ENTRY,
InspectionType.State.INTERRUPTED,
InspectionType.State.HOSTILE,
InspectionType.State.CANCELLED,
]
InspectionType.State.CHECK_IN:
return new_state in [
InspectionType.State.PATROLLING,
InspectionType.State.DEPARTING,
InspectionType.State.INTERRUPTED,
InspectionType.State.HOSTILE,
InspectionType.State.CANCELLED,
]
InspectionType.State.PATROLLING:
return new_state in [
InspectionType.State.REVIEWING,
InspectionType.State.DEPARTING,
InspectionType.State.INTERRUPTED,
InspectionType.State.HOSTILE,
InspectionType.State.CANCELLED,
]
InspectionType.State.REVIEWING:
return new_state in [
InspectionType.State.RESOLVING,
InspectionType.State.DEPARTING,
InspectionType.State.CANCELLED,
]
InspectionType.State.RESOLVING:
return new_state in [InspectionType.State.REVIEWING, InspectionType.State.DEPARTING]
InspectionType.State.BLOCKED_ENTRY, InspectionType.State.INTERRUPTED, InspectionType.State.HOSTILE:
return new_state == InspectionType.State.DEPARTING
InspectionType.State.DEPARTING:
return new_state == InspectionType.State.CLOSED
return false
static func _determine_resolution(score: int, options: Dictionary) -> AuthorityInspection.Result:
if _has_key(options, &"result"):
var requested := String(_value(options, &"result", "PENDING"))
var index := InspectionType.Result.keys().find(requested)
if index > InspectionType.Result.PENDING:
return index as AuthorityInspection.Result
var warning_threshold := maxi(int(_value(options, &"warning_threshold", 1)), 1)
var fine_threshold := maxi(int(_value(options, &"fine_threshold", 20)), warning_threshold)
var closure_threshold := maxi(int(_value(options, &"closure_threshold", 50)), fine_threshold)
var confiscation_threshold := maxi(int(_value(options, &"confiscation_threshold", 80)), closure_threshold)
var arrest_threshold := maxi(int(_value(options, &"arrest_threshold", 110)), confiscation_threshold)
var raid_threshold := maxi(int(_value(options, &"raid_threshold", 140)), arrest_threshold)
if score >= raid_threshold:
return InspectionType.Result.RAID_ESCALATION
if score >= arrest_threshold:
return InspectionType.Result.ARREST_ATTEMPT
if score >= confiscation_threshold:
return InspectionType.Result.CONFISCATION
if score >= closure_threshold:
return InspectionType.Result.TEMPORARY_CLOSURE
if score >= fine_threshold:
return InspectionType.Result.FINE
if score >= warning_threshold:
return InspectionType.Result.WARNING
return InspectionType.Result.NO_ACTION
static func _sanction_for_result(result: AuthorityInspection.Result) -> StringName:
match result:
InspectionType.Result.TEMPORARY_CLOSURE:
return &"TEMPORARY_CLOSURE"
InspectionType.Result.CONFISCATION:
return &"CONFISCATION"
InspectionType.Result.ARREST_ATTEMPT:
return &"ARREST_ATTEMPT"
InspectionType.Result.RAID_ESCALATION:
return &"RAID_ESCALATION"
return &"NONE"
static func _normalize_path_result(value: Variant, fallback_minutes: int) -> Dictionary:
if value is bool:
return {
&"reachable": value,
&"reason": &"OK" if value else &"PATH_UNREACHABLE",
&"travel_minutes": fallback_minutes,
}
if value is Dictionary:
return {
&"reachable": bool(_value(value, &"reachable", false)),
&"reason": StringName(_value(value, &"reason", &"OK")),
&"travel_minutes": maxi(int(_value(value, &"travel_minutes", fallback_minutes)), 0),
}
return {
&"reachable": false,
&"reason": &"PATH_QUERY_INVALID",
&"travel_minutes": fallback_minutes,
}
static func _normalize_callback_result(value: Variant) -> Dictionary:
if value is bool:
return {&"ok": value}
if value is Dictionary:
var result: Dictionary = value.duplicate(true)
result[&"ok"] = bool(_value(value, &"ok", false))
return result
return {&"ok": false, &"reason": &"INVALID_CALLBACK_RESULT"}
func _has_open_inspection() -> bool:
for inspection: AuthorityInspection in get_inspections():
if not inspection.is_terminal():
return true
return false
static func _waypoint_less(a: Dictionary, b: Dictionary) -> bool:
var utility_a := int(a.get(&"utility", 0))
var utility_b := int(b.get(&"utility", 0))
if utility_a != utility_b:
return utility_a > utility_b
return String(a.get(&"waypoint_id", "")) < String(b.get(&"waypoint_id", ""))
static func _waypoint_ids(waypoints: Array[Dictionary]) -> Array[StringName]:
var result: Array[StringName] = []
for waypoint: Dictionary in waypoints:
result.append(StringName(waypoint[&"waypoint_id"]))
return result
static func _stable_percent(text: String) -> int:
var value := 17
for index: int in range(text.length()):
value = (value * 131 + text.unicode_at(index)) % 10_000_019
return value % 100
static func _sorted_keys(source: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for raw_key: Variant in source.keys():
result.append(StringName(raw_key))
result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return result
static func _sorted_unique_names(values: Variant) -> Array[StringName]:
var result: Array[StringName] = []
if values is Array:
for raw_value: Variant in values:
var value := StringName(raw_value)
if not value.is_empty() and not result.has(value):
result.append(value)
result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return result
static func _names_to_strings(values: Array[StringName]) -> Array[String]:
var result: Array[String] = []
for value: StringName in values:
result.append(String(value))
return result
static func _has_key(source: Dictionary, key: StringName) -> bool:
return source.has(key) or source.has(String(key))
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
if source.has(key):
return source[key]
return source.get(String(key), fallback)
func _success(details: Dictionary = {}) -> CommandResult:
return CommandResultType.succeeded(0, details)
func _invalid(message: String, details: Dictionary = {}) -> CommandResult:
last_error = message
return CommandResultType.failed(
CommandResultType.Code.VALIDATION_FAILED,
0,
message,
details
)
func _not_found(kind: String, entity_id: StringName) -> CommandResult:
return _invalid("%s not found: %s" % [kind, entity_id], {&"entity_id": entity_id})
func _duplicate(kind: String, entity_id: StringName) -> CommandResult:
return _invalid("Duplicate %s ID: %s" % [kind, entity_id], {&"entity_id": entity_id})