class_name LogisticsRequest extends RefCounted enum State { OPEN, RESERVED, ASSIGNED, PICKING_UP, DELIVERING, COMPLETED, BLOCKED, CANCELLED, } const MAX_PRIORITY := 100 var request_id: StringName = &"" var source_inventory_id: StringName = &"" var destination_inventory_id: StringName = &"" var item_requirements: Dictionary = {} var priority := 0 var created_at := 0 var deadline := -1 var total_weight := 0 var reserved_stack_ids: Array[StringName] = [] var reservation_ids: Array[StringName] = [] var assigned_carrier_id: StringName = &"" var state: State = State.OPEN var retry_count := 0 var state_reason := "" var state_changed_at := 0 func configure( p_request_id: StringName, p_source_inventory_id: StringName, p_destination_inventory_id: StringName, p_item_requirements: Dictionary, p_priority: int, p_created_at: int, p_deadline: int, p_total_weight: int ) -> LogisticsRequest: request_id = p_request_id source_inventory_id = p_source_inventory_id destination_inventory_id = p_destination_inventory_id item_requirements = _normalize_requirements(p_item_requirements) priority = clampi(p_priority, 0, MAX_PRIORITY) created_at = maxi(p_created_at, 0) deadline = p_deadline total_weight = maxi(p_total_weight, 0) state_changed_at = created_at return self func mark_reserved( p_reservation_ids: Array[StringName], p_reserved_stack_ids: Array[StringName] = [], absolute_minute: int = -1 ) -> bool: if state != State.OPEN and state != State.BLOCKED: return false if p_reservation_ids.is_empty(): return false reservation_ids.assign(p_reservation_ids) reserved_stack_ids.assign(p_reserved_stack_ids) assigned_carrier_id = &"" return transition_to(State.RESERVED, "", absolute_minute) func assign(carrier_id: StringName, absolute_minute: int = -1) -> bool: if carrier_id == &"" or state != State.RESERVED: return false assigned_carrier_id = carrier_id if transition_to(State.ASSIGNED, "", absolute_minute): return true assigned_carrier_id = &"" return false func unassign(absolute_minute: int = -1) -> bool: if state != State.ASSIGNED: return false assigned_carrier_id = &"" return transition_to(State.RESERVED, "", absolute_minute) func mark_blocked(reason: String, absolute_minute: int = -1) -> bool: if state == State.COMPLETED or state == State.CANCELLED: return false retry_count += 1 assigned_carrier_id = &"" return transition_to(State.BLOCKED, reason, absolute_minute) func cancel(reason: String = "", absolute_minute: int = -1) -> bool: return transition_to(State.CANCELLED, reason, absolute_minute) func transition_to(next_state: State, reason: String = "", absolute_minute: int = -1) -> bool: if state == next_state: return true if not _can_transition(state, next_state): return false state = next_state state_reason = reason state_changed_at = created_at if absolute_minute < 0 else absolute_minute if state == State.COMPLETED or state == State.CANCELLED: assigned_carrier_id = &"" return true func is_terminal() -> bool: return state == State.COMPLETED or state == State.CANCELLED func is_dispatchable() -> bool: return state == State.RESERVED and assigned_carrier_id == &"" and total_weight > 0 func create_snapshot() -> Dictionary: return { &"request_id": String(request_id), &"source_inventory_id": String(source_inventory_id), &"destination_inventory_id": String(destination_inventory_id), &"item_requirements": item_requirements.duplicate(true), &"priority": priority, &"created_at": created_at, &"deadline": deadline, &"total_weight": total_weight, &"reserved_stack_ids": _names_to_strings(reserved_stack_ids), &"reservation_ids": _names_to_strings(reservation_ids), &"assigned_carrier_id": String(assigned_carrier_id), &"state": State.keys()[state], &"retry_count": retry_count, &"state_reason": state_reason, &"state_changed_at": state_changed_at, } static func from_snapshot(snapshot: Dictionary) -> LogisticsRequest: if not _is_valid_snapshot(snapshot): return null var state_index := State.keys().find(String(snapshot.get(&"state", "OPEN"))) if state_index < 0: return null var request := LogisticsRequest.new().configure( StringName(snapshot.get(&"request_id", "")), StringName(snapshot.get(&"source_inventory_id", "")), StringName(snapshot.get(&"destination_inventory_id", "")), snapshot.get(&"item_requirements", {}), int(snapshot.get(&"priority", 0)), int(snapshot.get(&"created_at", 0)), int(snapshot.get(&"deadline", -1)), int(snapshot.get(&"total_weight", 0)) ) request.reserved_stack_ids = _parse_names(snapshot.get(&"reserved_stack_ids", [])) request.reservation_ids = _parse_names(snapshot.get(&"reservation_ids", [])) request.assigned_carrier_id = StringName(snapshot.get(&"assigned_carrier_id", "")) request.state = state_index as State request.retry_count = maxi(int(snapshot.get(&"retry_count", 0)), 0) request.state_reason = String(snapshot.get(&"state_reason", "")) request.state_changed_at = int(snapshot.get(&"state_changed_at", request.created_at)) if request.state == State.ASSIGNED and request.assigned_carrier_id == &"": return null return request static func _can_transition(from_state: State, to_state: State) -> bool: match from_state: State.OPEN: return to_state in [State.RESERVED, State.BLOCKED, State.CANCELLED] State.RESERVED: return to_state in [State.OPEN, State.ASSIGNED, State.BLOCKED, State.CANCELLED] State.ASSIGNED: return to_state in [State.RESERVED, State.PICKING_UP, State.BLOCKED, State.CANCELLED] State.PICKING_UP: return to_state in [State.DELIVERING, State.BLOCKED, State.CANCELLED] State.DELIVERING: return to_state in [State.COMPLETED, State.BLOCKED, State.CANCELLED] State.BLOCKED: return to_state in [State.OPEN, State.RESERVED, State.CANCELLED] _: return false static func _normalize_requirements(source: Variant) -> Dictionary: var normalized: Dictionary = {} if not source is Dictionary: return normalized for raw_item_id: Variant in source: var item_id := StringName(raw_item_id) var quantity := int(source[raw_item_id]) if item_id != &"" and quantity > 0: normalized[item_id] = quantity return normalized static func _is_valid_snapshot(snapshot: Dictionary) -> bool: return ( not String(snapshot.get(&"request_id", "")).is_empty() and not String(snapshot.get(&"source_inventory_id", "")).is_empty() and not String(snapshot.get(&"destination_inventory_id", "")).is_empty() and snapshot.get(&"item_requirements", null) is Dictionary and int(snapshot.get(&"total_weight", -1)) >= 0 ) static func _parse_names(source: Variant) -> Array[StringName]: var result: Array[StringName] = [] if not source is Array: return result for value: Variant in source: result.append(StringName(value)) return result static func _names_to_strings(source: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in source: result.append(String(value)) return result