Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
class_name FacilityInstance
|
||||
extends RefCounted
|
||||
|
||||
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
|
||||
|
||||
## Values are deliberately ordered by operational priority.
|
||||
enum State {
|
||||
BROKEN,
|
||||
NO_ACCESS,
|
||||
NO_POWER,
|
||||
NO_STAFF,
|
||||
CLOSED,
|
||||
DEGRADED,
|
||||
OPEN,
|
||||
}
|
||||
|
||||
const FULL_DAY_MINUTES := 1_440
|
||||
const DEFAULT_DURABILITY := 10_000
|
||||
const DEGRADED_DURABILITY_THRESHOLD := 3_000
|
||||
|
||||
var instance_id: StringName = &""
|
||||
var definition_id: StringName = &""
|
||||
var origin := Vector3i.ZERO
|
||||
var rotation := 0
|
||||
var occupied_cells: Array[Vector3i] = []
|
||||
var level := 1
|
||||
var state: State = State.OPEN
|
||||
var state_reasons: Array[StringName] = []
|
||||
var durability := DEFAULT_DURABILITY
|
||||
var local_inventory_id: StringName = &""
|
||||
var assigned_staff_by_shift: Dictionary = {}
|
||||
var active_staff_ids: Array[StringName] = []
|
||||
var queue_entries: Array[StringName] = []
|
||||
var active_services: Dictionary = {}
|
||||
var policy_current: Dictionary = {}
|
||||
var policy_pending: Dictionary = {}
|
||||
var policy_revision := 0
|
||||
var policy_pending_revision := 0
|
||||
var policy_pending_effective_at := -1
|
||||
var statistics_today: Dictionary = {}
|
||||
var lifetime_statistics: Dictionary = {}
|
||||
var created_at := 0
|
||||
|
||||
var capacity := 1
|
||||
var queue_capacity := 0
|
||||
var base_service_minutes := 1
|
||||
var base_quality := 0
|
||||
var base_price := 0
|
||||
var staff_min := 0
|
||||
var staff_max := 0
|
||||
var access_available := true
|
||||
var power_available := true
|
||||
var operator_open := true
|
||||
var opening_intervals: Array[Vector2i] = [Vector2i(0, FULL_DAY_MINUTES)]
|
||||
var degraded_reasons: Array[StringName] = []
|
||||
|
||||
|
||||
func configure(
|
||||
p_instance_id: StringName,
|
||||
p_definition_id: StringName,
|
||||
options: Dictionary = {}
|
||||
) -> FacilityInstance:
|
||||
instance_id = p_instance_id
|
||||
definition_id = p_definition_id
|
||||
origin = _vector3i(_value(options, &"origin", Vector3i.ZERO))
|
||||
rotation = _normalize_rotation(int(_value(options, &"rotation", 0)))
|
||||
occupied_cells = _cell_array(_value(options, &"occupied_cells", []))
|
||||
level = maxi(int(_value(options, &"level", 1)), 1)
|
||||
durability = clampi(int(_value(options, &"durability", DEFAULT_DURABILITY)), 0, DEFAULT_DURABILITY)
|
||||
local_inventory_id = StringName(_value(options, &"local_inventory_id", &""))
|
||||
capacity = maxi(int(_value(options, &"capacity", 1)), 1)
|
||||
queue_capacity = maxi(int(_value(options, &"queue_capacity", 0)), 0)
|
||||
base_service_minutes = maxi(int(_value(options, &"base_service_minutes", 1)), 1)
|
||||
base_quality = int(_value(options, &"base_quality", 0))
|
||||
base_price = maxi(int(_value(options, &"base_price", 0)), 0)
|
||||
staff_min = maxi(int(_value(options, &"staff_min", 0)), 0)
|
||||
staff_max = maxi(int(_value(options, &"staff_max", maxi(staff_min, 0))), staff_min)
|
||||
policy_current = _normalize_policy(_value(options, &"policy_current", {}))
|
||||
policy_revision = maxi(int(_value(options, &"policy_revision", 0)), 0)
|
||||
created_at = maxi(int(_value(options, &"created_at", 0)), 0)
|
||||
access_available = bool(_value(options, &"access_available", true))
|
||||
power_available = bool(_value(options, &"power_available", true))
|
||||
operator_open = bool(_value(options, &"operator_open", true))
|
||||
var intervals := _parse_intervals(_value(options, &"opening_intervals", []))
|
||||
if not intervals.is_empty():
|
||||
opening_intervals = intervals
|
||||
active_staff_ids = _sorted_unique_names(_value(options, &"active_staff_ids", []))
|
||||
degraded_reasons = _sorted_unique_names(_value(options, &"degraded_reasons", []))
|
||||
evaluate_state(created_at)
|
||||
return self
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not instance_id.is_empty() \
|
||||
and not definition_id.is_empty() \
|
||||
and capacity > 0 \
|
||||
and queue_capacity >= 0 \
|
||||
and base_service_minutes > 0 \
|
||||
and staff_min >= 0 \
|
||||
and staff_max >= staff_min \
|
||||
and _intervals_are_valid(opening_intervals)
|
||||
|
||||
|
||||
func set_opening_hours(open_minute_of_day: int, close_minute_of_day: int) -> bool:
|
||||
if open_minute_of_day < 0 or open_minute_of_day >= FULL_DAY_MINUTES:
|
||||
return false
|
||||
if close_minute_of_day < 0 or close_minute_of_day > FULL_DAY_MINUTES:
|
||||
return false
|
||||
if open_minute_of_day == close_minute_of_day:
|
||||
opening_intervals = [Vector2i(0, FULL_DAY_MINUTES)]
|
||||
elif open_minute_of_day < close_minute_of_day:
|
||||
opening_intervals = [Vector2i(open_minute_of_day, close_minute_of_day)]
|
||||
else:
|
||||
opening_intervals = []
|
||||
if close_minute_of_day > 0:
|
||||
opening_intervals.append(Vector2i(0, close_minute_of_day))
|
||||
if open_minute_of_day < FULL_DAY_MINUTES:
|
||||
opening_intervals.append(Vector2i(open_minute_of_day, FULL_DAY_MINUTES))
|
||||
return true
|
||||
|
||||
|
||||
func set_opening_intervals(intervals: Variant) -> bool:
|
||||
var parsed := _parse_intervals(intervals)
|
||||
if parsed.is_empty() or not _intervals_are_valid(parsed):
|
||||
return false
|
||||
opening_intervals = parsed
|
||||
return true
|
||||
|
||||
|
||||
func is_within_opening_hours(absolute_minute: int) -> bool:
|
||||
var minute_of_day := posmod(absolute_minute, FULL_DAY_MINUTES)
|
||||
for interval: Vector2i in opening_intervals:
|
||||
if minute_of_day >= interval.x and minute_of_day < interval.y:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func set_operational_conditions(conditions: Dictionary, absolute_minute: int) -> bool:
|
||||
if _has_value(conditions, &"durability"):
|
||||
durability = clampi(int(_value(conditions, &"durability", durability)), 0, DEFAULT_DURABILITY)
|
||||
if _has_value(conditions, &"access_available"):
|
||||
access_available = bool(_value(conditions, &"access_available", access_available))
|
||||
if _has_value(conditions, &"power_available"):
|
||||
power_available = bool(_value(conditions, &"power_available", power_available))
|
||||
if _has_value(conditions, &"operator_open"):
|
||||
operator_open = bool(_value(conditions, &"operator_open", operator_open))
|
||||
if _has_value(conditions, &"active_staff_ids"):
|
||||
active_staff_ids = _sorted_unique_names(_value(conditions, &"active_staff_ids", []))
|
||||
if _has_value(conditions, &"degraded_reasons"):
|
||||
degraded_reasons = _sorted_unique_names(_value(conditions, &"degraded_reasons", []))
|
||||
return evaluate_state(absolute_minute)
|
||||
|
||||
|
||||
func evaluate_state(absolute_minute: int) -> bool:
|
||||
var next_reasons: Array[StringName] = []
|
||||
var next_state := State.OPEN
|
||||
if durability <= 0:
|
||||
next_reasons.append(&"BROKEN")
|
||||
next_state = State.BROKEN
|
||||
if not access_available:
|
||||
next_reasons.append(&"NO_ACCESS")
|
||||
if next_state > State.NO_ACCESS:
|
||||
next_state = State.NO_ACCESS
|
||||
if not power_available:
|
||||
next_reasons.append(&"NO_POWER")
|
||||
if next_state > State.NO_POWER:
|
||||
next_state = State.NO_POWER
|
||||
if active_staff_ids.size() < staff_min:
|
||||
next_reasons.append(&"NO_STAFF")
|
||||
if next_state > State.NO_STAFF:
|
||||
next_state = State.NO_STAFF
|
||||
if not operator_open:
|
||||
next_reasons.append(&"CLOSED_BY_OPERATOR")
|
||||
if next_state > State.CLOSED:
|
||||
next_state = State.CLOSED
|
||||
if not is_within_opening_hours(absolute_minute):
|
||||
next_reasons.append(&"OUTSIDE_OPENING_HOURS")
|
||||
if next_state > State.CLOSED:
|
||||
next_state = State.CLOSED
|
||||
if durability > 0 and durability < DEGRADED_DURABILITY_THRESHOLD:
|
||||
next_reasons.append(&"LOW_DURABILITY")
|
||||
if next_state > State.DEGRADED:
|
||||
next_state = State.DEGRADED
|
||||
for reason: StringName in degraded_reasons:
|
||||
if not next_reasons.has(reason):
|
||||
next_reasons.append(reason)
|
||||
if next_state > State.DEGRADED:
|
||||
next_state = State.DEGRADED
|
||||
var changed := state != next_state
|
||||
state = next_state
|
||||
state_reasons = next_reasons
|
||||
return changed
|
||||
|
||||
|
||||
func can_start_service(absolute_minute: int) -> bool:
|
||||
evaluate_state(absolute_minute)
|
||||
return state in [State.OPEN, State.DEGRADED] and available_slots() > 0
|
||||
|
||||
|
||||
func available_slots() -> int:
|
||||
return maxi(capacity - active_services.size(), 0)
|
||||
|
||||
|
||||
func has_queue_space() -> bool:
|
||||
return queue_entries.size() < queue_capacity
|
||||
|
||||
|
||||
func apply_policy_patch(patch: Dictionary) -> bool:
|
||||
var changed := false
|
||||
var keys: Array = patch.keys()
|
||||
keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b))
|
||||
for raw_key: Variant in keys:
|
||||
var key := StringName(raw_key)
|
||||
var value: Variant = patch[raw_key]
|
||||
var has_existing := _has_value(policy_current, key)
|
||||
var existing: Variant = _value(policy_current, key, null)
|
||||
if not has_existing or existing != value:
|
||||
policy_current.erase(String(key))
|
||||
policy_current[key] = _copy(value)
|
||||
changed = true
|
||||
if changed:
|
||||
policy_revision += 1
|
||||
return changed
|
||||
|
||||
|
||||
func schedule_policy_patch(patch: Dictionary, effective_at: int) -> bool:
|
||||
if effective_at < 0 or patch.is_empty():
|
||||
return false
|
||||
policy_pending = _normalize_policy(patch)
|
||||
policy_pending_effective_at = effective_at
|
||||
policy_pending_revision = policy_revision + 1
|
||||
return true
|
||||
|
||||
|
||||
func apply_pending_policy(absolute_minute: int) -> bool:
|
||||
if policy_pending.is_empty() or policy_pending_effective_at < 0:
|
||||
return false
|
||||
if absolute_minute < policy_pending_effective_at:
|
||||
return false
|
||||
var changed := apply_policy_patch(policy_pending)
|
||||
policy_pending.clear()
|
||||
policy_pending_effective_at = -1
|
||||
policy_pending_revision = 0
|
||||
return changed
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
var saved_services: Array[Dictionary] = []
|
||||
var service_ids := _sorted_keys(active_services)
|
||||
for service_id: StringName in service_ids:
|
||||
var service: ServiceInstance = active_services[service_id]
|
||||
saved_services.append(service.create_snapshot())
|
||||
return {
|
||||
&"instance_id": String(instance_id),
|
||||
&"definition_id": String(definition_id),
|
||||
&"origin": [origin.x, origin.y, origin.z],
|
||||
&"rotation": rotation,
|
||||
&"occupied_cells": _serialize_cells(occupied_cells),
|
||||
&"level": level,
|
||||
&"state": State.keys()[state],
|
||||
&"state_reasons": _names_to_strings(state_reasons),
|
||||
&"durability": durability,
|
||||
&"local_inventory_id": String(local_inventory_id),
|
||||
&"assigned_staff_by_shift": assigned_staff_by_shift.duplicate(true),
|
||||
&"active_staff_ids": _names_to_strings(active_staff_ids),
|
||||
&"queue_entries": _names_to_strings(queue_entries),
|
||||
&"active_services": saved_services,
|
||||
&"policy_current": policy_current.duplicate(true),
|
||||
&"policy_pending": policy_pending.duplicate(true),
|
||||
&"policy_revision": policy_revision,
|
||||
&"policy_pending_revision": policy_pending_revision,
|
||||
&"policy_pending_effective_at": policy_pending_effective_at,
|
||||
&"statistics_today": statistics_today.duplicate(true),
|
||||
&"lifetime_statistics": lifetime_statistics.duplicate(true),
|
||||
&"created_at": created_at,
|
||||
&"capacity": capacity,
|
||||
&"queue_capacity": queue_capacity,
|
||||
&"base_service_minutes": base_service_minutes,
|
||||
&"base_quality": base_quality,
|
||||
&"base_price": base_price,
|
||||
&"staff_min": staff_min,
|
||||
&"staff_max": staff_max,
|
||||
&"access_available": access_available,
|
||||
&"power_available": power_available,
|
||||
&"operator_open": operator_open,
|
||||
&"opening_intervals": _serialize_intervals(opening_intervals),
|
||||
&"degraded_reasons": _names_to_strings(degraded_reasons),
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> FacilityInstance:
|
||||
if not _snapshot_shapes_are_valid(snapshot):
|
||||
return null
|
||||
var instance := FacilityInstance.new().configure(
|
||||
StringName(snapshot.get(&"instance_id", "")),
|
||||
StringName(snapshot.get(&"definition_id", "")),
|
||||
{
|
||||
&"origin": snapshot.get(&"origin", []),
|
||||
&"rotation": snapshot.get(&"rotation", 0),
|
||||
&"occupied_cells": snapshot.get(&"occupied_cells", []),
|
||||
&"level": snapshot.get(&"level", 1),
|
||||
&"durability": snapshot.get(&"durability", DEFAULT_DURABILITY),
|
||||
&"local_inventory_id": snapshot.get(&"local_inventory_id", ""),
|
||||
&"capacity": snapshot.get(&"capacity", 1),
|
||||
&"queue_capacity": snapshot.get(&"queue_capacity", 0),
|
||||
&"base_service_minutes": snapshot.get(&"base_service_minutes", 1),
|
||||
&"base_quality": snapshot.get(&"base_quality", 0),
|
||||
&"base_price": snapshot.get(&"base_price", 0),
|
||||
&"staff_min": snapshot.get(&"staff_min", 0),
|
||||
&"staff_max": snapshot.get(&"staff_max", 0),
|
||||
&"policy_current": snapshot.get(&"policy_current", {}),
|
||||
&"policy_revision": snapshot.get(&"policy_revision", 0),
|
||||
&"created_at": snapshot.get(&"created_at", 0),
|
||||
&"access_available": snapshot.get(&"access_available", true),
|
||||
&"power_available": snapshot.get(&"power_available", true),
|
||||
&"operator_open": snapshot.get(&"operator_open", true),
|
||||
&"opening_intervals": snapshot.get(&"opening_intervals", []),
|
||||
&"active_staff_ids": snapshot.get(&"active_staff_ids", []),
|
||||
&"degraded_reasons": snapshot.get(&"degraded_reasons", []),
|
||||
}
|
||||
)
|
||||
var state_index := State.keys().find(String(snapshot.get(&"state", "OPEN")))
|
||||
if state_index < 0:
|
||||
return null
|
||||
instance.state = state_index as State
|
||||
instance.state_reasons = _string_name_array(snapshot.get(&"state_reasons", []))
|
||||
instance.assigned_staff_by_shift = snapshot.get(&"assigned_staff_by_shift", {}).duplicate(true)
|
||||
instance.queue_entries = _string_name_array(snapshot.get(&"queue_entries", []))
|
||||
instance.policy_pending = _normalize_policy(snapshot.get(&"policy_pending", {}))
|
||||
instance.policy_pending_revision = maxi(int(snapshot.get(&"policy_pending_revision", 0)), 0)
|
||||
instance.policy_pending_effective_at = int(snapshot.get(&"policy_pending_effective_at", -1))
|
||||
instance.statistics_today = snapshot.get(&"statistics_today", {}).duplicate(true)
|
||||
instance.lifetime_statistics = snapshot.get(&"lifetime_statistics", {}).duplicate(true)
|
||||
var seen_services: Dictionary = {}
|
||||
for saved_service: Variant in snapshot.get(&"active_services", []):
|
||||
var service := ServiceInstanceType.from_snapshot(saved_service)
|
||||
if service == null or service.is_terminal() or service.facility_id != instance.instance_id:
|
||||
return null
|
||||
if seen_services.has(service.service_id):
|
||||
return null
|
||||
seen_services[service.service_id] = true
|
||||
instance.active_services[service.service_id] = service
|
||||
if not instance.is_valid() or instance.active_services.size() > instance.capacity:
|
||||
return null
|
||||
return instance
|
||||
|
||||
|
||||
static func _snapshot_shapes_are_valid(snapshot: Dictionary) -> bool:
|
||||
var array_fields := [
|
||||
&"origin", &"occupied_cells", &"state_reasons", &"active_staff_ids",
|
||||
&"queue_entries", &"active_services", &"opening_intervals", &"degraded_reasons",
|
||||
]
|
||||
for field: StringName in array_fields:
|
||||
if not snapshot.get(field, null) is Array:
|
||||
return false
|
||||
var dictionary_fields := [
|
||||
&"assigned_staff_by_shift", &"policy_current", &"policy_pending",
|
||||
&"statistics_today", &"lifetime_statistics",
|
||||
]
|
||||
for field: StringName in dictionary_fields:
|
||||
if not snapshot.get(field, null) is Dictionary:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _has_value(source: Dictionary, key: StringName) -> bool:
|
||||
return source.has(key) or source.has(String(key))
|
||||
|
||||
|
||||
static func _copy(value: Variant) -> Variant:
|
||||
return value.duplicate(true) if value is Dictionary or value is Array else value
|
||||
|
||||
|
||||
static func _dictionary_copy(source: Variant) -> Dictionary:
|
||||
return source.duplicate(true) if source is Dictionary else {}
|
||||
|
||||
|
||||
static func _normalize_policy(source: Variant) -> Dictionary:
|
||||
var normalized: Dictionary = {}
|
||||
if not source is Dictionary:
|
||||
return normalized
|
||||
for raw_key: Variant in source:
|
||||
normalized[StringName(raw_key)] = _copy(source[raw_key])
|
||||
return normalized
|
||||
|
||||
|
||||
static func _vector3i(source: Variant) -> Vector3i:
|
||||
if source is Vector3i:
|
||||
return source
|
||||
if source is Array and source.size() == 3:
|
||||
return Vector3i(int(source[0]), int(source[1]), int(source[2]))
|
||||
return Vector3i.ZERO
|
||||
|
||||
|
||||
static func _cell_array(source: Variant) -> Array[Vector3i]:
|
||||
var cells: Array[Vector3i] = []
|
||||
if not source is Array:
|
||||
return cells
|
||||
for raw_cell: Variant in source:
|
||||
if raw_cell is Vector3i:
|
||||
cells.append(raw_cell)
|
||||
elif raw_cell is Array and raw_cell.size() == 3:
|
||||
cells.append(_vector3i(raw_cell))
|
||||
return cells
|
||||
|
||||
|
||||
static func _parse_intervals(source: Variant) -> Array[Vector2i]:
|
||||
var intervals: Array[Vector2i] = []
|
||||
if not source is Array:
|
||||
return intervals
|
||||
for raw_interval: Variant in source:
|
||||
if raw_interval is Vector2i:
|
||||
intervals.append(raw_interval)
|
||||
elif raw_interval is Array and raw_interval.size() == 2:
|
||||
intervals.append(Vector2i(int(raw_interval[0]), int(raw_interval[1])))
|
||||
intervals.sort_custom(func(a: Vector2i, b: Vector2i) -> bool:
|
||||
return a.x < b.x or (a.x == b.x and a.y < b.y)
|
||||
)
|
||||
return intervals
|
||||
|
||||
|
||||
static func _intervals_are_valid(intervals: Array[Vector2i]) -> bool:
|
||||
if intervals.is_empty():
|
||||
return false
|
||||
var previous_end := -1
|
||||
for interval: Vector2i in intervals:
|
||||
if interval.x < 0 or interval.y > FULL_DAY_MINUTES or interval.x >= interval.y:
|
||||
return false
|
||||
if interval.x < previous_end:
|
||||
return false
|
||||
previous_end = interval.y
|
||||
return true
|
||||
|
||||
|
||||
static func _sorted_unique_names(source: Variant) -> Array[StringName]:
|
||||
var values := _string_name_array(source)
|
||||
var seen: Dictionary = {}
|
||||
var unique: Array[StringName] = []
|
||||
for value: StringName in values:
|
||||
if not value.is_empty() and not seen.has(value):
|
||||
seen[value] = true
|
||||
unique.append(value)
|
||||
unique.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return unique
|
||||
|
||||
|
||||
static func _string_name_array(source: Variant) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
if source is Array:
|
||||
for value: Variant in source:
|
||||
values.append(StringName(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _sorted_keys(source: Dictionary) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
for value: Variant in source.keys():
|
||||
values.append(StringName(value))
|
||||
values.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return values
|
||||
|
||||
|
||||
static func _serialize_cells(source: Array[Vector3i]) -> Array[Array]:
|
||||
var cells: Array[Array] = []
|
||||
for cell: Vector3i in source:
|
||||
cells.append([cell.x, cell.y, cell.z])
|
||||
return cells
|
||||
|
||||
|
||||
static func _serialize_intervals(source: Array[Vector2i]) -> Array[Array]:
|
||||
var intervals: Array[Array] = []
|
||||
for interval: Vector2i in source:
|
||||
intervals.append([interval.x, interval.y])
|
||||
return intervals
|
||||
|
||||
|
||||
static func _names_to_strings(source: Array[StringName]) -> Array[String]:
|
||||
var values: Array[String] = []
|
||||
for value: StringName in source:
|
||||
values.append(String(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _normalize_rotation(value: int) -> int:
|
||||
var normalized := posmod(value, 360)
|
||||
return (normalized / 90) * 90
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwac1ecwvokkl
|
||||
@@ -0,0 +1,945 @@
|
||||
class_name FacilityService
|
||||
extends RefCounted
|
||||
|
||||
signal instance_registered(instance: FacilityInstance)
|
||||
signal instance_unregistered(instance_id: StringName)
|
||||
signal facility_state_changed(instance_id: StringName, old_state: int, new_state: int, reasons: Array[StringName])
|
||||
signal request_enqueued(request: ServiceRequest)
|
||||
signal request_left_queue(request_id: StringName, reason: StringName)
|
||||
signal service_started(service: ServiceInstance)
|
||||
signal service_interrupted(service: ServiceInstance, reason: StringName)
|
||||
signal service_completed(service: ServiceInstance)
|
||||
signal service_failed(service: ServiceInstance, reason: StringName)
|
||||
|
||||
const FacilityInstanceType = preload("res://scripts/facility/facility_instance.gd")
|
||||
const ServiceRequestType = preload("res://scripts/facility/service_request.gd")
|
||||
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
|
||||
const CommandResultType = preload("res://scripts/core/command_result.gd")
|
||||
|
||||
const SNAPSHOT_VERSION := 1
|
||||
const STAFF_REPLACEMENT_GRACE_MINUTES := 5
|
||||
|
||||
const INTERRUPTION_PLAYER_CLOSED := &"PLAYER_CLOSED"
|
||||
const INTERRUPTION_NO_POWER := &"NO_POWER"
|
||||
const INTERRUPTION_MANA_STOP := &"MANA_STOP"
|
||||
const INTERRUPTION_STAFF_UNAVAILABLE := &"STAFF_UNAVAILABLE"
|
||||
const INTERRUPTION_EMERGENCY := &"EMERGENCY"
|
||||
const INTERRUPTION_FIRE := &"FIRE"
|
||||
const INTERRUPTION_RIOT := &"RIOT"
|
||||
const INTERRUPTION_GUEST_LEFT := &"GUEST_LEFT"
|
||||
const INTERRUPTION_BROKEN := &"BROKEN"
|
||||
|
||||
var current_minute := 0
|
||||
var last_error := ""
|
||||
|
||||
var _instances: Dictionary = {}
|
||||
var _requests: Dictionary = {}
|
||||
var _known_instance_ids: Dictionary = {}
|
||||
var _known_request_ids: Dictionary = {}
|
||||
var _known_service_ids: Dictionary = {}
|
||||
var _service_history: Dictionary = {}
|
||||
var _next_service_sequence := 1
|
||||
var _start_validator := Callable()
|
||||
var _completion_callback := Callable()
|
||||
|
||||
|
||||
func configure_callbacks(
|
||||
start_validator: Callable = Callable(),
|
||||
completion_callback: Callable = Callable()
|
||||
) -> FacilityService:
|
||||
_start_validator = start_validator
|
||||
_completion_callback = completion_callback
|
||||
return self
|
||||
|
||||
|
||||
func set_start_validator(callback: Callable) -> void:
|
||||
_start_validator = callback
|
||||
|
||||
|
||||
func set_completion_callback(callback: Callable) -> void:
|
||||
_completion_callback = callback
|
||||
|
||||
|
||||
func register_instance(instance: Variant) -> CommandResult:
|
||||
if instance == null or not instance is FacilityInstance:
|
||||
return _invalid("Facility instance is missing or has an invalid type.")
|
||||
var facility: FacilityInstance = instance
|
||||
if not facility.is_valid():
|
||||
return _invalid("Facility instance is invalid.", {&"instance_id": facility.instance_id})
|
||||
if _known_instance_ids.has(facility.instance_id):
|
||||
return _duplicate("Facility instance", facility.instance_id)
|
||||
if not facility.queue_entries.is_empty() or not facility.active_services.is_empty():
|
||||
return _invalid(
|
||||
"A new facility must be registered before it owns requests or services.",
|
||||
{&"instance_id": facility.instance_id}
|
||||
)
|
||||
_instances[facility.instance_id] = facility
|
||||
_known_instance_ids[facility.instance_id] = true
|
||||
facility.evaluate_state(current_minute)
|
||||
instance_registered.emit(facility)
|
||||
last_error = ""
|
||||
return _success({&"instance_id": facility.instance_id})
|
||||
|
||||
|
||||
func create_instance(configuration: Dictionary) -> CommandResult:
|
||||
var instance := FacilityInstanceType.new().configure(
|
||||
StringName(_value(configuration, &"instance_id", &"")),
|
||||
StringName(_value(configuration, &"definition_id", &"")),
|
||||
configuration
|
||||
)
|
||||
var result := register_instance(instance)
|
||||
if result.is_success():
|
||||
result.details[&"instance"] = instance
|
||||
return result
|
||||
|
||||
|
||||
func has_instance(instance_id: StringName) -> bool:
|
||||
return _instances.has(instance_id)
|
||||
|
||||
|
||||
func get_instance(instance_id: StringName) -> FacilityInstance:
|
||||
return _instances.get(instance_id) as FacilityInstance
|
||||
|
||||
|
||||
func get_facility(instance_id: StringName) -> FacilityInstance:
|
||||
return get_instance(instance_id)
|
||||
|
||||
|
||||
func get_instances() -> Array[FacilityInstance]:
|
||||
var result: Array[FacilityInstance] = []
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
result.append(_instances[instance_id])
|
||||
return result
|
||||
|
||||
|
||||
func unregister_instance(instance_id: StringName) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
if not instance.queue_entries.is_empty() or not instance.active_services.is_empty():
|
||||
return _invalid(
|
||||
"A facility with queued or active services cannot be unregistered.",
|
||||
{&"instance_id": instance_id}
|
||||
)
|
||||
_instances.erase(instance_id)
|
||||
instance_unregistered.emit(instance_id)
|
||||
last_error = ""
|
||||
return _success({&"instance_id": instance_id})
|
||||
|
||||
|
||||
func set_operational_status(
|
||||
instance_id: StringName,
|
||||
conditions: Dictionary,
|
||||
absolute_minute: int = -1
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
var old_state := instance.state
|
||||
instance.set_operational_conditions(conditions, minute)
|
||||
_emit_state_change(instance, old_state)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"instance_id": instance_id,
|
||||
&"state": FacilityInstanceType.State.keys()[instance.state],
|
||||
&"state_reasons": instance.state_reasons.duplicate(),
|
||||
})
|
||||
|
||||
|
||||
func set_opening_hours(
|
||||
instance_id: StringName,
|
||||
open_minute_of_day: int,
|
||||
close_minute_of_day: int,
|
||||
absolute_minute: int = -1
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
if not instance.set_opening_hours(open_minute_of_day, close_minute_of_day):
|
||||
return _invalid("Opening hours must be valid minute-of-day values.")
|
||||
var old_state := instance.state
|
||||
instance.evaluate_state(_resolve_minute(absolute_minute))
|
||||
_emit_state_change(instance, old_state)
|
||||
last_error = ""
|
||||
return _success({&"instance_id": instance_id})
|
||||
|
||||
|
||||
func set_policy(
|
||||
instance_id: StringName,
|
||||
patch: Dictionary,
|
||||
effective_at: int = -1
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
if patch.is_empty():
|
||||
return _invalid("Policy patch cannot be empty.")
|
||||
if effective_at >= 0 and effective_at > current_minute:
|
||||
instance.schedule_policy_patch(patch, effective_at)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"instance_id": instance_id,
|
||||
&"pending_revision": instance.policy_pending_revision,
|
||||
&"effective_at": effective_at,
|
||||
})
|
||||
var changed := instance.apply_policy_patch(patch)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"instance_id": instance_id,
|
||||
&"policy_revision": instance.policy_revision,
|
||||
&"changed": changed,
|
||||
})
|
||||
|
||||
|
||||
func create_request(
|
||||
request_id: StringName,
|
||||
facility_id: StringName,
|
||||
guest_ids: Array[StringName],
|
||||
service_type: StringName,
|
||||
created_at: int,
|
||||
options: Dictionary = {}
|
||||
) -> ServiceRequest:
|
||||
var instance := get_instance(facility_id)
|
||||
if instance == null:
|
||||
return null
|
||||
var quotation := options.duplicate(true)
|
||||
if not _has_value(quotation, &"quoted_price"):
|
||||
quotation[&"quoted_price"] = instance.base_price
|
||||
if not _has_value(quotation, &"expected_quality"):
|
||||
quotation[&"expected_quality"] = instance.base_quality
|
||||
if not _has_value(quotation, &"service_minutes"):
|
||||
quotation[&"service_minutes"] = instance.base_service_minutes
|
||||
quotation[&"policy_snapshot"] = instance.policy_current.duplicate(true)
|
||||
quotation[&"policy_revision"] = instance.policy_revision
|
||||
return ServiceRequestType.new().configure(
|
||||
request_id,
|
||||
facility_id,
|
||||
guest_ids,
|
||||
service_type,
|
||||
created_at,
|
||||
quotation
|
||||
)
|
||||
|
||||
|
||||
func enqueue_request(request: Variant, absolute_minute: int = -1) -> CommandResult:
|
||||
if request == null or not request is ServiceRequest:
|
||||
return _invalid("Service request is missing or has an invalid type.")
|
||||
var queued_request: ServiceRequest = request
|
||||
if not queued_request.is_valid():
|
||||
return _invalid("Service request is invalid.")
|
||||
if _known_request_ids.has(queued_request.request_id):
|
||||
return _duplicate("Service request", queued_request.request_id)
|
||||
var instance := get_instance(queued_request.facility_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", queued_request.facility_id)
|
||||
if not instance.has_queue_space():
|
||||
return _invalid(
|
||||
"Facility queue is full.",
|
||||
{&"instance_id": instance.instance_id, &"queue_capacity": instance.queue_capacity}
|
||||
)
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
queued_request.enqueued_at = minute
|
||||
_requests[queued_request.request_id] = queued_request
|
||||
_known_request_ids[queued_request.request_id] = true
|
||||
instance.queue_entries.append(queued_request.request_id)
|
||||
_sort_queue(instance)
|
||||
queued_request.expected_wait = _estimate_wait_before(instance, queued_request.request_id)
|
||||
_increment_stat(instance.statistics_today, &"requests_enqueued")
|
||||
_increment_stat(instance.lifetime_statistics, &"requests_enqueued")
|
||||
request_enqueued.emit(queued_request)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"request_id": queued_request.request_id,
|
||||
&"expected_wait": queued_request.expected_wait,
|
||||
&"queue_position": instance.queue_entries.find(queued_request.request_id),
|
||||
})
|
||||
|
||||
|
||||
func submit_request(request: Variant, absolute_minute: int = -1) -> CommandResult:
|
||||
return enqueue_request(request, absolute_minute)
|
||||
|
||||
|
||||
func get_request(request_id: StringName) -> ServiceRequest:
|
||||
return _requests.get(request_id) as ServiceRequest
|
||||
|
||||
|
||||
func get_queue(instance_id: StringName) -> Array[ServiceRequest]:
|
||||
var queued: Array[ServiceRequest] = []
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return queued
|
||||
for request_id: StringName in instance.queue_entries:
|
||||
var request := get_request(request_id)
|
||||
if request != null:
|
||||
queued.append(request)
|
||||
return queued
|
||||
|
||||
|
||||
func peek_next_request(instance_id: StringName) -> ServiceRequest:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null or instance.queue_entries.is_empty():
|
||||
return null
|
||||
return get_request(instance.queue_entries[0])
|
||||
|
||||
|
||||
func leave_queue(
|
||||
instance_id: StringName,
|
||||
request_id: StringName,
|
||||
reason: StringName = &"GUEST_LEFT"
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
var queue_index := instance.queue_entries.find(request_id)
|
||||
if queue_index < 0:
|
||||
return _not_found("Queued service request", request_id)
|
||||
instance.queue_entries.remove_at(queue_index)
|
||||
_requests.erase(request_id)
|
||||
_increment_stat(instance.statistics_today, &"queue_departures")
|
||||
_increment_stat(instance.lifetime_statistics, &"queue_departures")
|
||||
request_left_queue.emit(request_id, reason)
|
||||
last_error = ""
|
||||
return _success({&"request_id": request_id, &"reason": reason})
|
||||
|
||||
|
||||
func cancel_request(request_id: StringName, reason: StringName = &"GUEST_LEFT") -> CommandResult:
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
var instance: FacilityInstance = _instances[instance_id]
|
||||
if instance.queue_entries.has(request_id):
|
||||
return leave_queue(instance_id, request_id, reason)
|
||||
return _not_found("Queued service request", request_id)
|
||||
|
||||
|
||||
func start_next_service(
|
||||
instance_id: StringName,
|
||||
absolute_minute: int = -1,
|
||||
validator: Callable = Callable()
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return _not_found("Facility instance", instance_id)
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
instance.apply_pending_policy(minute)
|
||||
var old_state := instance.state
|
||||
instance.evaluate_state(minute)
|
||||
_emit_state_change(instance, old_state)
|
||||
if not instance.can_start_service(minute):
|
||||
return _invalid(
|
||||
"Facility cannot start a service in its current state.",
|
||||
{
|
||||
&"instance_id": instance_id,
|
||||
&"state": FacilityInstanceType.State.keys()[instance.state],
|
||||
&"reasons": instance.state_reasons.duplicate(),
|
||||
}
|
||||
)
|
||||
_cleanup_missing_queue_requests(instance)
|
||||
if instance.queue_entries.is_empty():
|
||||
return _not_found("Queued service request", &"")
|
||||
var request := get_request(instance.queue_entries[0])
|
||||
if request == null:
|
||||
return _invalid("The next queue entry is corrupt.")
|
||||
var effective_validator := validator if validator.is_valid() else _start_validator
|
||||
var callback_failure := _run_boundary_callback(
|
||||
effective_validator,
|
||||
[request, instance, minute],
|
||||
"Service start validation failed.",
|
||||
CommandResultType.Code.VALIDATION_FAILED
|
||||
)
|
||||
if callback_failure != null:
|
||||
callback_failure.details[&"request_id"] = request.request_id
|
||||
callback_failure.details[&"instance_id"] = instance_id
|
||||
return callback_failure
|
||||
|
||||
var service_id := _next_service_id()
|
||||
var service := ServiceInstanceType.new().configure(
|
||||
service_id,
|
||||
request.request_id,
|
||||
instance_id,
|
||||
minute,
|
||||
request.service_minutes if request.service_minutes > 0 else instance.base_service_minutes,
|
||||
request.guest_ids,
|
||||
instance.active_staff_ids,
|
||||
{
|
||||
&"quality_snapshot": request.expected_quality,
|
||||
&"price_snapshot": request.quoted_price,
|
||||
&"policy_snapshot": request.policy_snapshot,
|
||||
&"inventory_reservations": request.reserved_inventory_ids,
|
||||
}
|
||||
)
|
||||
instance.queue_entries.remove_at(0)
|
||||
instance.active_services[service_id] = service
|
||||
_known_service_ids[service_id] = true
|
||||
_increment_stat(instance.statistics_today, &"services_started")
|
||||
_increment_stat(instance.lifetime_statistics, &"services_started")
|
||||
service_started.emit(service)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"instance_id": instance_id,
|
||||
&"request_id": request.request_id,
|
||||
&"service_id": service_id,
|
||||
&"slot": instance.capacity - instance.available_slots() - 1,
|
||||
})
|
||||
|
||||
|
||||
func fill_available_slots(
|
||||
instance_id: StringName,
|
||||
absolute_minute: int = -1,
|
||||
validator: Callable = Callable()
|
||||
) -> Array[CommandResult]:
|
||||
var results: Array[CommandResult] = []
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
results.append(_not_found("Facility instance", instance_id))
|
||||
return results
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
while instance.available_slots() > 0 and not instance.queue_entries.is_empty():
|
||||
var result := start_next_service(instance_id, minute, validator)
|
||||
results.append(result)
|
||||
if not result.is_success():
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
func advance_one_minute(absolute_minute: int = -1) -> Array[CommandResult]:
|
||||
if absolute_minute < 0:
|
||||
current_minute += 1
|
||||
elif absolute_minute < current_minute:
|
||||
return [_invalid("FacilityService cannot advance backwards in time.")]
|
||||
else:
|
||||
current_minute = absolute_minute
|
||||
var results: Array[CommandResult] = []
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
var instance: FacilityInstance = _instances[instance_id]
|
||||
instance.apply_pending_policy(current_minute)
|
||||
var old_state := instance.state
|
||||
instance.evaluate_state(current_minute)
|
||||
_emit_state_change(instance, old_state)
|
||||
var active_ids := _sorted_keys(instance.active_services)
|
||||
for service_id: StringName in active_ids:
|
||||
if not instance.active_services.has(service_id):
|
||||
continue
|
||||
var service: ServiceInstance = instance.active_services[service_id]
|
||||
var preparation := _prepare_service_for_tick(instance, service)
|
||||
if preparation != null:
|
||||
results.append(preparation)
|
||||
if not instance.active_services.has(service_id):
|
||||
continue
|
||||
if service.state != ServiceInstanceType.State.ACTIVE:
|
||||
continue
|
||||
var ready_to_complete := service.elapsed_minutes >= service.duration_minutes
|
||||
if not ready_to_complete:
|
||||
ready_to_complete = service.advance_one_minute()
|
||||
if ready_to_complete:
|
||||
results.append(_complete_service(instance, service))
|
||||
var starts := fill_available_slots(instance_id, current_minute)
|
||||
results.append_array(starts)
|
||||
last_error = ""
|
||||
return results
|
||||
|
||||
|
||||
func interrupt_service(
|
||||
service_id: StringName,
|
||||
reason: StringName,
|
||||
absolute_minute: int = -1
|
||||
) -> CommandResult:
|
||||
var located := _find_active_service(service_id)
|
||||
if located.is_empty():
|
||||
return _not_found("Active service", service_id)
|
||||
var instance: FacilityInstance = located[&"instance"]
|
||||
var service: ServiceInstance = located[&"service"]
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
match reason:
|
||||
INTERRUPTION_PLAYER_CLOSED:
|
||||
return _success({&"service_id": service_id, &"continues": true})
|
||||
INTERRUPTION_NO_POWER, INTERRUPTION_MANA_STOP:
|
||||
service.pause(INTERRUPTION_NO_POWER, minute)
|
||||
INTERRUPTION_STAFF_UNAVAILABLE:
|
||||
service.wait_for_staff(reason, minute, STAFF_REPLACEMENT_GRACE_MINUTES)
|
||||
INTERRUPTION_BROKEN, INTERRUPTION_EMERGENCY, INTERRUPTION_FIRE, INTERRUPTION_RIOT, INTERRUPTION_GUEST_LEFT:
|
||||
_fail_service(instance, service, reason, minute)
|
||||
return _success({&"service_id": service_id, &"failed": true, &"reason": reason})
|
||||
_:
|
||||
return _invalid("Unknown service interruption reason.", {&"reason": reason})
|
||||
service_interrupted.emit(service, reason)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"service_id": service_id,
|
||||
&"state": ServiceInstanceType.State.keys()[service.state],
|
||||
&"reason": reason,
|
||||
})
|
||||
|
||||
|
||||
func resume_service(service_id: StringName) -> CommandResult:
|
||||
var located := _find_active_service(service_id)
|
||||
if located.is_empty():
|
||||
return _not_found("Active service", service_id)
|
||||
var service: ServiceInstance = located[&"service"]
|
||||
if not service.resume():
|
||||
return _invalid("Only an interrupted service can resume.")
|
||||
last_error = ""
|
||||
return _success({&"service_id": service_id})
|
||||
|
||||
|
||||
func replace_service_staff(service_id: StringName, staff_ids: Array[StringName]) -> CommandResult:
|
||||
var located := _find_active_service(service_id)
|
||||
if located.is_empty():
|
||||
return _not_found("Active service", service_id)
|
||||
if staff_ids.is_empty():
|
||||
return _invalid("Replacement staff list cannot be empty.")
|
||||
var service: ServiceInstance = located[&"service"]
|
||||
service.staff_ids.assign(staff_ids)
|
||||
if service.state == ServiceInstanceType.State.WAITING_FOR_STAFF:
|
||||
service.resume()
|
||||
last_error = ""
|
||||
return _success({&"service_id": service_id})
|
||||
|
||||
|
||||
func get_service(service_id: StringName) -> ServiceInstance:
|
||||
var located := _find_active_service(service_id)
|
||||
if not located.is_empty():
|
||||
return located[&"service"] as ServiceInstance
|
||||
return _service_history.get(service_id) as ServiceInstance
|
||||
|
||||
|
||||
func get_active_services(instance_id: StringName) -> Array[ServiceInstance]:
|
||||
var services: Array[ServiceInstance] = []
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return services
|
||||
for service_id: StringName in _sorted_keys(instance.active_services):
|
||||
services.append(instance.active_services[service_id])
|
||||
return services
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
var saved_instances: Array[Dictionary] = []
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
var instance: FacilityInstance = _instances[instance_id]
|
||||
saved_instances.append(instance.create_snapshot())
|
||||
var saved_requests: Array[Dictionary] = []
|
||||
for request_id: StringName in _sorted_keys(_requests):
|
||||
var request: ServiceRequest = _requests[request_id]
|
||||
saved_requests.append(request.create_snapshot())
|
||||
var saved_history: Array[Dictionary] = []
|
||||
for service_id: StringName in _sorted_keys(_service_history):
|
||||
var service: ServiceInstance = _service_history[service_id]
|
||||
saved_history.append(service.create_snapshot())
|
||||
return {
|
||||
&"schema_version": SNAPSHOT_VERSION,
|
||||
&"current_minute": current_minute,
|
||||
&"next_service_sequence": _next_service_sequence,
|
||||
&"known_instance_ids": _keys_to_strings(_known_instance_ids),
|
||||
&"known_request_ids": _keys_to_strings(_known_request_ids),
|
||||
&"known_service_ids": _keys_to_strings(_known_service_ids),
|
||||
&"instances": saved_instances,
|
||||
&"requests": saved_requests,
|
||||
&"service_history": saved_history,
|
||||
}
|
||||
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if int(_value(snapshot, &"schema_version", 0)) != SNAPSHOT_VERSION:
|
||||
last_error = "Unsupported FacilityService snapshot version."
|
||||
return false
|
||||
var array_fields := [
|
||||
&"known_instance_ids", &"known_request_ids", &"known_service_ids",
|
||||
&"instances", &"requests", &"service_history",
|
||||
]
|
||||
for field: StringName in array_fields:
|
||||
if not _value(snapshot, field, null) is Array:
|
||||
last_error = "FacilityService snapshot field '%s' must be an Array." % field
|
||||
return false
|
||||
|
||||
var restored_instances: Dictionary = {}
|
||||
for saved_instance: Variant in _value(snapshot, &"instances", []):
|
||||
if not saved_instance is Dictionary:
|
||||
last_error = "Facility snapshot entry must be a Dictionary."
|
||||
return false
|
||||
var instance := FacilityInstanceType.from_snapshot(saved_instance)
|
||||
if instance == null or restored_instances.has(instance.instance_id):
|
||||
last_error = "Facility snapshot contains an invalid or duplicate instance."
|
||||
return false
|
||||
restored_instances[instance.instance_id] = instance
|
||||
|
||||
var restored_requests: Dictionary = {}
|
||||
for saved_request: Variant in _value(snapshot, &"requests", []):
|
||||
if not saved_request is Dictionary:
|
||||
last_error = "Request snapshot entry must be a Dictionary."
|
||||
return false
|
||||
var request := ServiceRequestType.from_snapshot(saved_request)
|
||||
if request == null or restored_requests.has(request.request_id):
|
||||
last_error = "Facility snapshot contains an invalid or duplicate request."
|
||||
return false
|
||||
restored_requests[request.request_id] = request
|
||||
|
||||
var restored_history: Dictionary = {}
|
||||
for saved_service: Variant in _value(snapshot, &"service_history", []):
|
||||
if not saved_service is Dictionary:
|
||||
last_error = "Service history entry must be a Dictionary."
|
||||
return false
|
||||
var service := ServiceInstanceType.from_snapshot(saved_service)
|
||||
if service == null or not service.is_terminal() or restored_history.has(service.service_id):
|
||||
last_error = "Facility snapshot contains invalid service history."
|
||||
return false
|
||||
restored_history[service.service_id] = service
|
||||
|
||||
var restored_known_instances: Variant = _id_set(_value(snapshot, &"known_instance_ids", []))
|
||||
var restored_known_requests: Variant = _id_set(_value(snapshot, &"known_request_ids", []))
|
||||
var restored_known_services: Variant = _id_set(_value(snapshot, &"known_service_ids", []))
|
||||
if restored_known_instances == null or restored_known_requests == null or restored_known_services == null:
|
||||
last_error = "Known ID lists contain an empty or duplicate ID."
|
||||
return false
|
||||
if not _validate_restored_graph(
|
||||
restored_instances,
|
||||
restored_requests,
|
||||
restored_history,
|
||||
restored_known_instances,
|
||||
restored_known_requests,
|
||||
restored_known_services
|
||||
):
|
||||
return false
|
||||
|
||||
var restored_next_sequence := int(_value(snapshot, &"next_service_sequence", 0))
|
||||
var restored_minute := int(_value(snapshot, &"current_minute", -1))
|
||||
if restored_next_sequence < 1 or restored_minute < 0:
|
||||
last_error = "FacilityService counters are invalid."
|
||||
return false
|
||||
|
||||
# Commit only after every object and cross-reference has validated.
|
||||
_instances = restored_instances
|
||||
_requests = restored_requests
|
||||
_service_history = restored_history
|
||||
_known_instance_ids = restored_known_instances
|
||||
_known_request_ids = restored_known_requests
|
||||
_known_service_ids = restored_known_services
|
||||
_next_service_sequence = restored_next_sequence
|
||||
current_minute = restored_minute
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func _validate_restored_graph(
|
||||
instances: Dictionary,
|
||||
requests: Dictionary,
|
||||
history: Dictionary,
|
||||
known_instances: Dictionary,
|
||||
known_requests: Dictionary,
|
||||
known_services: Dictionary
|
||||
) -> bool:
|
||||
var referenced_requests: Dictionary = {}
|
||||
var active_service_ids: Dictionary = {}
|
||||
for instance_id: StringName in _sorted_keys(instances):
|
||||
var instance: FacilityInstance = instances[instance_id]
|
||||
if not known_instances.has(instance_id):
|
||||
last_error = "Active facility ID is absent from known IDs."
|
||||
return false
|
||||
for request_id: StringName in instance.queue_entries:
|
||||
if referenced_requests.has(request_id) or not requests.has(request_id):
|
||||
last_error = "Queue contains a duplicate or missing request."
|
||||
return false
|
||||
var request: ServiceRequest = requests[request_id]
|
||||
if request.facility_id != instance_id:
|
||||
last_error = "Queued request targets another facility."
|
||||
return false
|
||||
referenced_requests[request_id] = true
|
||||
for service_id: StringName in _sorted_keys(instance.active_services):
|
||||
var service: ServiceInstance = instance.active_services[service_id]
|
||||
if active_service_ids.has(service_id) or history.has(service_id):
|
||||
last_error = "Service ID occurs more than once."
|
||||
return false
|
||||
if not known_services.has(service_id) or not requests.has(service.request_id):
|
||||
last_error = "Active service has a missing known ID or request."
|
||||
return false
|
||||
if referenced_requests.has(service.request_id):
|
||||
last_error = "Request is referenced by multiple queue/service entries."
|
||||
return false
|
||||
var request: ServiceRequest = requests[service.request_id]
|
||||
if request.facility_id != instance_id:
|
||||
last_error = "Active service request targets another facility."
|
||||
return false
|
||||
referenced_requests[service.request_id] = true
|
||||
active_service_ids[service_id] = true
|
||||
for request_id: StringName in _sorted_keys(requests):
|
||||
if not known_requests.has(request_id) or not referenced_requests.has(request_id):
|
||||
last_error = "Live request is orphaned or absent from known IDs."
|
||||
return false
|
||||
for service_id: StringName in _sorted_keys(history):
|
||||
if not known_services.has(service_id):
|
||||
last_error = "Historical service is absent from known IDs."
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _prepare_service_for_tick(
|
||||
instance: FacilityInstance,
|
||||
service: ServiceInstance
|
||||
) -> CommandResult:
|
||||
if instance.state == FacilityInstanceType.State.BROKEN:
|
||||
_fail_service(instance, service, INTERRUPTION_BROKEN, current_minute)
|
||||
return _success({&"service_id": service.service_id, &"failed": true})
|
||||
if not instance.power_available:
|
||||
if service.state == ServiceInstanceType.State.ACTIVE:
|
||||
service.pause(INTERRUPTION_NO_POWER, current_minute)
|
||||
service_interrupted.emit(service, INTERRUPTION_NO_POWER)
|
||||
return null
|
||||
if service.state == ServiceInstanceType.State.PAUSED \
|
||||
and service.interruption_reason == INTERRUPTION_NO_POWER:
|
||||
service.resume()
|
||||
var has_required_staff := instance.active_staff_ids.size() >= instance.staff_min
|
||||
if not has_required_staff:
|
||||
if service.state in [ServiceInstanceType.State.ACTIVE, ServiceInstanceType.State.PAUSED]:
|
||||
service.wait_for_staff(
|
||||
INTERRUPTION_STAFF_UNAVAILABLE,
|
||||
current_minute,
|
||||
STAFF_REPLACEMENT_GRACE_MINUTES
|
||||
)
|
||||
service_interrupted.emit(service, INTERRUPTION_STAFF_UNAVAILABLE)
|
||||
elif instance.staff_min > 0 \
|
||||
and service.state == ServiceInstanceType.State.WAITING_FOR_STAFF \
|
||||
and service.interruption_reason == INTERRUPTION_STAFF_UNAVAILABLE:
|
||||
service.resume()
|
||||
if service.state == ServiceInstanceType.State.WAITING_FOR_STAFF:
|
||||
if service.staff_grace_deadline >= 0 and current_minute >= service.staff_grace_deadline:
|
||||
_fail_service(instance, service, INTERRUPTION_STAFF_UNAVAILABLE, current_minute)
|
||||
return _success({&"service_id": service.service_id, &"failed": true})
|
||||
return null
|
||||
|
||||
|
||||
func _complete_service(
|
||||
instance: FacilityInstance,
|
||||
service: ServiceInstance,
|
||||
callback: Callable = Callable()
|
||||
) -> CommandResult:
|
||||
var request := get_request(service.request_id)
|
||||
if request == null:
|
||||
_fail_service(instance, service, &"MISSING_REQUEST", current_minute)
|
||||
return _invalid(
|
||||
"Active service lost its request and was failed safely.",
|
||||
{&"service_id": service.service_id}
|
||||
)
|
||||
var effective_callback := callback if callback.is_valid() else _completion_callback
|
||||
var callback_failure := _run_boundary_callback(
|
||||
effective_callback,
|
||||
[service, request, instance, current_minute],
|
||||
"Service completion transaction failed.",
|
||||
CommandResultType.Code.TRANSACTION_APPLY_FAILED
|
||||
)
|
||||
if callback_failure != null:
|
||||
callback_failure.details[&"service_id"] = service.service_id
|
||||
callback_failure.details[&"request_id"] = request.request_id
|
||||
return callback_failure
|
||||
service.mark_completed(current_minute)
|
||||
instance.active_services.erase(service.service_id)
|
||||
_requests.erase(request.request_id)
|
||||
_service_history[service.service_id] = service
|
||||
_increment_stat(instance.statistics_today, &"services_completed")
|
||||
_increment_stat(instance.lifetime_statistics, &"services_completed")
|
||||
service_completed.emit(service)
|
||||
return _success({
|
||||
&"instance_id": instance.instance_id,
|
||||
&"request_id": request.request_id,
|
||||
&"service_id": service.service_id,
|
||||
&"completed": true,
|
||||
})
|
||||
|
||||
|
||||
func _fail_service(
|
||||
instance: FacilityInstance,
|
||||
service: ServiceInstance,
|
||||
reason: StringName,
|
||||
absolute_minute: int
|
||||
) -> void:
|
||||
service.mark_failed(reason, absolute_minute)
|
||||
instance.active_services.erase(service.service_id)
|
||||
_requests.erase(service.request_id)
|
||||
_service_history[service.service_id] = service
|
||||
_increment_stat(instance.statistics_today, &"services_failed")
|
||||
_increment_stat(instance.lifetime_statistics, &"services_failed")
|
||||
service_failed.emit(service, reason)
|
||||
|
||||
|
||||
func _run_boundary_callback(
|
||||
callback: Callable,
|
||||
arguments: Array,
|
||||
fallback_message: String,
|
||||
failure_code: CommandResult.Code
|
||||
) -> CommandResult:
|
||||
if not callback.is_valid():
|
||||
return null
|
||||
var argument_count := callback.get_argument_count()
|
||||
var effective_arguments := arguments
|
||||
if argument_count >= 0 and argument_count < arguments.size():
|
||||
effective_arguments = arguments.slice(0, argument_count)
|
||||
var returned: Variant = callback.callv(effective_arguments)
|
||||
if returned == null or (returned is bool and returned):
|
||||
return null
|
||||
if returned is CommandResult:
|
||||
var command_result: CommandResult = returned
|
||||
return null if command_result.is_success() else command_result.copy()
|
||||
if returned is Dictionary:
|
||||
if bool(returned.get(&"ok", returned.get("ok", false))):
|
||||
return null
|
||||
var dictionary_message := String(
|
||||
returned.get(&"message", returned.get("message", fallback_message))
|
||||
)
|
||||
return CommandResultType.failed(failure_code, 0, dictionary_message, returned)
|
||||
var message := String(returned) if returned is String and not returned.is_empty() else fallback_message
|
||||
return CommandResultType.failed(failure_code, 0, message)
|
||||
|
||||
|
||||
func _estimate_wait_before(instance: FacilityInstance, target_request_id: StringName) -> int:
|
||||
var outstanding_minutes := 0
|
||||
for service_id: StringName in _sorted_keys(instance.active_services):
|
||||
var service: ServiceInstance = instance.active_services[service_id]
|
||||
outstanding_minutes += service.remaining_minutes()
|
||||
for request_id: StringName in instance.queue_entries:
|
||||
if request_id == target_request_id:
|
||||
break
|
||||
var request := get_request(request_id)
|
||||
if request != null:
|
||||
outstanding_minutes += request.service_minutes
|
||||
return ceili(float(outstanding_minutes) / float(maxi(instance.capacity, 1)))
|
||||
|
||||
|
||||
func _cleanup_missing_queue_requests(instance: FacilityInstance) -> void:
|
||||
var valid_ids: Array[StringName] = []
|
||||
for request_id: StringName in instance.queue_entries:
|
||||
if _requests.has(request_id):
|
||||
valid_ids.append(request_id)
|
||||
else:
|
||||
push_warning(
|
||||
"Removed corrupt FacilityService queue entry '%s' from '%s'." \
|
||||
% [request_id, instance.instance_id]
|
||||
)
|
||||
instance.queue_entries = valid_ids
|
||||
_sort_queue(instance)
|
||||
|
||||
|
||||
func _sort_queue(instance: FacilityInstance) -> void:
|
||||
instance.queue_entries.sort_custom(Callable(self, "_queue_entry_less"))
|
||||
|
||||
|
||||
func _queue_entry_less(left_id: StringName, right_id: StringName) -> bool:
|
||||
var left := get_request(left_id)
|
||||
var right := get_request(right_id)
|
||||
if left == null:
|
||||
return false
|
||||
if right == null:
|
||||
return true
|
||||
if left.priority != right.priority:
|
||||
return left.priority > right.priority
|
||||
if left.enqueued_at != right.enqueued_at:
|
||||
return left.enqueued_at < right.enqueued_at
|
||||
return String(left.request_id) < String(right.request_id)
|
||||
|
||||
|
||||
func _find_active_service(service_id: StringName) -> Dictionary:
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
var instance: FacilityInstance = _instances[instance_id]
|
||||
if instance.active_services.has(service_id):
|
||||
return {
|
||||
&"instance": instance,
|
||||
&"service": instance.active_services[service_id],
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
func _next_service_id() -> StringName:
|
||||
while true:
|
||||
var candidate := StringName("service_%08d" % _next_service_sequence)
|
||||
_next_service_sequence += 1
|
||||
if not _known_service_ids.has(candidate):
|
||||
return candidate
|
||||
return &""
|
||||
|
||||
|
||||
func _resolve_minute(absolute_minute: int) -> int:
|
||||
if absolute_minute >= 0:
|
||||
current_minute = maxi(current_minute, absolute_minute)
|
||||
return current_minute
|
||||
|
||||
|
||||
func _emit_state_change(instance: FacilityInstance, old_state: int) -> void:
|
||||
if old_state != instance.state:
|
||||
facility_state_changed.emit(
|
||||
instance.instance_id,
|
||||
old_state,
|
||||
instance.state,
|
||||
instance.state_reasons.duplicate()
|
||||
)
|
||||
|
||||
|
||||
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(entity_name: String, entity_id: StringName) -> CommandResult:
|
||||
var message := "%s '%s' was not found." % [entity_name, entity_id]
|
||||
last_error = message
|
||||
return CommandResultType.failed(
|
||||
CommandResultType.Code.NOT_FOUND,
|
||||
0,
|
||||
message,
|
||||
{&"id": entity_id}
|
||||
)
|
||||
|
||||
|
||||
func _duplicate(entity_name: String, entity_id: StringName) -> CommandResult:
|
||||
var message := "%s '%s' already exists." % [entity_name, entity_id]
|
||||
last_error = message
|
||||
return CommandResultType.failed(
|
||||
CommandResultType.Code.DUPLICATE_COMMAND_ID,
|
||||
0,
|
||||
message,
|
||||
{&"id": entity_id}
|
||||
)
|
||||
|
||||
|
||||
static func _increment_stat(statistics: Dictionary, key: StringName) -> void:
|
||||
statistics[key] = int(statistics.get(key, 0)) + 1
|
||||
|
||||
|
||||
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _has_value(source: Dictionary, key: StringName) -> bool:
|
||||
return source.has(key) or source.has(String(key))
|
||||
|
||||
|
||||
static func _sorted_keys(source: Dictionary) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
for value: Variant in source.keys():
|
||||
values.append(StringName(value))
|
||||
values.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return values
|
||||
|
||||
|
||||
static func _keys_to_strings(source: Dictionary) -> Array[String]:
|
||||
var values: Array[String] = []
|
||||
for key: StringName in _sorted_keys(source):
|
||||
values.append(String(key))
|
||||
return values
|
||||
|
||||
|
||||
static func _id_set(source: Variant) -> Variant:
|
||||
if not source is Array:
|
||||
return null
|
||||
var ids: Dictionary = {}
|
||||
for raw_id: Variant in source:
|
||||
var id := StringName(raw_id)
|
||||
if id.is_empty() or ids.has(id):
|
||||
return null
|
||||
ids[id] = true
|
||||
return ids
|
||||
@@ -0,0 +1 @@
|
||||
uid://ln0q7wmvm8kx
|
||||
@@ -0,0 +1,232 @@
|
||||
class_name ServiceInstance
|
||||
extends RefCounted
|
||||
|
||||
enum State {
|
||||
ACTIVE,
|
||||
PAUSED,
|
||||
WAITING_FOR_STAFF,
|
||||
COMPLETED,
|
||||
FAILED,
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
const NO_DEADLINE := -1
|
||||
|
||||
var service_id: StringName = &""
|
||||
var request_id: StringName = &""
|
||||
var facility_id: StringName = &""
|
||||
var started_at := 0
|
||||
var finish_at := 1
|
||||
var guest_ids: Array[StringName] = []
|
||||
var staff_ids: Array[StringName] = []
|
||||
var quality_snapshot := 0
|
||||
var price_snapshot := 0
|
||||
var policy_snapshot: Dictionary = {}
|
||||
var inventory_reservations: Array[StringName] = []
|
||||
var interruption_policy: Dictionary = {}
|
||||
var duration_minutes := 1
|
||||
var elapsed_minutes := 0
|
||||
var state: State = State.ACTIVE
|
||||
var interruption_reason: StringName = &""
|
||||
var interrupted_at := -1
|
||||
var staff_grace_deadline := NO_DEADLINE
|
||||
var completed_at := -1
|
||||
var failure_reason: StringName = &""
|
||||
|
||||
|
||||
func configure(
|
||||
p_service_id: StringName,
|
||||
p_request_id: StringName,
|
||||
p_facility_id: StringName,
|
||||
p_started_at: int,
|
||||
p_duration_minutes: int,
|
||||
p_guest_ids: Array[StringName],
|
||||
p_staff_ids: Array[StringName],
|
||||
options: Dictionary = {}
|
||||
) -> ServiceInstance:
|
||||
service_id = p_service_id
|
||||
request_id = p_request_id
|
||||
facility_id = p_facility_id
|
||||
started_at = maxi(p_started_at, 0)
|
||||
duration_minutes = maxi(p_duration_minutes, 1)
|
||||
finish_at = started_at + duration_minutes
|
||||
guest_ids.assign(p_guest_ids)
|
||||
staff_ids.assign(p_staff_ids)
|
||||
quality_snapshot = int(_value(options, &"quality_snapshot", 0))
|
||||
price_snapshot = maxi(int(_value(options, &"price_snapshot", 0)), 0)
|
||||
policy_snapshot = _dictionary_copy(_value(options, &"policy_snapshot", {}))
|
||||
inventory_reservations = _string_name_array(
|
||||
_value(options, &"inventory_reservations", [])
|
||||
)
|
||||
interruption_policy = _dictionary_copy(_value(options, &"interruption_policy", {}))
|
||||
return self
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not service_id.is_empty() \
|
||||
and not request_id.is_empty() \
|
||||
and not facility_id.is_empty() \
|
||||
and not guest_ids.is_empty() \
|
||||
and duration_minutes > 0 \
|
||||
and elapsed_minutes >= 0 \
|
||||
and elapsed_minutes <= duration_minutes
|
||||
|
||||
|
||||
func is_terminal() -> bool:
|
||||
return state in [State.COMPLETED, State.FAILED, State.CANCELLED]
|
||||
|
||||
|
||||
func can_progress() -> bool:
|
||||
return state == State.ACTIVE and elapsed_minutes < duration_minutes
|
||||
|
||||
|
||||
func advance_one_minute() -> bool:
|
||||
if not can_progress():
|
||||
return false
|
||||
elapsed_minutes = mini(elapsed_minutes + 1, duration_minutes)
|
||||
return elapsed_minutes >= duration_minutes
|
||||
|
||||
|
||||
func pause(reason: StringName, absolute_minute: int) -> bool:
|
||||
if state != State.ACTIVE:
|
||||
return false
|
||||
state = State.PAUSED
|
||||
interruption_reason = reason
|
||||
interrupted_at = absolute_minute
|
||||
return true
|
||||
|
||||
|
||||
func wait_for_staff(reason: StringName, absolute_minute: int, grace_minutes := 5) -> bool:
|
||||
if state not in [State.ACTIVE, State.PAUSED]:
|
||||
return false
|
||||
state = State.WAITING_FOR_STAFF
|
||||
interruption_reason = reason
|
||||
interrupted_at = absolute_minute
|
||||
staff_grace_deadline = absolute_minute + maxi(grace_minutes, 0)
|
||||
return true
|
||||
|
||||
|
||||
func resume() -> bool:
|
||||
if state not in [State.PAUSED, State.WAITING_FOR_STAFF]:
|
||||
return false
|
||||
state = State.ACTIVE
|
||||
interruption_reason = &""
|
||||
interrupted_at = -1
|
||||
staff_grace_deadline = NO_DEADLINE
|
||||
return true
|
||||
|
||||
|
||||
func mark_completed(absolute_minute: int) -> void:
|
||||
elapsed_minutes = duration_minutes
|
||||
state = State.COMPLETED
|
||||
completed_at = absolute_minute
|
||||
interruption_reason = &""
|
||||
failure_reason = &""
|
||||
staff_grace_deadline = NO_DEADLINE
|
||||
|
||||
|
||||
func mark_failed(reason: StringName, absolute_minute: int) -> void:
|
||||
state = State.FAILED
|
||||
failure_reason = reason
|
||||
completed_at = absolute_minute
|
||||
staff_grace_deadline = NO_DEADLINE
|
||||
|
||||
|
||||
func mark_cancelled(reason: StringName, absolute_minute: int) -> void:
|
||||
state = State.CANCELLED
|
||||
failure_reason = reason
|
||||
completed_at = absolute_minute
|
||||
staff_grace_deadline = NO_DEADLINE
|
||||
|
||||
|
||||
func remaining_minutes() -> int:
|
||||
return maxi(duration_minutes - elapsed_minutes, 0)
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"service_id": String(service_id),
|
||||
&"request_id": String(request_id),
|
||||
&"facility_id": String(facility_id),
|
||||
&"started_at": started_at,
|
||||
&"finish_at": finish_at,
|
||||
&"guest_ids": _names_to_strings(guest_ids),
|
||||
&"staff_ids": _names_to_strings(staff_ids),
|
||||
&"quality_snapshot": quality_snapshot,
|
||||
&"price_snapshot": price_snapshot,
|
||||
&"policy_snapshot": policy_snapshot.duplicate(true),
|
||||
&"inventory_reservations": _names_to_strings(inventory_reservations),
|
||||
&"interruption_policy": interruption_policy.duplicate(true),
|
||||
&"duration_minutes": duration_minutes,
|
||||
&"elapsed_minutes": elapsed_minutes,
|
||||
&"state": State.keys()[state],
|
||||
&"interruption_reason": String(interruption_reason),
|
||||
&"interrupted_at": interrupted_at,
|
||||
&"staff_grace_deadline": staff_grace_deadline,
|
||||
&"completed_at": completed_at,
|
||||
&"failure_reason": String(failure_reason),
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> ServiceInstance:
|
||||
if not snapshot.get(&"guest_ids", null) is Array:
|
||||
return null
|
||||
if not snapshot.get(&"staff_ids", null) is Array:
|
||||
return null
|
||||
if not snapshot.get(&"policy_snapshot", null) is Dictionary:
|
||||
return null
|
||||
if not snapshot.get(&"inventory_reservations", null) is Array:
|
||||
return null
|
||||
if not snapshot.get(&"interruption_policy", null) is Dictionary:
|
||||
return null
|
||||
var service := ServiceInstance.new().configure(
|
||||
StringName(snapshot.get(&"service_id", "")),
|
||||
StringName(snapshot.get(&"request_id", "")),
|
||||
StringName(snapshot.get(&"facility_id", "")),
|
||||
int(snapshot.get(&"started_at", 0)),
|
||||
int(snapshot.get(&"duration_minutes", 1)),
|
||||
_string_name_array(snapshot.get(&"guest_ids", [])),
|
||||
_string_name_array(snapshot.get(&"staff_ids", [])),
|
||||
{
|
||||
&"quality_snapshot": snapshot.get(&"quality_snapshot", 0),
|
||||
&"price_snapshot": snapshot.get(&"price_snapshot", 0),
|
||||
&"policy_snapshot": snapshot.get(&"policy_snapshot", {}),
|
||||
&"inventory_reservations": snapshot.get(&"inventory_reservations", []),
|
||||
&"interruption_policy": snapshot.get(&"interruption_policy", {}),
|
||||
}
|
||||
)
|
||||
var state_index := State.keys().find(String(snapshot.get(&"state", "ACTIVE")))
|
||||
if state_index < 0:
|
||||
return null
|
||||
service.state = state_index as State
|
||||
service.finish_at = int(snapshot.get(&"finish_at", service.started_at + service.duration_minutes))
|
||||
service.elapsed_minutes = int(snapshot.get(&"elapsed_minutes", 0))
|
||||
service.interruption_reason = StringName(snapshot.get(&"interruption_reason", ""))
|
||||
service.interrupted_at = int(snapshot.get(&"interrupted_at", -1))
|
||||
service.staff_grace_deadline = int(snapshot.get(&"staff_grace_deadline", NO_DEADLINE))
|
||||
service.completed_at = int(snapshot.get(&"completed_at", -1))
|
||||
service.failure_reason = StringName(snapshot.get(&"failure_reason", ""))
|
||||
return service if service.is_valid() else null
|
||||
|
||||
|
||||
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _dictionary_copy(source: Variant) -> Dictionary:
|
||||
return source.duplicate(true) if source is Dictionary else {}
|
||||
|
||||
|
||||
static func _string_name_array(source: Variant) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
if source is Array:
|
||||
for value: Variant in source:
|
||||
values.append(StringName(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _names_to_strings(source: Array[StringName]) -> Array[String]:
|
||||
var values: Array[String] = []
|
||||
for value: StringName in source:
|
||||
values.append(String(value))
|
||||
return values
|
||||
@@ -0,0 +1 @@
|
||||
uid://cj7rmhd4xylof
|
||||
@@ -0,0 +1,138 @@
|
||||
class_name ServiceRequest
|
||||
extends RefCounted
|
||||
|
||||
## Immutable-at-enqueue quotation for one facility visit.
|
||||
##
|
||||
## FacilityService owns the lifecycle. Price and policy values are copied when
|
||||
## this object is configured so later facility policy changes cannot alter an
|
||||
## already queued request.
|
||||
|
||||
var request_id: StringName = &""
|
||||
var guest_ids: Array[StringName] = []
|
||||
var facility_id: StringName = &""
|
||||
var service_type: StringName = &"default"
|
||||
var created_at := 0
|
||||
var quoted_price := 0
|
||||
var policy_snapshot: Dictionary = {}
|
||||
var policy_revision := 0
|
||||
var reserved_inventory_ids: Array[StringName] = []
|
||||
var preference_match := 0
|
||||
var expected_quality := 0
|
||||
var expected_wait := 0
|
||||
var priority := 0
|
||||
var enqueued_at := -1
|
||||
var service_minutes := 1
|
||||
|
||||
|
||||
func configure(
|
||||
p_request_id: StringName,
|
||||
p_facility_id: StringName,
|
||||
p_guest_ids: Array[StringName],
|
||||
p_service_type: StringName,
|
||||
p_created_at: int,
|
||||
options: Dictionary = {}
|
||||
) -> ServiceRequest:
|
||||
request_id = p_request_id
|
||||
facility_id = p_facility_id
|
||||
guest_ids.assign(p_guest_ids)
|
||||
service_type = p_service_type
|
||||
created_at = maxi(p_created_at, 0)
|
||||
quoted_price = maxi(int(_value(options, &"quoted_price", 0)), 0)
|
||||
policy_snapshot = _dictionary_copy(_value(options, &"policy_snapshot", {}))
|
||||
policy_revision = maxi(int(_value(options, &"policy_revision", 0)), 0)
|
||||
reserved_inventory_ids = _string_name_array(
|
||||
_value(options, &"reserved_inventory_ids", [])
|
||||
)
|
||||
preference_match = int(_value(options, &"preference_match", 0))
|
||||
expected_quality = int(_value(options, &"expected_quality", 0))
|
||||
expected_wait = maxi(int(_value(options, &"expected_wait", 0)), 0)
|
||||
priority = int(_value(options, &"priority", 0))
|
||||
enqueued_at = int(_value(options, &"enqueued_at", -1))
|
||||
service_minutes = maxi(int(_value(options, &"service_minutes", 1)), 1)
|
||||
return self
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if request_id.is_empty() or facility_id.is_empty() or service_type.is_empty():
|
||||
return false
|
||||
if guest_ids.is_empty() or service_minutes <= 0 or quoted_price < 0:
|
||||
return false
|
||||
var seen: Dictionary = {}
|
||||
for guest_id: StringName in guest_ids:
|
||||
if guest_id.is_empty() or seen.has(guest_id):
|
||||
return false
|
||||
seen[guest_id] = true
|
||||
return true
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"request_id": String(request_id),
|
||||
&"guest_ids": _names_to_strings(guest_ids),
|
||||
&"facility_id": String(facility_id),
|
||||
&"service_type": String(service_type),
|
||||
&"created_at": created_at,
|
||||
&"quoted_price": quoted_price,
|
||||
&"policy_snapshot": policy_snapshot.duplicate(true),
|
||||
&"policy_revision": policy_revision,
|
||||
&"reserved_inventory_ids": _names_to_strings(reserved_inventory_ids),
|
||||
&"preference_match": preference_match,
|
||||
&"expected_quality": expected_quality,
|
||||
&"expected_wait": expected_wait,
|
||||
&"priority": priority,
|
||||
&"enqueued_at": enqueued_at,
|
||||
&"service_minutes": service_minutes,
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> ServiceRequest:
|
||||
if not snapshot.get(&"guest_ids", null) is Array:
|
||||
return null
|
||||
if not snapshot.get(&"policy_snapshot", null) is Dictionary:
|
||||
return null
|
||||
if not snapshot.get(&"reserved_inventory_ids", null) is Array:
|
||||
return null
|
||||
var request := ServiceRequest.new().configure(
|
||||
StringName(snapshot.get(&"request_id", "")),
|
||||
StringName(snapshot.get(&"facility_id", "")),
|
||||
_string_name_array(snapshot.get(&"guest_ids", [])),
|
||||
StringName(snapshot.get(&"service_type", "default")),
|
||||
int(snapshot.get(&"created_at", 0)),
|
||||
{
|
||||
&"quoted_price": snapshot.get(&"quoted_price", 0),
|
||||
&"policy_snapshot": snapshot.get(&"policy_snapshot", {}),
|
||||
&"policy_revision": snapshot.get(&"policy_revision", 0),
|
||||
&"reserved_inventory_ids": snapshot.get(&"reserved_inventory_ids", []),
|
||||
&"preference_match": snapshot.get(&"preference_match", 0),
|
||||
&"expected_quality": snapshot.get(&"expected_quality", 0),
|
||||
&"expected_wait": snapshot.get(&"expected_wait", 0),
|
||||
&"priority": snapshot.get(&"priority", 0),
|
||||
&"enqueued_at": snapshot.get(&"enqueued_at", -1),
|
||||
&"service_minutes": snapshot.get(&"service_minutes", 1),
|
||||
}
|
||||
)
|
||||
return request if request.is_valid() else null
|
||||
|
||||
|
||||
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _dictionary_copy(source: Variant) -> Dictionary:
|
||||
return source.duplicate(true) if source is Dictionary else {}
|
||||
|
||||
|
||||
static func _string_name_array(source: Variant) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
if not source is Array:
|
||||
return values
|
||||
for value: Variant in source:
|
||||
values.append(StringName(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _names_to_strings(source: Array[StringName]) -> Array[String]:
|
||||
var values: Array[String] = []
|
||||
for value: StringName in source:
|
||||
values.append(String(value))
|
||||
return values
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqhf3q6264fc3
|
||||
Reference in New Issue
Block a user