Files
TheTree_mock/scripts/inventory/inventory_service.gd
T

1564 lines
57 KiB
GDScript

class_name InventoryService
extends RefCounted
const InventoryReservationType = preload("res://scripts/inventory/inventory_reservation.gd")
const SNAPSHOT_VERSION := 2
const LEGACY_SNAPSHOT_VERSION := 1
const DEFAULT_QUALITY := 100
const DEFAULT_UNIT_WEIGHT := 1
var current_minute := 0
var last_restore_error := ""
var last_repair_report: Dictionary = {}
var _inventories: Dictionary = {}
## Derived O(1) stack location index. It is rebuilt after restore and repaired
## lazily if diagnostic code mutates the public test seam `_inventories`.
var _stack_owner_by_id: Dictionary = {}
var _reservations: Dictionary = {}
## Persisted tombstones keep generated and explicit ids single-use even after
## their live stack/reservation has been removed.
var _known_stack_ids: Dictionary = {}
var _known_reservation_ids: Dictionary = {}
var _idempotency_results: Dictionary = {}
var _next_stack_sequence := 1
var _next_reservation_sequence := 1
var _atomic_batch_depth := 0
func add_stack(
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
quality: int = DEFAULT_QUALITY,
expires_at: int = -1,
created_at: int = -1,
unit_weight: int = DEFAULT_UNIT_WEIGHT,
stack_id: StringName = &""
) -> Dictionary:
if owner_inventory_id == &"" or item_id == &"":
return _failure(&"INVALID_ID", "Inventory and item ids are required.")
if quantity <= 0 or unit_weight <= 0:
return _failure(&"INVALID_QUANTITY", "Quantity and unit weight must be positive.")
var effective_stack_id := stack_id
if effective_stack_id == &"":
effective_stack_id = _next_stack_id()
elif _known_stack_ids.has(effective_stack_id) or bool(_find_stack(effective_stack_id)[&"found"]):
return _failure(&"DUPLICATE_STACK", "Stack id has already been used.")
var stack := _make_stack(
effective_stack_id,
owner_inventory_id,
item_id,
quantity,
quality,
-1 if expires_at < 0 else expires_at,
current_minute if created_at < 0 else created_at,
unit_weight
)
_insert_stack(stack)
return _success({&"stack_id": effective_stack_id, &"quantity": quantity})
func reserve_item(
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
requester_id: StringName,
purpose: StringName,
created_at: int = -1,
expires_at: int = -1,
operation_id: StringName = &""
) -> Dictionary:
var minute := current_minute if created_at < 0 else created_at
var normalized_expires_at := -1 if expires_at < 0 else expires_at
var fingerprint := _canonical_fingerprint([
owner_inventory_id,
item_id,
quantity,
requester_id,
purpose,
minute,
normalized_expires_at,
])
var replay := _get_replay(&"reserve", operation_id, fingerprint)
if not replay.is_empty():
return replay
if owner_inventory_id == &"" or item_id == &"" or requester_id == &"":
return _remember_failure(
&"reserve", operation_id, fingerprint, &"INVALID_ID", "Inventory, item, and requester ids are required."
)
if quantity <= 0:
return _remember_failure(
&"reserve", operation_id, fingerprint, &"INVALID_QUANTITY", "Reservation quantity must be positive."
)
var candidates := _available_stacks(owner_inventory_id, item_id, minute)
var total_available := 0
for stack: Dictionary in candidates:
total_available += _available_in_stack(stack)
if total_available < quantity:
return _remember_failure(
&"reserve",
operation_id,
fingerprint,
&"INSUFFICIENT_AVAILABLE",
"Available stock is below the requested quantity.",
{&"requested": quantity, &"available": total_available}
)
var remaining := quantity
var allocations: Array[Dictionary] = []
for stack: Dictionary in candidates:
if remaining <= 0:
break
var allocated := mini(remaining, _available_in_stack(stack))
if allocated > 0:
allocations.append({&"stack_id": stack[&"stack_id"], &"quantity": allocated})
remaining -= allocated
var reservation_ids: Array[StringName] = []
var stack_ids: Array[StringName] = []
for allocation: Dictionary in allocations:
var stack_id: StringName = allocation[&"stack_id"]
var location := _find_stack(stack_id)
var stack: Dictionary = location[&"stack"]
var allocated := int(allocation[&"quantity"])
stack[&"reserved_quantity"] = int(stack[&"reserved_quantity"]) + allocated
_set_stack(location[&"owner_inventory_id"], stack)
var reservation_id := _next_reservation_id()
var reservation := InventoryReservationType.new().configure(
reservation_id,
stack_id,
owner_inventory_id,
item_id,
allocated,
requester_id,
purpose,
minute,
normalized_expires_at
)
_reservations[reservation_id] = reservation
reservation_ids.append(reservation_id)
stack_ids.append(stack_id)
var result := _success({
&"reservation_ids": reservation_ids,
&"stack_ids": stack_ids,
&"quantity": quantity,
})
return _remember(&"reserve", operation_id, result, fingerprint)
func reserve(
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
requester_id: StringName,
purpose: StringName,
created_at: int = -1,
expires_at: int = -1,
operation_id: StringName = &""
) -> Dictionary:
return reserve_item(
owner_inventory_id,
item_id,
quantity,
requester_id,
purpose,
created_at,
expires_at,
operation_id
)
func consume_reservation(
reservation_id: StringName,
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var fingerprint := _canonical_fingerprint([reservation_id, minute])
var replay := _get_replay(&"consume", operation_id, fingerprint)
if not replay.is_empty():
return replay
if not _reservations.has(reservation_id):
return _remember_failure(
&"consume", operation_id, fingerprint, &"NOT_FOUND", "Reservation does not exist."
)
var reservation: InventoryReservation = _reservations[reservation_id]
if reservation.state == InventoryReservationType.State.CONSUMED:
return _remember(&"consume", operation_id, _success({
&"reservation_id": reservation_id,
&"quantity": 0,
&"replay": true,
&"code": &"ALREADY_CONSUMED",
}), fingerprint)
if not reservation.is_active():
return _remember_failure(
&"consume", operation_id, fingerprint, &"RESERVATION_CLOSED", "Reservation is no longer active."
)
var validation := _validate_active_reservation(reservation, minute)
if not bool(validation[&"ok"]):
return _remember(&"consume", operation_id, validation, fingerprint)
var location: Dictionary = validation[&"location"]
var stack: Dictionary = location[&"stack"]
stack[&"quantity"] = int(stack[&"quantity"]) - reservation.quantity
stack[&"reserved_quantity"] = int(stack[&"reserved_quantity"]) - reservation.quantity
reservation.mark_consumed(minute)
_set_or_remove_stack(location[&"owner_inventory_id"], stack)
var result := _success({
&"reservation_id": reservation_id,
&"stack_id": reservation.stack_id,
&"quantity": reservation.quantity,
})
return _remember(&"consume", operation_id, result, fingerprint)
func consume_reservations(
reservation_ids: Array[StringName],
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var fingerprint_ids := _sorted_names(reservation_ids)
var fingerprint := _canonical_fingerprint([fingerprint_ids, minute])
var replay := _get_replay(&"consume_many", operation_id, fingerprint)
if not replay.is_empty():
return replay
var unique_ids := _unique_sorted_names(reservation_ids)
if unique_ids.is_empty() or unique_ids.size() != reservation_ids.size():
return _remember_failure(
&"consume_many", operation_id, fingerprint, &"INVALID_RESERVATIONS", "Reservation ids must be unique and non-empty."
)
for reservation_id: StringName in unique_ids:
if not _reservations.has(reservation_id):
return _remember_failure(
&"consume_many", operation_id, fingerprint, &"NOT_FOUND", "Reservation does not exist."
)
var validation := _validate_active_reservation(_reservations[reservation_id], minute)
if not bool(validation[&"ok"]):
return _remember(&"consume_many", operation_id, validation, fingerprint)
var capture := create_snapshot()
var consumed := 0
for reservation_id: StringName in unique_ids:
var result := consume_reservation(reservation_id, &"", minute)
if not bool(result[&"ok"]):
restore_snapshot(capture)
return _remember_failure(
&"consume_many",
operation_id,
fingerprint,
&"ATOMIC_APPLY_FAILED",
"Reservation group consumption was rolled back.",
{&"cause": result}
)
consumed += int(result.get(&"quantity", 0))
var final_result := _success({
&"reservation_ids": unique_ids,
&"quantity": consumed,
})
return _remember(&"consume_many", operation_id, final_result, fingerprint)
func release_reservation(
reservation_id: StringName,
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var fingerprint := _canonical_fingerprint([reservation_id, minute])
var replay := _get_replay(&"release", operation_id, fingerprint)
if not replay.is_empty():
return replay
if not _reservations.has(reservation_id):
return _remember_failure(
&"release", operation_id, fingerprint, &"NOT_FOUND", "Reservation does not exist."
)
var reservation: InventoryReservation = _reservations[reservation_id]
if reservation.state == InventoryReservationType.State.RELEASED or reservation.state == InventoryReservationType.State.EXPIRED:
return _remember(&"release", operation_id, _success({
&"reservation_id": reservation_id,
&"quantity": 0,
&"replay": true,
&"code": &"ALREADY_RELEASED",
}), fingerprint)
if not reservation.is_active():
return _remember_failure(
&"release", operation_id, fingerprint, &"RESERVATION_CLOSED", "Consumed stock cannot be released."
)
var location := _find_stack(reservation.stack_id)
if not bool(location[&"found"]):
reservation.mark_released(minute)
var orphan_result := _success({
&"reservation_id": reservation_id,
&"quantity": 0,
&"orphan_repaired": true,
})
return _remember(&"release", operation_id, orphan_result, fingerprint)
var stack: Dictionary = location[&"stack"]
stack[&"reserved_quantity"] = maxi(
int(stack[&"reserved_quantity"]) - reservation.quantity,
0
)
reservation.mark_released(minute)
_set_or_remove_stack(location[&"owner_inventory_id"], stack)
var result := _success({
&"reservation_id": reservation_id,
&"quantity": reservation.quantity,
})
return _remember(&"release", operation_id, result, fingerprint)
func transfer_available(
source_inventory_id: StringName,
destination_inventory_id: StringName,
item_id: StringName,
quantity: int,
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var fingerprint := _transfer_fingerprint(
source_inventory_id,
destination_inventory_id,
item_id,
quantity,
minute
)
var replay := _get_replay(&"transfer", operation_id, fingerprint)
if not replay.is_empty():
return replay
if source_inventory_id == &"" or destination_inventory_id == &"" or item_id == &"":
return _remember_failure(&"transfer", operation_id, fingerprint, &"INVALID_ID", "Transfer ids are required.")
if source_inventory_id == destination_inventory_id:
return _remember_failure(
&"transfer", operation_id, fingerprint, &"SAME_INVENTORY", "Source and destination must differ."
)
if quantity <= 0:
return _remember_failure(
&"transfer", operation_id, fingerprint, &"INVALID_QUANTITY", "Transfer quantity must be positive."
)
var candidates := _available_stacks(source_inventory_id, item_id, minute)
var available := 0
for stack: Dictionary in candidates:
available += _available_in_stack(stack)
if available < quantity:
return _remember_failure(
&"transfer",
operation_id,
fingerprint,
&"INSUFFICIENT_AVAILABLE",
"Available stock is below the transfer quantity.",
{&"requested": quantity, &"available": available}
)
var capture := {} if _atomic_batch_depth > 0 else create_snapshot()
var remaining := quantity
var destination_stack_ids: Array[StringName] = []
for candidate: Dictionary in candidates:
if remaining <= 0:
break
var moved := mini(remaining, _available_in_stack(candidate))
if moved <= 0:
continue
var location := _find_stack(candidate[&"stack_id"])
if not bool(location[&"found"]):
_restore_standalone_capture(capture)
return _remember_failure(
&"transfer", operation_id, fingerprint, &"ATOMIC_APPLY_FAILED", "Transfer was rolled back."
)
var source_stack: Dictionary = location[&"stack"]
source_stack[&"quantity"] = int(source_stack[&"quantity"]) - moved
_set_or_remove_stack(source_inventory_id, source_stack)
var destination_stack_id := _next_stack_id()
_insert_stack(_make_stack(
destination_stack_id,
destination_inventory_id,
item_id,
moved,
int(candidate[&"quality"]),
int(candidate[&"expires_at"]),
int(candidate[&"created_at"]),
int(candidate[&"unit_weight"])
))
destination_stack_ids.append(destination_stack_id)
remaining -= moved
if remaining != 0 or (_atomic_batch_depth == 0 and not validate_invariants()[&"ok"]):
_restore_standalone_capture(capture)
return _remember_failure(
&"transfer", operation_id, fingerprint, &"ATOMIC_APPLY_FAILED", "Transfer invariant failed and was rolled back."
)
var result := _success({
&"source_inventory_id": source_inventory_id,
&"destination_inventory_id": destination_inventory_id,
&"item_id": item_id,
&"quantity": quantity,
&"destination_stack_ids": destination_stack_ids,
})
return _remember(&"transfer", operation_id, result, fingerprint)
func transfer(
source_inventory_id: StringName,
destination_inventory_id: StringName,
item_id: StringName,
quantity: int,
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
return transfer_available(
source_inventory_id,
destination_inventory_id,
item_id,
quantity,
operation_id,
absolute_minute
)
## Applies the same ordered transfers as repeated transfer_available calls, but
## captures and validates state once for the whole atomic group. This keeps a
## large logistics wave deterministic without doing a full-world snapshot for
## every item line.
func transfer_available_batch(
transfers: Array[Dictionary],
absolute_minute: int = -1
) -> Dictionary:
if transfers.is_empty() or _atomic_batch_depth != 0:
return _failure(&"INVALID_BATCH", "A non-empty top-level transfer batch is required.")
var minute := current_minute if absolute_minute < 0 else absolute_minute
var preflight := _preflight_available_batch(transfers, minute)
if not bool(preflight[&"ok"]):
return preflight
var capture := create_snapshot()
var results: Array[Dictionary] = []
_atomic_batch_depth = 1
for index: int in range(transfers.size()):
var entry: Dictionary = transfers[index]
var result := transfer_available(
StringName(entry.get(&"source_inventory_id", entry.get("source_inventory_id", ""))),
StringName(entry.get(&"destination_inventory_id", entry.get("destination_inventory_id", ""))),
StringName(entry.get(&"item_id", entry.get("item_id", ""))),
int(entry.get(&"quantity", entry.get("quantity", 0))),
StringName(entry.get(&"operation_id", entry.get("operation_id", ""))),
minute
)
if not bool(result.get(&"ok", false)):
_atomic_batch_depth = 0
restore_snapshot(capture)
return _failure(
&"ATOMIC_BATCH_FAILED",
"Available-stock transfer batch was rolled back.",
{&"failed_index": index, &"cause": result}
)
results.append(result)
_atomic_batch_depth = 0
if not validate_invariants()[&"ok"]:
restore_snapshot(capture)
return _failure(
&"ATOMIC_BATCH_FAILED",
"Available-stock transfer batch invariant failed and was rolled back."
)
return _success({&"results": results, &"transfer_count": results.size()})
func transfer_reserved(
reservation_ids: Array[StringName],
destination_inventory_id: StringName,
operation_id: StringName = &"",
absolute_minute: int = -1
) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var fingerprint_ids := _sorted_names(reservation_ids)
var fingerprint := _reserved_transfer_fingerprint(
fingerprint_ids,
destination_inventory_id,
minute
)
var replay := _get_replay(&"transfer_reserved", operation_id, fingerprint)
if not replay.is_empty():
return replay
var ids := _unique_sorted_names(reservation_ids)
if ids.is_empty() or ids.size() != reservation_ids.size() or destination_inventory_id == &"":
return _remember_failure(
&"transfer_reserved",
operation_id,
fingerprint,
&"INVALID_ARGUMENT",
"Reservation ids must be unique and destination is required."
)
var source_inventory_id: StringName = &""
for reservation_id: StringName in ids:
if not _reservations.has(reservation_id):
return _remember_failure(
&"transfer_reserved", operation_id, fingerprint, &"NOT_FOUND", "Reservation does not exist."
)
var reservation: InventoryReservation = _reservations[reservation_id]
var validation := _validate_active_reservation(reservation, minute)
if not bool(validation[&"ok"]):
return _remember(&"transfer_reserved", operation_id, validation, fingerprint)
if source_inventory_id == &"":
source_inventory_id = reservation.owner_inventory_id
elif source_inventory_id != reservation.owner_inventory_id:
return _remember_failure(
&"transfer_reserved", operation_id, fingerprint, &"MULTIPLE_SOURCES", "One transfer cannot mix source inventories."
)
if source_inventory_id == destination_inventory_id:
return _remember_failure(
&"transfer_reserved", operation_id, fingerprint, &"SAME_INVENTORY", "Source and destination must differ."
)
var capture := {} if _atomic_batch_depth > 0 else create_snapshot()
var moved_quantity := 0
var moved_weight := 0
var destination_stack_ids: Array[StringName] = []
for reservation_id: StringName in ids:
var reservation: InventoryReservation = _reservations[reservation_id]
var location := _find_stack(reservation.stack_id)
if not bool(location[&"found"]):
_restore_standalone_capture(capture)
return _remember_failure(
&"transfer_reserved", operation_id, fingerprint, &"ATOMIC_APPLY_FAILED", "Transfer was rolled back."
)
var source_stack: Dictionary = location[&"stack"]
source_stack[&"quantity"] = int(source_stack[&"quantity"]) - reservation.quantity
source_stack[&"reserved_quantity"] = int(source_stack[&"reserved_quantity"]) - reservation.quantity
_set_or_remove_stack(source_inventory_id, source_stack)
reservation.mark_consumed(minute)
var destination_stack_id := _next_stack_id()
_insert_stack(_make_stack(
destination_stack_id,
destination_inventory_id,
reservation.item_id,
reservation.quantity,
int(source_stack[&"quality"]),
int(source_stack[&"expires_at"]),
int(source_stack[&"created_at"]),
int(source_stack[&"unit_weight"])
))
destination_stack_ids.append(destination_stack_id)
moved_quantity += reservation.quantity
moved_weight += reservation.quantity * int(source_stack[&"unit_weight"])
if _atomic_batch_depth == 0 and not validate_invariants()[&"ok"]:
_restore_standalone_capture(capture)
return _remember_failure(
&"transfer_reserved",
operation_id,
fingerprint,
&"ATOMIC_APPLY_FAILED",
"Transfer invariant failed and was rolled back."
)
var result := _success({
&"source_inventory_id": source_inventory_id,
&"destination_inventory_id": destination_inventory_id,
&"reservation_ids": ids,
&"destination_stack_ids": destination_stack_ids,
&"quantity": moved_quantity,
&"weight": moved_weight,
})
return _remember(&"transfer_reserved", operation_id, result, fingerprint)
## Atomic ordered counterpart to repeated transfer_reserved calls. Individual
## operation ids retain their normal replay semantics and snapshots remain
## byte-structure equivalent to the sequential path.
func transfer_reserved_batch(
transfers: Array[Dictionary],
absolute_minute: int = -1
) -> Dictionary:
if transfers.is_empty() or _atomic_batch_depth != 0:
return _failure(&"INVALID_BATCH", "A non-empty top-level reserved transfer batch is required.")
var minute := current_minute if absolute_minute < 0 else absolute_minute
var preflight := _preflight_reserved_batch(transfers, minute)
if not bool(preflight[&"ok"]):
return preflight
var capture := create_snapshot()
var results: Array[Dictionary] = []
_atomic_batch_depth = 1
for index: int in range(transfers.size()):
var entry: Dictionary = transfers[index]
var reservation_ids: Array[StringName] = []
var raw_ids: Variant = entry.get(&"reservation_ids", entry.get("reservation_ids", []))
if raw_ids is Array:
for raw_id: Variant in raw_ids:
reservation_ids.append(StringName(raw_id))
var result := transfer_reserved(
reservation_ids,
StringName(entry.get(&"destination_inventory_id", entry.get("destination_inventory_id", ""))),
StringName(entry.get(&"operation_id", entry.get("operation_id", ""))),
minute
)
if not bool(result.get(&"ok", false)):
_atomic_batch_depth = 0
restore_snapshot(capture)
return _failure(
&"ATOMIC_BATCH_FAILED",
"Reserved-stock transfer batch was rolled back.",
{&"failed_index": index, &"cause": result}
)
results.append(result)
_atomic_batch_depth = 0
if not validate_invariants()[&"ok"]:
restore_snapshot(capture)
return _failure(
&"ATOMIC_BATCH_FAILED",
"Reserved-stock transfer batch invariant failed and was rolled back."
)
return _success({&"results": results, &"transfer_count": results.size()})
func process_expired(absolute_minute: int, waste_inventory_id: StringName = &"") -> Dictionary:
current_minute = maxi(current_minute, absolute_minute)
var expired_reservations: Array[StringName] = []
for reservation_id: StringName in _sorted_names(_reservations.keys()):
var reservation: InventoryReservation = _reservations[reservation_id]
if not reservation.is_active() or not reservation.is_expired(absolute_minute):
continue
var location := _find_stack(reservation.stack_id)
if bool(location[&"found"]):
var stack: Dictionary = location[&"stack"]
stack[&"reserved_quantity"] = maxi(
int(stack[&"reserved_quantity"]) - reservation.quantity,
0
)
_set_or_remove_stack(location[&"owner_inventory_id"], stack)
reservation.mark_expired(absolute_minute)
expired_reservations.append(reservation_id)
var discarded: Array[Dictionary] = []
var waste_lines: Array[Dictionary] = []
for owner_inventory_id: StringName in _sorted_names(_inventories.keys()):
var stack_ids := _sorted_names((_inventories[owner_inventory_id] as Dictionary).keys())
for stack_id: StringName in stack_ids:
var location := _find_stack(stack_id)
if not bool(location[&"found"]):
continue
var stack: Dictionary = location[&"stack"]
if not _stack_is_expired(stack, absolute_minute):
continue
var disposable := _available_in_stack(stack)
if disposable <= 0:
continue
stack[&"quantity"] = int(stack[&"quantity"]) - disposable
_set_or_remove_stack(owner_inventory_id, stack)
var line := {
&"owner_inventory_id": owner_inventory_id,
&"stack_id": stack_id,
&"item_id": stack[&"item_id"],
&"quantity": disposable,
}
discarded.append(line)
if waste_inventory_id != &"":
waste_lines.append(line)
for line: Dictionary in waste_lines:
_insert_stack(_make_stack(
_next_stack_id(),
waste_inventory_id,
StringName("waste/%s" % String(line[&"item_id"])),
int(line[&"quantity"]),
0,
-1,
absolute_minute,
DEFAULT_UNIT_WEIGHT
))
return {
&"ok": true,
&"expired_reservation_ids": expired_reservations,
&"discarded": discarded,
&"discarded_quantity": _sum_lines(discarded),
}
func is_stack_expired(stack_id: StringName, absolute_minute: int = -1) -> bool:
var location := _find_stack(stack_id)
if not bool(location[&"found"]):
return false
return _stack_is_expired(
location[&"stack"],
current_minute if absolute_minute < 0 else absolute_minute
)
func available_quantity(
owner_inventory_id: StringName,
item_id: StringName,
absolute_minute: int = -1
) -> int:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var total := 0
for stack: Dictionary in _available_stacks(owner_inventory_id, item_id, minute):
total += _available_in_stack(stack)
return total
func total_quantity(owner_inventory_id: StringName, item_id: StringName) -> int:
var total := 0
if not _inventories.has(owner_inventory_id):
return total
for stack: Dictionary in (_inventories[owner_inventory_id] as Dictionary).values():
if stack[&"item_id"] == item_id:
total += int(stack[&"quantity"])
return total
func get_stack(stack_id: StringName) -> Dictionary:
var location := _find_stack(stack_id)
if not bool(location[&"found"]):
return {}
return (location[&"stack"] as Dictionary).duplicate(true)
func get_stacks(owner_inventory_id: StringName = &"") -> Array[Dictionary]:
var result: Array[Dictionary] = []
var owners: Array[StringName] = []
if owner_inventory_id == &"":
owners = _sorted_names(_inventories.keys())
else:
owners.append(owner_inventory_id)
for owner: StringName in owners:
if not _inventories.has(owner):
continue
for stack_id: StringName in _sorted_names((_inventories[owner] as Dictionary).keys()):
result.append(((_inventories[owner] as Dictionary)[stack_id] as Dictionary).duplicate(true))
return result
func get_reservation(reservation_id: StringName) -> InventoryReservation:
return _reservations.get(reservation_id) as InventoryReservation
func get_reservations(active_only := false) -> Array[InventoryReservation]:
var result: Array[InventoryReservation] = []
for reservation_id: StringName in _sorted_names(_reservations.keys()):
var reservation: InventoryReservation = _reservations[reservation_id]
if not active_only or reservation.is_active():
result.append(reservation)
return result
func audit_and_repair_reservations(absolute_minute: int = -1) -> Dictionary:
var minute := current_minute if absolute_minute < 0 else absolute_minute
var report := {
&"ok": true,
&"scanned_reservations": _reservations.size(),
&"orphan_count": 0,
&"capacity_violation_count": 0,
&"released_reservation_ids": [],
&"corrected_stacks": [],
}
var reservations_by_stack: Dictionary = {}
for reservation_id: StringName in _sorted_names(_reservations.keys()):
var reservation: InventoryReservation = _reservations[reservation_id]
if not reservation.is_active():
continue
var location := _find_stack(reservation.stack_id)
var orphan: bool = (
not bool(location[&"found"])
or location[&"owner_inventory_id"] != reservation.owner_inventory_id
or (bool(location[&"found"]) and location[&"stack"][&"item_id"] != reservation.item_id)
or reservation.quantity <= 0
)
if orphan:
reservation.mark_released(minute)
report[&"orphan_count"] = int(report[&"orphan_count"]) + 1
report[&"released_reservation_ids"].append(reservation_id)
continue
if not reservations_by_stack.has(reservation.stack_id):
reservations_by_stack[reservation.stack_id] = []
(reservations_by_stack[reservation.stack_id] as Array).append(reservation_id)
for stack_id: StringName in _sorted_names(reservations_by_stack.keys()):
var location := _find_stack(stack_id)
if not bool(location[&"found"]):
continue
var stack: Dictionary = location[&"stack"]
var reservation_ids: Array = reservations_by_stack[stack_id]
var expected := 0
for reservation_id: StringName in reservation_ids:
expected += (_reservations[reservation_id] as InventoryReservation).quantity
if expected > int(stack[&"quantity"]):
reservation_ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) > String(b))
for reservation_id: StringName in reservation_ids:
if expected <= int(stack[&"quantity"]):
break
var reservation: InventoryReservation = _reservations[reservation_id]
expected -= reservation.quantity
reservation.mark_released(minute)
report[&"capacity_violation_count"] = int(report[&"capacity_violation_count"]) + 1
report[&"released_reservation_ids"].append(reservation_id)
var expected_by_stack: Dictionary = {}
for reservation_id: StringName in _sorted_names(_reservations.keys()):
var reservation: InventoryReservation = _reservations[reservation_id]
if reservation.is_active():
expected_by_stack[reservation.stack_id] = int(expected_by_stack.get(reservation.stack_id, 0)) + reservation.quantity
for owner_inventory_id: StringName in _sorted_names(_inventories.keys()):
for stack_id: StringName in _sorted_names((_inventories[owner_inventory_id] as Dictionary).keys()):
var stack: Dictionary = (_inventories[owner_inventory_id] as Dictionary)[stack_id]
var expected := int(expected_by_stack.get(stack_id, 0))
if int(stack[&"reserved_quantity"]) == expected:
continue
report[&"corrected_stacks"].append({
&"stack_id": stack_id,
&"before": int(stack[&"reserved_quantity"]),
&"after": expected,
})
stack[&"reserved_quantity"] = expected
_set_stack(owner_inventory_id, stack)
last_repair_report = report.duplicate(true)
return report
func repair_orphan_reservations(absolute_minute: int = -1) -> Dictionary:
return audit_and_repair_reservations(absolute_minute)
func validate_invariants() -> Dictionary:
var error := _validate_state(_inventories, _reservations)
return {
&"ok": error.is_empty(),
&"error": error,
}
func create_snapshot() -> Dictionary:
var inventories: Array[Dictionary] = []
for owner_inventory_id: StringName in _sorted_names(_inventories.keys()):
var stacks: Array[Dictionary] = []
for stack_id: StringName in _sorted_names((_inventories[owner_inventory_id] as Dictionary).keys()):
var stack: Dictionary = (_inventories[owner_inventory_id] as Dictionary)[stack_id]
stacks.append(_serialize_stack(stack))
inventories.append({
&"owner_inventory_id": String(owner_inventory_id),
&"stacks": stacks,
})
var reservations: Array[Dictionary] = []
for reservation_id: StringName in _sorted_names(_reservations.keys()):
reservations.append((_reservations[reservation_id] as InventoryReservation).create_snapshot())
var idempotency_results: Array[Dictionary] = []
var operation_keys := _idempotency_results.keys()
operation_keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b))
for operation_key: Variant in operation_keys:
var record: Dictionary = _idempotency_results[operation_key]
var serialized_record := {
&"operation_id": String(operation_key),
&"action": String(record[&"action"]),
&"input_fingerprint": String(record[&"input_fingerprint"]),
&"result": (record[&"result"] as Dictionary).duplicate(true),
}
if bool(record.get(&"legacy_unbound", false)):
serialized_record[&"legacy_unbound"] = true
idempotency_results.append(serialized_record)
return {
&"version": SNAPSHOT_VERSION,
&"current_minute": current_minute,
&"next_stack_sequence": _next_stack_sequence,
&"next_reservation_sequence": _next_reservation_sequence,
&"known_stack_ids": _string_array(_sorted_names(_known_stack_ids.keys())),
&"known_reservation_ids": _string_array(_sorted_names(_known_reservation_ids.keys())),
&"inventories": inventories,
&"reservations": reservations,
&"idempotency_results": idempotency_results,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
last_restore_error = ""
var version := int(snapshot.get(&"version", -1))
if version != SNAPSHOT_VERSION and version != LEGACY_SNAPSHOT_VERSION:
last_restore_error = "Unsupported inventory snapshot version."
return false
var next_current_minute := int(snapshot.get(&"current_minute", -1))
if next_current_minute < 0:
last_restore_error = "Inventory snapshot current minute is invalid."
return false
if not snapshot.get(&"inventories", null) is Array or not snapshot.get(&"reservations", null) is Array:
last_restore_error = "Inventory snapshot collections are malformed."
return false
var next_inventories: Dictionary = {}
var live_stack_ids: Dictionary = {}
for raw_inventory: Variant in snapshot[&"inventories"]:
if not raw_inventory is Dictionary:
last_restore_error = "Inventory entry must be a Dictionary."
return false
var owner_inventory_id := StringName(raw_inventory.get(&"owner_inventory_id", ""))
var raw_stacks: Variant = raw_inventory.get(&"stacks", null)
if owner_inventory_id == &"" or not raw_stacks is Array or next_inventories.has(owner_inventory_id):
last_restore_error = "Inventory owner entry is invalid or duplicated."
return false
var stacks: Dictionary = {}
for raw_stack: Variant in raw_stacks:
var stack := _parse_stack(raw_stack, owner_inventory_id)
if stack.is_empty():
last_restore_error = "Inventory stack entry is invalid."
return false
var stack_id: StringName = stack[&"stack_id"]
if live_stack_ids.has(stack_id):
last_restore_error = "Inventory stack id is duplicated."
return false
live_stack_ids[stack_id] = true
stacks[stack_id] = stack
next_inventories[owner_inventory_id] = stacks
var next_reservations: Dictionary = {}
var live_reservation_ids: Dictionary = {}
for raw_reservation: Variant in snapshot[&"reservations"]:
if not raw_reservation is Dictionary:
last_restore_error = "Reservation entry must be a Dictionary."
return false
if not _reservation_timestamps_are_valid(raw_reservation):
last_restore_error = "Reservation timestamps are invalid."
return false
var reservation := InventoryReservationType.from_snapshot(raw_reservation)
if reservation == null or next_reservations.has(reservation.reservation_id):
last_restore_error = "Reservation entry is invalid or duplicated."
return false
next_reservations[reservation.reservation_id] = reservation
live_reservation_ids[reservation.reservation_id] = true
var next_known_stack_ids: Dictionary = {}
var next_known_reservation_ids: Dictionary = {}
if version == SNAPSHOT_VERSION:
var raw_known_stack_ids: Variant = snapshot.get(&"known_stack_ids", null)
var raw_known_reservation_ids: Variant = snapshot.get(&"known_reservation_ids", null)
if not raw_known_stack_ids is Array or not raw_known_reservation_ids is Array:
last_restore_error = "Inventory known-id collections are malformed."
return false
for raw_id: Variant in raw_known_stack_ids:
var known_id := StringName(raw_id)
if known_id == &"" or next_known_stack_ids.has(known_id):
last_restore_error = "Known stack id is invalid or duplicated."
return false
next_known_stack_ids[known_id] = true
for raw_id: Variant in raw_known_reservation_ids:
var known_id := StringName(raw_id)
if known_id == &"" or next_known_reservation_ids.has(known_id):
last_restore_error = "Known reservation id is invalid or duplicated."
return false
next_known_reservation_ids[known_id] = true
else:
next_known_stack_ids = live_stack_ids.duplicate(true)
next_known_reservation_ids = live_reservation_ids.duplicate(true)
for stack_id: Variant in live_stack_ids:
if not next_known_stack_ids.has(stack_id):
last_restore_error = "Live stack id is missing from known-id history."
return false
for reservation_id: Variant in live_reservation_ids:
if not next_known_reservation_ids.has(reservation_id):
last_restore_error = "Live reservation id is missing from known-id history."
return false
var invariant_error := _validate_state(next_inventories, next_reservations)
if not invariant_error.is_empty():
last_restore_error = invariant_error
return false
var next_idempotency: Dictionary = {}
var raw_results: Variant = snapshot.get(&"idempotency_results", [])
if not raw_results is Array:
last_restore_error = "Idempotency result collection is malformed."
return false
for raw_result: Variant in raw_results:
if not raw_result is Dictionary or not raw_result.get(&"result", null) is Dictionary:
last_restore_error = "Idempotency result entry is malformed."
return false
var operation_id := StringName(raw_result.get(&"operation_id", ""))
var action := StringName(raw_result.get(&"action", ""))
var input_fingerprint := String(raw_result.get(&"input_fingerprint", ""))
var legacy_unbound := bool(raw_result.get(&"legacy_unbound", false))
if version == LEGACY_SNAPSHOT_VERSION:
var legacy_key := String(raw_result.get(&"operation_key", ""))
var separator := legacy_key.find("|")
if separator <= 0 or separator >= legacy_key.length() - 1:
last_restore_error = "Legacy idempotency result key is invalid."
return false
action = StringName(legacy_key.substr(0, separator))
operation_id = StringName(legacy_key.substr(separator + 1))
input_fingerprint = ""
legacy_unbound = true
if (
operation_id == &""
or action == &""
or (input_fingerprint.is_empty() and not legacy_unbound)
):
last_restore_error = "Idempotency result key or binding is invalid or duplicated."
return false
if next_idempotency.has(operation_id):
if version != LEGACY_SNAPSHOT_VERSION:
last_restore_error = "Idempotency operation id is duplicated."
return false
# V1 namespaced ids by action, so two otherwise-valid legacy records can
# share one operation id. Preserve both payloads but fail closed on any
# replay until the caller issues a fresh operation id under V2 semantics.
var prior_record: Dictionary = next_idempotency[operation_id]
var legacy_results: Array[Dictionary] = []
if prior_record[&"result"].has(&"legacy_ambiguous_results"):
legacy_results.assign(prior_record[&"result"][&"legacy_ambiguous_results"])
else:
legacy_results.append({
&"action": String(prior_record[&"action"]),
&"result": (prior_record[&"result"] as Dictionary).duplicate(true),
})
legacy_results.append({
&"action": String(action),
&"result": (raw_result[&"result"] as Dictionary).duplicate(true),
})
next_idempotency[operation_id] = {
&"action": &"__legacy_ambiguous__",
&"input_fingerprint": "",
&"legacy_unbound": true,
&"result": {&"legacy_ambiguous_results": legacy_results},
}
continue
next_idempotency[operation_id] = {
&"action": action,
&"input_fingerprint": input_fingerprint,
&"legacy_unbound": legacy_unbound,
&"result": (raw_result[&"result"] as Dictionary).duplicate(true),
}
var next_stack_sequence := int(snapshot.get(&"next_stack_sequence", 0))
var next_reservation_sequence := int(snapshot.get(&"next_reservation_sequence", 0))
if next_stack_sequence < 1 or next_reservation_sequence < 1:
last_restore_error = "Inventory snapshot sequence is invalid."
return false
if version == LEGACY_SNAPSHOT_VERSION:
# V1 only persisted the next counters. Reconstruct the consumed canonical
# ranges once so the V2 migration cannot later reuse a deleted legacy id.
for sequence: int in range(1, next_stack_sequence):
next_known_stack_ids[StringName("stack_%08d" % sequence)] = true
for sequence: int in range(1, next_reservation_sequence):
next_known_reservation_ids[StringName("reservation_%08d" % sequence)] = true
if (
next_stack_sequence <= _max_canonical_sequence(next_known_stack_ids.keys(), "stack_")
or next_reservation_sequence <= _max_canonical_sequence(next_known_reservation_ids.keys(), "reservation_")
):
last_restore_error = "Inventory snapshot sequence would reuse a known canonical id."
return false
_inventories = next_inventories
_rebuild_stack_location_index()
_reservations = next_reservations
_known_stack_ids = next_known_stack_ids
_known_reservation_ids = next_known_reservation_ids
_idempotency_results = next_idempotency
_next_stack_sequence = next_stack_sequence
_next_reservation_sequence = next_reservation_sequence
current_minute = next_current_minute
last_restore_error = ""
return true
func _validate_active_reservation(reservation: InventoryReservation, absolute_minute: int) -> Dictionary:
if reservation.is_expired(absolute_minute):
return _failure(&"RESERVATION_EXPIRED", "Reservation has expired.")
var location := _find_stack(reservation.stack_id)
if not bool(location[&"found"]):
return _failure(&"ORPHAN_RESERVATION", "Reserved stack is missing.")
var stack: Dictionary = location[&"stack"]
if location[&"owner_inventory_id"] != reservation.owner_inventory_id:
return _failure(&"ORPHAN_RESERVATION", "Reserved stack belongs to another inventory.")
if _stack_is_expired(stack, absolute_minute):
return _failure(&"STACK_EXPIRED", "Reserved stock has expired.")
if int(stack[&"quantity"]) < reservation.quantity or int(stack[&"reserved_quantity"]) < reservation.quantity:
return _failure(&"INVARIANT_VIOLATION", "Reserved stack no longer contains the reserved quantity.")
return {&"ok": true, &"location": location}
func _available_stacks(owner_inventory_id: StringName, item_id: StringName, absolute_minute: int) -> Array[Dictionary]:
var result: Array[Dictionary] = []
if not _inventories.has(owner_inventory_id):
return result
for stack: Dictionary in (_inventories[owner_inventory_id] as Dictionary).values():
if stack[&"item_id"] == item_id and not _stack_is_expired(stack, absolute_minute) and _available_in_stack(stack) > 0:
result.append(stack.duplicate(true))
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
var a_expiry := int(a[&"expires_at"])
var b_expiry := int(b[&"expires_at"])
var a_order := 9223372036854775807 if a_expiry < 0 else a_expiry
var b_order := 9223372036854775807 if b_expiry < 0 else b_expiry
if a_order != b_order:
return a_order < b_order
if int(a[&"quality"]) != int(b[&"quality"]):
return int(a[&"quality"]) > int(b[&"quality"])
return String(a[&"stack_id"]) < String(b[&"stack_id"])
)
return result
func _find_stack(stack_id: StringName) -> Dictionary:
if _stack_owner_by_id.has(stack_id):
var indexed_owner_id: StringName = _stack_owner_by_id[stack_id]
if _inventories.has(indexed_owner_id):
var indexed_stacks: Dictionary = _inventories[indexed_owner_id]
if indexed_stacks.has(stack_id):
return {
&"found": true,
&"owner_inventory_id": indexed_owner_id,
&"stack": (indexed_stacks[stack_id] as Dictionary).duplicate(true),
}
# A stale derived entry must never mask an externally corrupted state.
_stack_owner_by_id.erase(stack_id)
for owner_inventory_id: Variant in _inventories:
var stacks: Dictionary = _inventories[owner_inventory_id]
if stacks.has(stack_id):
_stack_owner_by_id[stack_id] = StringName(owner_inventory_id)
return {
&"found": true,
&"owner_inventory_id": StringName(owner_inventory_id),
&"stack": (stacks[stack_id] as Dictionary).duplicate(true),
}
return {&"found": false, &"owner_inventory_id": &"", &"stack": {}}
func _insert_stack(stack: Dictionary) -> void:
var owner_inventory_id: StringName = stack[&"owner_inventory_id"]
var stack_id: StringName = stack[&"stack_id"]
if not _inventories.has(owner_inventory_id):
_inventories[owner_inventory_id] = {}
var stacks: Dictionary = _inventories[owner_inventory_id]
stacks[stack_id] = stack.duplicate(true)
_inventories[owner_inventory_id] = stacks
_stack_owner_by_id[stack_id] = owner_inventory_id
_known_stack_ids[stack_id] = true
var canonical_sequence := _canonical_sequence(stack_id, "stack_")
if canonical_sequence >= _next_stack_sequence:
_next_stack_sequence = canonical_sequence + 1
func _set_stack(owner_inventory_id: StringName, stack: Dictionary) -> void:
if not _inventories.has(owner_inventory_id):
_inventories[owner_inventory_id] = {}
var stacks: Dictionary = _inventories[owner_inventory_id]
stacks[stack[&"stack_id"]] = stack.duplicate(true)
_inventories[owner_inventory_id] = stacks
_stack_owner_by_id[stack[&"stack_id"]] = owner_inventory_id
_known_stack_ids[stack[&"stack_id"]] = true
func _set_or_remove_stack(owner_inventory_id: StringName, stack: Dictionary) -> void:
if int(stack[&"quantity"]) > 0:
_set_stack(owner_inventory_id, stack)
return
if not _inventories.has(owner_inventory_id):
return
var stacks: Dictionary = _inventories[owner_inventory_id]
stacks.erase(stack[&"stack_id"])
_stack_owner_by_id.erase(stack[&"stack_id"])
if stacks.is_empty():
_inventories.erase(owner_inventory_id)
else:
_inventories[owner_inventory_id] = stacks
func _restore_standalone_capture(capture: Dictionary) -> void:
if _atomic_batch_depth == 0 and not capture.is_empty():
restore_snapshot(capture)
func _rebuild_stack_location_index() -> void:
_stack_owner_by_id.clear()
for owner_inventory_id: Variant in _inventories:
var stacks: Dictionary = _inventories[owner_inventory_id]
for stack_id: Variant in stacks:
_stack_owner_by_id[StringName(stack_id)] = StringName(owner_inventory_id)
func _next_stack_id() -> StringName:
while true:
var value := StringName("stack_%08d" % _next_stack_sequence)
_next_stack_sequence += 1
# Generated ids are fully represented by the maintained index. Falling
# back to an all-inventory diagnostic scan here made every destination
# insertion O(owner count) during large delivery waves.
if not _known_stack_ids.has(value):
_known_stack_ids[value] = true
return value
return &""
func _next_reservation_id() -> StringName:
while true:
var value := StringName("reservation_%08d" % _next_reservation_sequence)
_next_reservation_sequence += 1
if not _known_reservation_ids.has(value):
_known_reservation_ids[value] = true
return value
return &""
func _get_replay(
action: StringName,
operation_id: StringName,
input_fingerprint: String,
bind_legacy: bool = true
) -> Dictionary:
if operation_id == &"":
return {}
if not _idempotency_results.has(operation_id):
return {}
var record: Dictionary = _idempotency_results[operation_id]
if (
StringName(record[&"action"]) != action
or (
not bool(record.get(&"legacy_unbound", false))
and String(record[&"input_fingerprint"]) != input_fingerprint
)
):
return _failure(
&"IDEMPOTENCY_CONFLICT",
"Operation id is already bound to a different action or normalized input.",
{&"operation_id": operation_id}
)
if bool(record.get(&"legacy_unbound", false)) and bind_legacy:
# Version-1 snapshots did not retain request fingerprints. Preserve their
# first valid replay, then permanently bind that id to the observed input.
record[&"input_fingerprint"] = input_fingerprint
record[&"legacy_unbound"] = false
_idempotency_results[operation_id] = record
var result: Dictionary = (record[&"result"] as Dictionary).duplicate(true)
result[&"replay"] = true
return result
func _remember(
action: StringName,
operation_id: StringName,
result: Dictionary,
input_fingerprint: String
) -> Dictionary:
if operation_id != &"":
if _idempotency_results.has(operation_id):
return _get_replay(action, operation_id, input_fingerprint)
_idempotency_results[operation_id] = {
&"action": action,
&"input_fingerprint": input_fingerprint,
&"legacy_unbound": false,
&"result": result.duplicate(true),
}
return result
func _remember_failure(
action: StringName,
operation_id: StringName,
input_fingerprint: String,
code: StringName,
message: String,
details: Dictionary = {}
) -> Dictionary:
return _remember(action, operation_id, _failure(code, message, details), input_fingerprint)
func _preflight_available_batch(transfers: Array[Dictionary], minute: int) -> Dictionary:
var seen_operation_ids: Dictionary = {}
for index: int in range(transfers.size()):
var entry: Dictionary = transfers[index]
var operation_id := StringName(entry.get(&"operation_id", entry.get("operation_id", "")))
var fingerprint := _transfer_fingerprint(
StringName(entry.get(&"source_inventory_id", entry.get("source_inventory_id", ""))),
StringName(entry.get(&"destination_inventory_id", entry.get("destination_inventory_id", ""))),
StringName(entry.get(&"item_id", entry.get("item_id", ""))),
int(entry.get(&"quantity", entry.get("quantity", 0))),
minute
)
var validation := _preflight_operation_id(
&"transfer", operation_id, fingerprint, seen_operation_ids, index
)
if not bool(validation[&"ok"]):
return validation
return {&"ok": true}
func _preflight_reserved_batch(transfers: Array[Dictionary], minute: int) -> Dictionary:
var seen_operation_ids: Dictionary = {}
for index: int in range(transfers.size()):
var entry: Dictionary = transfers[index]
var reservation_ids: Array[StringName] = []
var raw_ids: Variant = entry.get(&"reservation_ids", entry.get("reservation_ids", []))
if raw_ids is Array:
for raw_id: Variant in raw_ids:
reservation_ids.append(StringName(raw_id))
var operation_id := StringName(entry.get(&"operation_id", entry.get("operation_id", "")))
var fingerprint := _reserved_transfer_fingerprint(
_sorted_names(reservation_ids),
StringName(entry.get(&"destination_inventory_id", entry.get("destination_inventory_id", ""))),
minute
)
var validation := _preflight_operation_id(
&"transfer_reserved", operation_id, fingerprint, seen_operation_ids, index
)
if not bool(validation[&"ok"]):
return validation
return {&"ok": true}
func _preflight_operation_id(
action: StringName,
operation_id: StringName,
input_fingerprint: String,
seen_operation_ids: Dictionary,
index: int
) -> Dictionary:
if operation_id == &"":
return {&"ok": true}
if seen_operation_ids.has(operation_id):
return _failure(
&"IDEMPOTENCY_CONFLICT",
"A non-empty operation id may occur only once in an atomic batch.",
{&"operation_id": operation_id, &"failed_index": index}
)
seen_operation_ids[operation_id] = true
var existing := _get_replay(action, operation_id, input_fingerprint, false)
if (
not existing.is_empty()
and not bool(existing.get(&"ok", false))
and existing.get(&"code", &"") == &"IDEMPOTENCY_CONFLICT"
):
existing[&"failed_index"] = index
return existing
return {&"ok": true}
static func _transfer_fingerprint(
source_inventory_id: StringName,
destination_inventory_id: StringName,
item_id: StringName,
quantity: int,
minute: int
) -> String:
return _canonical_fingerprint([
source_inventory_id,
destination_inventory_id,
item_id,
quantity,
minute,
])
static func _reserved_transfer_fingerprint(
reservation_ids: Array[StringName],
destination_inventory_id: StringName,
minute: int
) -> String:
return _canonical_fingerprint([reservation_ids, destination_inventory_id, minute])
static func _canonical_fingerprint(values: Array) -> String:
var encoded := ""
for value: Variant in values:
var token := _canonical_fingerprint_token(value)
encoded += "%d:%s" % [token.length(), token]
return encoded
static func _canonical_fingerprint_token(value: Variant) -> String:
if value is Array:
var items := ""
for item: Variant in value:
var token := _canonical_fingerprint_token(item)
items += "%d:%s" % [token.length(), token]
return "a%d:%s" % [value.size(), items]
if value is int:
return "i:%d" % value
var text := String(value)
return "s%d:%s" % [text.length(), text]
static func _success(details: Dictionary = {}) -> Dictionary:
var result := {&"ok": true, &"code": &"OK", &"replay": false}
result.merge(details, true)
return result
static func _failure(code: StringName, message: String, details: Dictionary = {}) -> Dictionary:
var result := {
&"ok": false,
&"code": code,
&"message": message,
&"replay": false,
}
result.merge(details, true)
return result
static func _make_stack(
stack_id: StringName,
owner_inventory_id: StringName,
item_id: StringName,
quantity: int,
quality: int,
expires_at: int,
created_at: int,
unit_weight: int
) -> Dictionary:
return {
&"stack_id": stack_id,
&"owner_inventory_id": owner_inventory_id,
&"item_id": item_id,
&"quality": quality,
&"quantity": quantity,
&"reserved_quantity": 0,
&"created_at": maxi(created_at, 0),
&"expires_at": expires_at,
&"unit_weight": unit_weight,
}
static func _serialize_stack(stack: Dictionary) -> Dictionary:
var serialized := stack.duplicate(true)
serialized[&"stack_id"] = String(stack[&"stack_id"])
serialized[&"owner_inventory_id"] = String(stack[&"owner_inventory_id"])
serialized[&"item_id"] = String(stack[&"item_id"])
return serialized
static func _parse_stack(raw_stack: Variant, expected_owner_id: StringName) -> Dictionary:
if not raw_stack is Dictionary:
return {}
var stack_id := StringName(raw_stack.get(&"stack_id", ""))
var owner_inventory_id := StringName(raw_stack.get(&"owner_inventory_id", ""))
var item_id := StringName(raw_stack.get(&"item_id", ""))
var quantity := int(raw_stack.get(&"quantity", -1))
var reserved_quantity := int(raw_stack.get(&"reserved_quantity", -1))
var unit_weight := int(raw_stack.get(&"unit_weight", 0))
var created_at := int(raw_stack.get(&"created_at", -1))
var expires_at := int(raw_stack.get(&"expires_at", -2))
if (
stack_id == &""
or owner_inventory_id == &""
or owner_inventory_id != expected_owner_id
or item_id == &""
or quantity <= 0
or reserved_quantity < 0
or reserved_quantity > quantity
or unit_weight <= 0
or created_at < 0
or expires_at < -1
):
return {}
return {
&"stack_id": stack_id,
&"owner_inventory_id": owner_inventory_id,
&"item_id": item_id,
&"quality": int(raw_stack.get(&"quality", DEFAULT_QUALITY)),
&"quantity": quantity,
&"reserved_quantity": reserved_quantity,
&"created_at": created_at,
&"expires_at": expires_at,
&"unit_weight": unit_weight,
}
static func _validate_state(inventories: Dictionary, reservations: Dictionary) -> String:
var stack_locations: Dictionary = {}
for owner_inventory_id: Variant in inventories:
var stacks: Dictionary = inventories[owner_inventory_id]
for stack_id: Variant in stacks:
var stack: Dictionary = stacks[stack_id]
if stack_locations.has(stack_id):
return "Stack id is duplicated across inventories."
if int(stack[&"quantity"]) <= 0:
return "Empty stack remained in inventory state."
if int(stack[&"reserved_quantity"]) < 0 or int(stack[&"reserved_quantity"]) > int(stack[&"quantity"]):
return "Stack available quantity invariant is violated."
stack_locations[stack_id] = {
&"owner_inventory_id": StringName(owner_inventory_id),
&"stack": stack,
}
var expected_reserved: Dictionary = {}
for reservation_id: Variant in reservations:
var reservation: InventoryReservation = reservations[reservation_id]
if not reservation.is_active():
continue
if not stack_locations.has(reservation.stack_id):
return "Active reservation refers to a missing stack."
var location: Dictionary = stack_locations[reservation.stack_id]
if location[&"owner_inventory_id"] != reservation.owner_inventory_id:
return "Active reservation owner does not match its stack."
var stack: Dictionary = location[&"stack"]
if stack[&"item_id"] != reservation.item_id:
return "Active reservation item does not match its stack."
expected_reserved[reservation.stack_id] = int(expected_reserved.get(reservation.stack_id, 0)) + reservation.quantity
for stack_id: Variant in stack_locations:
var stack: Dictionary = stack_locations[stack_id][&"stack"]
if int(stack[&"reserved_quantity"]) != int(expected_reserved.get(stack_id, 0)):
return "Stack reserved quantity does not match active reservations."
return ""
static func _available_in_stack(stack: Dictionary) -> int:
return int(stack[&"quantity"]) - int(stack[&"reserved_quantity"])
static func _stack_is_expired(stack: Dictionary, absolute_minute: int) -> bool:
var expires_at := int(stack[&"expires_at"])
return expires_at >= 0 and absolute_minute >= expires_at
static func _unique_sorted_names(values: Array[StringName]) -> Array[StringName]:
var seen: Dictionary = {}
var result: Array[StringName] = []
for value: StringName in values:
if value == &"" or seen.has(value):
continue
seen[value] = true
result.append(value)
result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return result
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
static func _string_array(values: Array[StringName]) -> Array[String]:
var result: Array[String] = []
for value: StringName in values:
result.append(String(value))
return result
static func _max_canonical_sequence(values: Variant, prefix: String) -> int:
var maximum := 0
for value: Variant in values:
var sequence := _canonical_sequence(value, prefix)
if sequence > maximum:
maximum = sequence
return maximum
static func _canonical_sequence(value: Variant, prefix: String) -> int:
var text := String(value)
if not text.begins_with(prefix):
return 0
var suffix := text.substr(prefix.length())
if suffix.is_empty() or not suffix.is_valid_int():
return 0
return maxi(int(suffix), 0)
static func _reservation_timestamps_are_valid(snapshot: Dictionary) -> bool:
var created_at := int(snapshot.get(&"created_at", -1))
var expires_at := int(snapshot.get(&"expires_at", -2))
var terminal_at := int(snapshot.get(&"terminal_at", -2))
var state_name := String(snapshot.get(&"state", ""))
if created_at < 0 or expires_at < -1 or terminal_at < -1:
return false
if state_name == "ACTIVE":
return terminal_at == -1
return state_name in ["CONSUMED", "RELEASED", "EXPIRED"] and terminal_at >= 0
static func _sum_lines(lines: Array[Dictionary]) -> int:
var total := 0
for line: Dictionary in lines:
total += int(line.get(&"quantity", 0))
return total