Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
class_name InventoryReservation
extends RefCounted
enum State {
ACTIVE,
CONSUMED,
RELEASED,
EXPIRED,
}
var reservation_id: StringName = &""
var stack_id: StringName = &""
var owner_inventory_id: StringName = &""
var item_id: StringName = &""
var quantity := 0
var requester_id: StringName = &""
var purpose: StringName = &""
var created_at := 0
var expires_at := -1
var state: State = State.ACTIVE
var terminal_at := -1
func configure(
p_reservation_id: StringName,
p_stack_id: StringName,
p_owner_inventory_id: StringName,
p_item_id: StringName,
p_quantity: int,
p_requester_id: StringName,
p_purpose: StringName,
p_created_at: int,
p_expires_at: int = -1
) -> InventoryReservation:
reservation_id = p_reservation_id
stack_id = p_stack_id
owner_inventory_id = p_owner_inventory_id
item_id = p_item_id
quantity = maxi(p_quantity, 0)
requester_id = p_requester_id
purpose = p_purpose
created_at = maxi(p_created_at, 0)
expires_at = p_expires_at
return self
func is_active() -> bool:
return state == State.ACTIVE
func is_expired(absolute_minute: int) -> bool:
return expires_at >= 0 and absolute_minute >= expires_at
func mark_consumed(absolute_minute: int) -> bool:
if state != State.ACTIVE:
return false
state = State.CONSUMED
terminal_at = absolute_minute
return true
func mark_released(absolute_minute: int) -> bool:
if state != State.ACTIVE:
return false
state = State.RELEASED
terminal_at = absolute_minute
return true
func mark_expired(absolute_minute: int) -> bool:
if state != State.ACTIVE:
return false
state = State.EXPIRED
terminal_at = absolute_minute
return true
func create_snapshot() -> Dictionary:
return {
&"reservation_id": String(reservation_id),
&"stack_id": String(stack_id),
&"owner_inventory_id": String(owner_inventory_id),
&"item_id": String(item_id),
&"quantity": quantity,
&"requester_id": String(requester_id),
&"purpose": String(purpose),
&"created_at": created_at,
&"expires_at": expires_at,
&"state": State.keys()[state],
&"terminal_at": terminal_at,
}
static func from_snapshot(snapshot: Dictionary) -> InventoryReservation:
if not _is_valid_snapshot(snapshot):
return null
var state_index := State.keys().find(String(snapshot.get(&"state", "ACTIVE")))
if state_index < 0:
return null
var reservation := InventoryReservation.new().configure(
StringName(snapshot.get(&"reservation_id", "")),
StringName(snapshot.get(&"stack_id", "")),
StringName(snapshot.get(&"owner_inventory_id", "")),
StringName(snapshot.get(&"item_id", "")),
int(snapshot.get(&"quantity", 0)),
StringName(snapshot.get(&"requester_id", "")),
StringName(snapshot.get(&"purpose", "")),
int(snapshot.get(&"created_at", 0)),
int(snapshot.get(&"expires_at", -1))
)
reservation.state = state_index as State
reservation.terminal_at = int(snapshot.get(&"terminal_at", -1))
return reservation
static func _is_valid_snapshot(snapshot: Dictionary) -> bool:
return (
not String(snapshot.get(&"reservation_id", "")).is_empty()
and not String(snapshot.get(&"stack_id", "")).is_empty()
and not String(snapshot.get(&"owner_inventory_id", "")).is_empty()
and not String(snapshot.get(&"item_id", "")).is_empty()
and int(snapshot.get(&"quantity", 0)) > 0
and int(snapshot.get(&"created_at", -1)) >= 0
)
@@ -0,0 +1 @@
uid://ci4av5wp5tmid
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://kuhl1ypidibh
+481
View File
@@ -0,0 +1,481 @@
class_name LogisticsDispatcher
extends RefCounted
const LogisticsRequestType = preload("res://scripts/inventory/logistics_request.gd")
const LEGACY_SNAPSHOT_VERSION := 1
const SNAPSHOT_VERSION := 2
const PRIORITY_MULTIPLIER := 10
const PATH_MINUTE_PENALTY := 3
const DETOUR_MINUTE_PENALTY := 2
const DEADLINE_RISK_WINDOW := 120
var last_report: Dictionary = {}
var last_restore_error := ""
var restored_snapshot_version := SNAPSHOT_VERSION
var _carrier_reserved_load: Dictionary = {}
var _assignment_ownership: Dictionary = {}
var _assignment_history: Array[Dictionary] = []
func dispatch(
requests: Array,
carriers: Dictionary,
absolute_minute: int = 0
) -> Array[Dictionary]:
var pending: Array[LogisticsRequest] = []
for value: Variant in requests:
if value is LogisticsRequest and value.is_dispatchable():
pending.append(value)
pending.sort_custom(func(a: LogisticsRequest, b: LogisticsRequest) -> bool:
if a.priority != b.priority:
return a.priority > b.priority
var a_deadline := 9223372036854775807 if a.deadline < 0 else a.deadline
var b_deadline := 9223372036854775807 if b.deadline < 0 else b.deadline
if a_deadline != b_deadline:
return a_deadline < b_deadline
if a.created_at != b.created_at:
return a.created_at < b.created_at
return String(a.request_id) < String(b.request_id)
)
var normalized_carriers := _normalize_carriers(carriers)
# Carrier order is invariant for this dispatch pass. Building and sorting it
# once avoids repeating the same allocation and lexical sort for every request.
var ordered_carrier_ids := _sorted_names(normalized_carriers.keys())
# A carrier can disappear from one scheduler projection while its accepted
# assignment is still in flight. Keep those reservations until the owner
# explicitly releases them; otherwise reappearing carriers can be overbooked.
var reserved_loads: Dictionary = _carrier_reserved_load.duplicate(true)
var working_loads: Dictionary = {}
for carrier_id: StringName in ordered_carrier_ids:
var carrier: Dictionary = normalized_carriers[carrier_id]
working_loads[carrier_id] = maxi(
int(carrier[&"current_load"]),
int(_carrier_reserved_load.get(carrier_id, 0))
)
var assignments: Array[Dictionary] = []
var unassigned: Array[Dictionary] = []
for request: LogisticsRequest in pending:
if _assignment_ownership.has(request.request_id):
unassigned.append({
&"request_id": request.request_id,
&"reason": &"REQUEST_ALREADY_OWNED",
})
continue
var best: Dictionary = {}
var deadline_score := _deadline_risk(request.deadline, absolute_minute)
for carrier_id: StringName in ordered_carrier_ids:
var carrier: Dictionary = normalized_carriers[carrier_id]
var current_load := int(working_loads[carrier_id])
if current_load + request.total_weight > int(carrier[&"capacity"]):
continue
var path_minutes := _path_minutes_for(carrier, request)
if path_minutes < 0:
continue
# Keep score_candidate as the public single-candidate API, but avoid a
# second path lookup while evaluating the same pair in a dispatch batch.
var score := (
request.priority * PRIORITY_MULTIPLIER
- path_minutes * PATH_MINUTE_PENALTY
- int(carrier[&"detour_minutes"]) * DETOUR_MINUTE_PENALTY
+ _role_fitness(carrier, request)
+ deadline_score
)
if (
best.is_empty()
or score > int(best[&"score"])
or (score == int(best[&"score"]) and String(carrier_id) < String(best[&"carrier_id"]))
):
best = {
&"carrier_id": carrier_id,
&"score": score,
&"path_minutes": path_minutes,
&"position": carrier[&"position"],
}
if best.is_empty():
unassigned.append({
&"request_id": request.request_id,
&"reason": &"NO_ELIGIBLE_CARRIER",
})
continue
var selected_carrier_id: StringName = best[&"carrier_id"]
if not request.assign(selected_carrier_id, absolute_minute):
unassigned.append({
&"request_id": request.request_id,
&"reason": &"STATE_CHANGED",
})
continue
working_loads[selected_carrier_id] = int(working_loads[selected_carrier_id]) + request.total_weight
reserved_loads[selected_carrier_id] = (
int(reserved_loads.get(selected_carrier_id, 0)) + request.total_weight
)
var assignment := {
&"request_id": request.request_id,
&"carrier_id": selected_carrier_id,
&"score": int(best[&"score"]),
&"path_minutes": int(best[&"path_minutes"]),
&"weight": request.total_weight,
&"assigned_at": absolute_minute,
}
assignments.append(assignment)
_assignment_history.append(assignment.duplicate(true))
_assignment_ownership[request.request_id] = assignment.duplicate(true)
_carrier_reserved_load = reserved_loads
last_report = {
&"assigned": assignments.duplicate(true),
&"unassigned": unassigned,
&"carrier_loads": _carrier_reserved_load.duplicate(true),
}
return assignments
func score_candidate(
request: LogisticsRequest,
carrier: Dictionary,
absolute_minute: int
) -> int:
var path_minutes := _path_minutes_for(carrier, request)
if path_minutes < 0:
return -2_147_483_648
return (
request.priority * PRIORITY_MULTIPLIER
- path_minutes * PATH_MINUTE_PENALTY
- int(carrier.get(&"detour_minutes", 0)) * DETOUR_MINUTE_PENALTY
+ _role_fitness(carrier, request)
+ _deadline_risk(request.deadline, absolute_minute)
)
func release_assignment(carrier_id: StringName, weight: int) -> int:
var candidates: Array[StringName] = []
for request_id: StringName in _sorted_names(_assignment_ownership.keys()):
var ownership: Dictionary = _assignment_ownership[request_id]
if ownership[&"carrier_id"] == carrier_id and int(ownership[&"weight"]) == weight:
candidates.append(request_id)
if candidates.size() == 1:
release_assignment_for_request(candidates[0], carrier_id, weight)
return get_reserved_load(carrier_id)
if candidates.size() > 1 or restored_snapshot_version != LEGACY_SNAPSHOT_VERSION:
return get_reserved_load(carrier_id)
var next_load := maxi(int(_carrier_reserved_load.get(carrier_id, 0)) - maxi(weight, 0), 0)
if next_load == 0:
_carrier_reserved_load.erase(carrier_id)
else:
_carrier_reserved_load[carrier_id] = next_load
return next_load
func release_assignment_for_request(
request_id: StringName,
carrier_id: StringName,
weight: int
) -> bool:
if request_id.is_empty() or carrier_id.is_empty() or weight <= 0 \
or not _assignment_ownership.has(request_id):
return false
var ownership: Dictionary = _assignment_ownership[request_id]
if ownership[&"carrier_id"] != carrier_id or int(ownership[&"weight"]) != weight:
return false
var current_load := int(_carrier_reserved_load.get(carrier_id, 0))
if current_load < weight:
return false
var next_load := current_load - weight
if next_load == 0:
_carrier_reserved_load.erase(carrier_id)
else:
_carrier_reserved_load[carrier_id] = next_load
_assignment_ownership.erase(request_id)
return true
func get_reserved_load(carrier_id: StringName) -> int:
return int(_carrier_reserved_load.get(carrier_id, 0))
func get_assignment_ownership() -> Dictionary:
var result: Dictionary = {}
for request_id: StringName in _sorted_names(_assignment_ownership.keys()):
result[request_id] = (_assignment_ownership[request_id] as Dictionary).duplicate(true)
return result
func get_assignment_history() -> Array[Dictionary]:
return _assignment_history.duplicate(true)
func clear_runtime_loads() -> void:
_carrier_reserved_load.clear()
_assignment_ownership.clear()
func create_snapshot() -> Dictionary:
var carrier_loads: Array[Dictionary] = []
for carrier_id: StringName in _sorted_names(_carrier_reserved_load.keys()):
carrier_loads.append({
&"carrier_id": String(carrier_id),
&"reserved_load": int(_carrier_reserved_load[carrier_id]),
})
var history: Array[Dictionary] = []
for assignment: Dictionary in _assignment_history:
var serialized := assignment.duplicate(true)
serialized[&"request_id"] = String(assignment[&"request_id"])
serialized[&"carrier_id"] = String(assignment[&"carrier_id"])
history.append(serialized)
var ownership: Array[Dictionary] = []
for request_id: StringName in _sorted_names(_assignment_ownership.keys()):
var entry: Dictionary = _assignment_ownership[request_id]
ownership.append({
&"request_id": String(request_id),
&"carrier_id": String(entry[&"carrier_id"]),
&"weight": int(entry[&"weight"]),
&"assigned_at": int(entry[&"assigned_at"]),
})
return {
&"version": SNAPSHOT_VERSION,
&"carrier_loads": carrier_loads,
&"assignment_ownership": ownership,
&"assignment_history": history,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
last_restore_error = ""
var version := int(snapshot.get(&"version", -1))
if version not in [LEGACY_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
last_restore_error = "Unsupported logistics dispatcher snapshot version."
return false
var raw_loads: Variant = snapshot.get(&"carrier_loads", null)
var raw_history: Variant = snapshot.get(&"assignment_history", null)
var raw_ownership: Variant = snapshot.get(&"assignment_ownership", null)
if not raw_loads is Array or not raw_history is Array \
or (version == SNAPSHOT_VERSION and not raw_ownership is Array):
last_restore_error = "Logistics dispatcher snapshot collections are malformed."
return false
var next_loads: Dictionary = {}
for raw_load: Variant in raw_loads:
if not raw_load is Dictionary:
last_restore_error = "Carrier load entry must be a Dictionary."
return false
var carrier_id := StringName(raw_load.get(&"carrier_id", ""))
var reserved_load := int(raw_load.get(&"reserved_load", -1))
if carrier_id == &"" or reserved_load < 0 or next_loads.has(carrier_id) \
or (version == SNAPSHOT_VERSION and reserved_load == 0):
last_restore_error = "Carrier load entry is invalid or duplicated."
return false
next_loads[carrier_id] = reserved_load
var next_history: Array[Dictionary] = []
var history_fingerprints: Dictionary = {}
for raw_assignment: Variant in raw_history:
if not raw_assignment is Dictionary:
last_restore_error = "Assignment history entry must be a Dictionary."
return false
var request_id := StringName(raw_assignment.get(&"request_id", ""))
var carrier_id := StringName(raw_assignment.get(&"carrier_id", ""))
var weight_value: Variant = raw_assignment.get(&"weight", null)
var assigned_at_value: Variant = raw_assignment.get(&"assigned_at", null)
if request_id == &"" or carrier_id == &"" or not weight_value is int \
or int(weight_value) < (1 if version == SNAPSHOT_VERSION else 0) \
or (version == SNAPSHOT_VERSION and (
not assigned_at_value is int or int(assigned_at_value) < 0
)):
last_restore_error = "Assignment history entry is invalid."
return false
var assignment: Dictionary = raw_assignment.duplicate(true)
assignment[&"request_id"] = request_id
assignment[&"carrier_id"] = carrier_id
var fingerprint := "%s|%s|%d|%d" % [
request_id,
carrier_id,
int(weight_value),
int(assigned_at_value) if assigned_at_value is int else -1,
]
if version == SNAPSHOT_VERSION and history_fingerprints.has(fingerprint):
last_restore_error = "Assignment history contains an exact duplicate."
return false
history_fingerprints[fingerprint] = true
next_history.append(assignment)
var next_ownership: Dictionary = {}
if version == SNAPSHOT_VERSION:
for raw_entry: Variant in raw_ownership:
if not raw_entry is Dictionary:
last_restore_error = "Assignment ownership entry must be a Dictionary."
return false
var request_id := StringName(raw_entry.get(&"request_id", ""))
var carrier_id := StringName(raw_entry.get(&"carrier_id", ""))
var weight_value: Variant = raw_entry.get(&"weight", null)
var assigned_at_value: Variant = raw_entry.get(&"assigned_at", null)
if request_id.is_empty() or carrier_id.is_empty() or next_ownership.has(request_id) \
or not weight_value is int or int(weight_value) <= 0 \
or not assigned_at_value is int or int(assigned_at_value) < 0:
last_restore_error = "Assignment ownership entry is invalid or duplicated."
return false
next_ownership[request_id] = {
&"request_id": request_id,
&"carrier_id": carrier_id,
&"weight": int(weight_value),
&"assigned_at": int(assigned_at_value),
}
if not _ownership_matches_loads_and_history(next_ownership, next_loads, next_history):
last_restore_error = "Assignment ownership does not match reserved loads or history."
return false
_carrier_reserved_load = next_loads
_assignment_ownership = next_ownership
_assignment_history = next_history
restored_snapshot_version = version
last_restore_error = ""
return true
func migrate_legacy_assignment_ownership(expected: Dictionary) -> bool:
if restored_snapshot_version != LEGACY_SNAPSHOT_VERSION or not _assignment_ownership.is_empty():
return false
var normalized: Dictionary = {}
for request_id: StringName in _sorted_names(expected.keys()):
var raw_entry: Variant = expected[request_id]
if request_id.is_empty() or not raw_entry is Dictionary:
return false
var carrier_id := StringName(raw_entry.get(&"carrier_id", &""))
var weight := int(raw_entry.get(&"weight", -1))
var assigned_at := int(raw_entry.get(&"assigned_at", -1))
if carrier_id.is_empty() or weight <= 0 or assigned_at < 0:
return false
normalized[request_id] = {
&"request_id": request_id,
&"carrier_id": carrier_id,
&"weight": weight,
&"assigned_at": assigned_at,
}
var rebuilt_loads: Dictionary = {}
for request_id: StringName in _sorted_names(normalized.keys()):
var entry: Dictionary = normalized[request_id]
var carrier_id: StringName = entry[&"carrier_id"]
rebuilt_loads[carrier_id] = int(rebuilt_loads.get(carrier_id, 0)) + int(entry[&"weight"])
if int(_carrier_reserved_load.get(carrier_id, -1)) < int(rebuilt_loads[carrier_id]):
return false
# v1 mixed external carrier.current_load into this table. Active Requests plus
# assignment history are the only durable ownership authority during migration;
# any excess legacy load is therefore discarded deterministically.
if not _ownership_matches_loads_and_history(
normalized,
rebuilt_loads,
_assignment_history
):
return false
_carrier_reserved_load = rebuilt_loads
_assignment_ownership = normalized
restored_snapshot_version = SNAPSHOT_VERSION
return true
static func _ownership_matches_loads_and_history(
ownership: Dictionary,
loads: Dictionary,
history: Array[Dictionary]
) -> bool:
var expected_loads: Dictionary = {}
var latest_by_request: Dictionary = {}
for assignment: Dictionary in history:
latest_by_request[StringName(assignment[&"request_id"])] = assignment
for request_id: StringName in _sorted_names(ownership.keys()):
var entry: Dictionary = ownership[request_id]
var carrier_id: StringName = entry[&"carrier_id"]
var weight := int(entry[&"weight"])
expected_loads[carrier_id] = int(expected_loads.get(carrier_id, 0)) + weight
if not latest_by_request.has(request_id):
return false
var latest: Dictionary = latest_by_request[request_id]
if StringName(latest[&"carrier_id"]) != carrier_id \
or int(latest[&"weight"]) != weight \
or int(latest.get(&"assigned_at", -1)) != int(entry[&"assigned_at"]):
return false
if expected_loads.size() != loads.size():
return false
for carrier_id: StringName in _sorted_names(expected_loads.keys()):
if int(loads.get(carrier_id, -1)) != int(expected_loads[carrier_id]):
return false
return true
static func _normalize_carriers(carriers: Dictionary) -> Dictionary:
var result: Dictionary = {}
for raw_carrier_id: Variant in carriers:
if not carriers[raw_carrier_id] is Dictionary:
continue
var carrier_id := StringName(raw_carrier_id)
var source: Dictionary = carriers[raw_carrier_id]
var capacity := int(source.get(&"capacity", source.get(&"capacity_weight", 0)))
var current_load := maxi(int(source.get(&"current_load", 0)), 0)
if carrier_id == &"" or capacity <= 0 or current_load > capacity:
continue
result[carrier_id] = {
&"carrier_id": carrier_id,
&"position": source.get(&"position", Vector3i.ZERO),
&"capacity": capacity,
&"current_load": current_load,
&"role": StringName(source.get(&"role", &"")),
&"role_fit": source.get(&"role_fit", 0),
&"path_minutes": source.get(&"path_minutes", -1),
&"detour_minutes": maxi(int(source.get(&"detour_minutes", 0)), 0),
}
return result
static func _path_minutes_for(carrier: Dictionary, request: LogisticsRequest) -> int:
var path_source: Variant = carrier.get(&"path_minutes", -1)
if path_source is int or path_source is float:
return int(path_source)
if not path_source is Dictionary:
return -1
var paths: Dictionary = path_source
for key: Variant in [request.request_id, request.source_inventory_id, &"default", "default"]:
if paths.has(key):
return int(paths[key])
return -1
static func _role_fitness(carrier: Dictionary, request: LogisticsRequest) -> int:
var explicit_fit: Variant = carrier.get(&"role_fit", 0)
if explicit_fit is Dictionary:
var fits: Dictionary = explicit_fit
if fits.has(request.request_id):
return int(fits[request.request_id])
if fits.has(&"default"):
return int(fits[&"default"])
elif explicit_fit is int or explicit_fit is float:
var fit := int(explicit_fit)
if fit != 0:
return fit
match String(carrier.get(&"role", &"")).to_lower():
"logistics":
return 100
"porter":
return 80
"carrier":
return 60
_:
return 0
static func _deadline_risk(deadline: int, absolute_minute: int) -> int:
if deadline < 0:
return 0
var remaining := deadline - absolute_minute
if remaining <= 0:
return 200 + mini(-remaining, 100)
return maxi(DEADLINE_RISK_WINDOW - remaining, 0)
static func _sorted_names(values: Variant) -> Array[StringName]:
var result: Array[StringName] = []
for value: Variant in values:
result.append(StringName(value))
result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return result
@@ -0,0 +1 @@
uid://bakay5aor6q03
+222
View File
@@ -0,0 +1,222 @@
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
@@ -0,0 +1 @@
uid://bny06mspvcg8u