class_name AuthorityIncident extends RefCounted enum State { OPEN, RESOLVED, DISMISSED, } var incident_id: StringName = &"" var incident_type: StringName = &"" var created_at := 0 var location_cell := Vector3i.ZERO var facility_id: StringName = &"" var actor_ids: Array[StringName] = [] var severity := 0 var heat_delta := 0 var detector_ids: Array[StringName] = [] var evidence_ids: Array[StringName] = [] var state: State = State.OPEN var resolution_id: StringName = &"" var state_reason: StringName = &"" var revision := 0 func configure( p_incident_id: StringName, p_incident_type: StringName, p_created_at: int, options: Dictionary = {} ) -> AuthorityIncident: incident_id = p_incident_id incident_type = p_incident_type created_at = maxi(p_created_at, 0) location_cell = _vector3i(_value(options, &"location_cell", Vector3i.ZERO)) facility_id = StringName(_value(options, &"facility_id", &"")) actor_ids = _sorted_unique_names(_value(options, &"actor_ids", [])) severity = clampi(int(_value(options, &"severity", 0)), 0, 100) heat_delta = clampi(int(_value(options, &"heat_delta", severity / 5)), 0, 100) detector_ids = _sorted_unique_names(_value(options, &"detector_ids", [])) evidence_ids = _sorted_unique_names(_value(options, &"evidence_ids", [])) state = _state_from_value(_value(options, &"state", State.OPEN)) resolution_id = StringName(_value(options, &"resolution_id", &"")) state_reason = StringName(_value(options, &"state_reason", &"")) revision = maxi(int(_value(options, &"revision", 0)), 0) return self func add_detector(detector_id: StringName) -> bool: if detector_id.is_empty() or detector_ids.has(detector_id): return false detector_ids.append(detector_id) detector_ids.sort_custom(_name_less) revision += 1 return true func add_evidence(evidence_id: StringName) -> bool: if evidence_id.is_empty() or evidence_ids.has(evidence_id): return false evidence_ids.append(evidence_id) evidence_ids.sort_custom(_name_less) revision += 1 return true func resolve(p_resolution_id: StringName, reason: StringName = &"") -> bool: if state != State.OPEN or p_resolution_id.is_empty(): return false state = State.RESOLVED resolution_id = p_resolution_id state_reason = reason revision += 1 return true func dismiss(reason: StringName) -> bool: if state != State.OPEN: return false state = State.DISMISSED state_reason = reason revision += 1 return true func is_valid() -> bool: if incident_id.is_empty() or incident_type.is_empty() or created_at < 0: return false if severity < 0 or severity > 100 or heat_delta < 0 or heat_delta > 100 or revision < 0: return false if actor_ids != _sorted_unique_names(actor_ids): return false if detector_ids != _sorted_unique_names(detector_ids): return false if evidence_ids != _sorted_unique_names(evidence_ids): return false if state == State.RESOLVED and resolution_id.is_empty(): return false if state != State.RESOLVED and not resolution_id.is_empty(): return false return true func create_snapshot() -> Dictionary: return { &"incident_id": String(incident_id), &"incident_type": String(incident_type), &"created_at": created_at, &"location_cell": [location_cell.x, location_cell.y, location_cell.z], &"facility_id": String(facility_id), &"actor_ids": _names_to_strings(actor_ids), &"severity": severity, &"heat_delta": heat_delta, &"detector_ids": _names_to_strings(detector_ids), &"evidence_ids": _names_to_strings(evidence_ids), &"state": State.keys()[state], &"resolution_id": String(resolution_id), &"state_reason": String(state_reason), &"revision": revision, } static func from_snapshot(snapshot: Dictionary) -> AuthorityIncident: if not _snapshot_is_valid(snapshot): return null var incident := AuthorityIncident.new().configure( StringName(_value(snapshot, &"incident_id", &"")), StringName(_value(snapshot, &"incident_type", &"")), int(_value(snapshot, &"created_at", -1)), snapshot ) return incident if incident.is_valid() else null static func _snapshot_is_valid(snapshot: Dictionary) -> bool: for key: StringName in [ &"incident_id", &"incident_type", &"created_at", &"location_cell", &"facility_id", &"actor_ids", &"severity", &"heat_delta", &"detector_ids", &"evidence_ids", &"state", &"resolution_id", &"state_reason", &"revision", ]: if not _has_key(snapshot, key): return false if StringName(_value(snapshot, &"incident_id", &"")).is_empty() \ or StringName(_value(snapshot, &"incident_type", &"")).is_empty(): return false if not _value(snapshot, &"location_cell", null) is Array \ or (_value(snapshot, &"location_cell", []) as Array).size() != 3: return false if not _value(snapshot, &"actor_ids", null) is Array \ or not _value(snapshot, &"detector_ids", null) is Array \ or not _value(snapshot, &"evidence_ids", null) is Array: return false if not _name_array_is_canonical(_value(snapshot, &"actor_ids", [])) \ or not _name_array_is_canonical(_value(snapshot, &"detector_ids", [])) \ or not _name_array_is_canonical(_value(snapshot, &"evidence_ids", [])): return false var severity_value := int(_value(snapshot, &"severity", -1)) var heat_value := int(_value(snapshot, &"heat_delta", -1)) if int(_value(snapshot, &"created_at", -1)) < 0 or severity_value < 0 or severity_value > 100: return false if heat_value < 0 or heat_value > 100 or int(_value(snapshot, &"revision", -1)) < 0: return false return State.keys().has(String(_value(snapshot, &"state", ""))) static func _state_from_value(value: Variant) -> State: if value is String or value is StringName: var index := State.keys().find(String(value)) return (index as State) if index >= 0 else State.OPEN var number := int(value) return (number as State) if number >= 0 and number < State.size() else State.OPEN static func _vector3i(value: Variant) -> Vector3i: if value is Vector3i: return value if value is Array and value.size() >= 3: return Vector3i(int(value[0]), int(value[1]), int(value[2])) if value is Dictionary: return Vector3i(int(value.get(&"x", 0)), int(value.get(&"y", 0)), int(value.get(&"z", 0))) return Vector3i.ZERO 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(_name_less) 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 _name_less(a: StringName, b: StringName) -> bool: return String(a) < String(b) static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: if source.has(key): return source[key] return source.get(String(key), fallback) static func _has_key(source: Dictionary, key: StringName) -> bool: return source.has(key) or source.has(String(key)) static func _name_array_is_canonical(value: Variant) -> bool: if not value is Array: return false var previous := "" for index: int in range(value.size()): var current := String(value[index]) if current.is_empty() or (index > 0 and current <= previous): return false previous = current return true