Add supply auto-purchase and preserve save recovery state

This commit is contained in:
Takeshi
2026-10-02 07:35:03 +09:00
parent 285528b7a3
commit 39f6068461
44 changed files with 2625 additions and 61 deletions
@@ -0,0 +1,276 @@
class_name SupplyAutoPurchasePlans
extends RefCounted
## Owned player intent and the last actual attempt. No plan is inferred on load.
## Current catalogue prices/storage are reused; target stock is player input.
const Catalog := preload("res://scripts/supply/supply_catalog.gd")
const SNAPSHOT_VERSION := 1
const RECEIPT_PREFIX := "supply_purchase.auto.v1:"
const MAX_SIGNED_INT_TEXT := "9223372036854775807"
const RESULT_PURCHASED := &"PURCHASED"
const FAILED_RESULTS: Array[StringName] = [
&"INVENTORY_ERROR", &"PAYMENT_FAILED", &"INSUFFICIENT_FUNDS", &"STORAGE_FULL",
&"FACILITY_CLOSED", &"INVALID_QUANTITY", &"ITEM_NOT_PURCHASABLE", &"NOT_WAREHOUSE",
&"REPLAY_MISMATCH",
]
var last_error := ""
var _plans: Dictionary = {}
func get_plan(warehouse_id: StringName, item_id: StringName) -> Dictionary:
if _plans.has(warehouse_id) and (_plans[warehouse_id] as Dictionary).has(item_id):
return (_plans[warehouse_id][item_id] as Dictionary).duplicate(true)
return _empty_plan(warehouse_id, item_id)
func get_plans() -> Array[Dictionary]:
var rows: Array[Dictionary] = []
for warehouse_id: StringName in _sorted_names(_plans):
for item_id: StringName in _sorted_names(_plans[warehouse_id]):
rows.append(get_plan(warehouse_id, item_id))
return rows
func set_plan(warehouse_id: StringName, item_id: StringName, enabled: bool,
target_stock: int, facilities: FacilityService) -> Dictionary:
var validation := validate_intent(warehouse_id, item_id, enabled, target_stock, facilities)
if not bool(validation[&"ok"]):
return validation
var plan := get_plan(warehouse_id, item_id)
plan[&"enabled"] = enabled
plan[&"target_stock"] = target_stock
if not _plans.has(warehouse_id):
_plans[warehouse_id] = {}
_plans[warehouse_id][item_id] = plan
last_error = ""
return {&"ok": true, &"plan": plan.duplicate(true)}
func record_attempt(warehouse_id: StringName, item_id: StringName, minute: int,
result: CommandResult, economy: EconomyService) -> bool:
if not _plans.has(warehouse_id) or not (_plans[warehouse_id] as Dictionary).has(item_id):
return false
var plan: Dictionary = _plans[warehouse_id][item_id]
if minute < 0 or minute <= int(plan[&"last_attempt_minute"]):
return false
var reason: StringName = RESULT_PURCHASED if result.is_success() \
else StringName(result.details.get(&"reason", &"PAYMENT_FAILED"))
if reason != RESULT_PURCHASED and reason not in FAILED_RESULTS:
return false
var transaction_id := StringName(result.details.get(&"transaction_id", &"")) if result.is_success() else &""
var quantity: int = int(result.details.get(&"quantity", 0)) if result.is_success() else 0
var unit_price: int = int(result.details.get(&"unit_price", 0)) if result.is_success() else 0
var total_cost: int = int(result.details.get(&"total_cost", 0)) if result.is_success() else 0
if result.is_success() and (not valid_charge(quantity, unit_price, total_cost) or not valid_receipt(economy, warehouse_id, item_id, minute, transaction_id, total_cost, quantity, unit_price)):
return false
plan[&"last_attempt_minute"] = minute
plan[&"last_result"] = reason
plan[&"last_transaction_id"] = String(transaction_id)
plan[&"last_quantity"] = quantity
plan[&"last_unit_price"] = unit_price
plan[&"last_total_cost"] = total_cost
return true
func remove_warehouse(warehouse_id: StringName) -> void:
_plans.erase(warehouse_id)
func create_snapshot() -> Dictionary:
return {&"version": SNAPSHOT_VERSION, &"plans": get_plans()}
func restore_snapshot(snapshot: Dictionary, facilities: FacilityService,
economy: EconomyService, minute: int) -> bool:
last_error = ""
if not _keys_match(snapshot, [&"version", &"plans"]) \
or typeof(snapshot[&"version"]) != TYPE_INT or snapshot[&"version"] != SNAPSHOT_VERSION \
or not snapshot[&"plans"] is Array or facilities == null or economy == null or minute < 0:
return _restore_failure("Invalid automatic purchase snapshot.")
# At most one plan per real warehouse/catalog item; input cannot broaden it.
var maximum_rows := facilities.get_instances().size() * Catalog.get_purchase_items().size()
if (snapshot[&"plans"] as Array).size() > maximum_rows:
return _restore_failure("Too many automatic purchase plans.")
var candidate: Dictionary = {}
for value: Variant in snapshot[&"plans"]:
if not value is Dictionary:
return _restore_failure("Automatic purchase row is not a Dictionary.")
var row: Dictionary = value
if not _keys_match(row, [&"warehouse_id", &"item_id", &"enabled", &"target_stock",
&"last_attempt_minute", &"last_result", &"last_transaction_id", &"last_quantity", &"last_unit_price", &"last_total_cost"]) \
or typeof(row[&"warehouse_id"]) != TYPE_STRING or typeof(row[&"item_id"]) != TYPE_STRING \
or not _text(row[&"warehouse_id"]) or not _text(row[&"item_id"]) \
or typeof(row[&"enabled"]) != TYPE_BOOL or typeof(row[&"target_stock"]) != TYPE_INT \
or typeof(row[&"last_attempt_minute"]) != TYPE_INT \
or typeof(row[&"last_result"]) != TYPE_STRING_NAME \
or typeof(row[&"last_transaction_id"]) != TYPE_STRING \
or typeof(row[&"last_quantity"]) != TYPE_INT or typeof(row[&"last_unit_price"]) != TYPE_INT or typeof(row[&"last_total_cost"]) != TYPE_INT:
return _restore_failure("Invalid automatic purchase row fields/types.")
var warehouse_id := StringName(row[&"warehouse_id"])
var item_id := StringName(row[&"item_id"])
if not bool(validate_intent(warehouse_id, item_id, row[&"enabled"], row[&"target_stock"], facilities)[&"ok"]):
return _restore_failure("Automatic purchase plan does not match its warehouse/catalog.")
if candidate.has(warehouse_id) and (candidate[warehouse_id] as Dictionary).has(item_id):
return _restore_failure("Duplicate automatic purchase plan.")
var attempted := int(row[&"last_attempt_minute"])
var reason: StringName = row[&"last_result"]
var transaction_id := StringName(row[&"last_transaction_id"])
var quantity: int = row[&"last_quantity"]
var unit_price: int = row[&"last_unit_price"]
var total_cost: int = row[&"last_total_cost"]
if reason == RESULT_PURCHASED and not valid_charge(quantity, unit_price, total_cost):
return _restore_failure("Saved automatic purchase quote is invalid.")
if reason != RESULT_PURCHASED and (quantity != 0 or unit_price != 0 or total_cost != 0):
return _restore_failure("Unpurchased automatic intent retained a charge.")
if attempted < -1 or attempted > minute \
or (attempted == -1 and (not reason.is_empty() or not transaction_id.is_empty())) \
or (attempted >= 0 and reason != RESULT_PURCHASED and reason not in FAILED_RESULTS):
return _restore_failure("Invalid automatic purchase attempt state.")
if attempted >= 0:
var paid_id := economy.get_transaction_id_for_idempotency_key(order_key(warehouse_id, item_id, attempted))
if reason == RESULT_PURCHASED:
if not valid_receipt(economy, warehouse_id, item_id, attempted, transaction_id, total_cost, quantity, unit_price):
return _restore_failure("Automatic purchase receipt differs from its unique ledger transaction.")
elif not transaction_id.is_empty() or (not paid_id.is_empty() and (reason != &"REPLAY_MISMATCH" or valid_receipt(economy, warehouse_id, item_id, attempted, paid_id))):
return _restore_failure("Failed automatic purchase retained its own payment.")
if not candidate.has(warehouse_id):
candidate[warehouse_id] = {}
candidate[warehouse_id][item_id] = row.duplicate(true)
_plans = candidate
return true
static func receipt_memo(item_id: StringName, quantity: int, unit_price: int, total_cost: int) -> StringName:
if String(item_id).strip_edges().is_empty() or not valid_charge(quantity, unit_price, total_cost):
return &""
# Character length, not delimiter escaping, keeps arbitrary item punctuation
# unambiguous. Only automatic payments use this immutable historical quote.
return StringName(RECEIPT_PREFIX + "%d:%s:%d:%d:%d" % [
String(item_id).length(), item_id, quantity, unit_price, total_cost])
static func decode_receipt_memo(memo: StringName) -> Dictionary:
var text := String(memo)
if not text.begins_with(RECEIPT_PREFIX): return {}
var body := text.substr(RECEIPT_PREFIX.length())
var delimiter := body.find(":")
if delimiter <= 0: return {}
var item_length := _positive_decimal(body.substr(0, delimiter))
var remainder := body.substr(delimiter + 1)
if item_length <= 0 or item_length >= remainder.length() or remainder.substr(item_length, 1) != ":":
return {}
var item_text := remainder.substr(0, item_length)
if item_text.strip_edges().is_empty(): return {}
var amounts := remainder.substr(item_length + 1).split(":", true)
if amounts.size() != 3: return {}
var quantity := _positive_decimal(amounts[0])
var unit_price := _positive_decimal(amounts[1])
var total_cost := _positive_decimal(amounts[2])
if not valid_charge(quantity, unit_price, total_cost) \
or receipt_memo(StringName(item_text), quantity, unit_price, total_cost) != memo:
return {}
return {&"item_id": item_text, &"quantity": quantity, &"unit_price": unit_price, &"total_cost": total_cost}
static func receipt_from_ledger(economy: EconomyService, warehouse_id: StringName,
item_id: StringName, minute: int, transaction_id: StringName) -> Dictionary:
if economy == null or transaction_id.is_empty() \
or economy.get_transaction_id_for_idempotency_key(order_key(warehouse_id, item_id, minute)) != transaction_id:
return {}
var entries: Array = economy.get_transaction_entries(transaction_id)
if entries.size() != 1: return {}
var entry: LedgerEntry = entries[0]
var receipt := decode_receipt_memo(entry.memo_key)
if receipt.is_empty() or receipt[&"item_id"] != String(item_id) \
or entry.absolute_minute != minute or entry.source_entity_id != warehouse_id \
or entry.category != &"OPERATING_EXPENSE" or not entry.related_transaction_id.is_empty() \
or entry.credit_account != &"PLAYER_CASH" or entry.debit_account != &"OPERATING_EXPENSE" \
or entry.amount != receipt[&"total_cost"]:
return {}
return receipt
static func valid_receipt(economy: EconomyService, warehouse_id: StringName,
item_id: StringName, minute: int, transaction_id: StringName, total_cost: int = -1,
quantity: int = -1, unit_price: int = -1) -> bool:
var receipt := receipt_from_ledger(economy, warehouse_id, item_id, minute, transaction_id)
return not receipt.is_empty() and (total_cost < 0 or receipt[&"total_cost"] == total_cost) \
and (quantity < 0 or receipt[&"quantity"] == quantity) \
and (unit_price < 0 or receipt[&"unit_price"] == unit_price)
static func _positive_decimal(text: String) -> int:
# Validate textual range before to_int: no float conversion or saturating
# overflow can reinterpret a corrupt receipt as another positive amount.
if text.is_empty() or text.length() > MAX_SIGNED_INT_TEXT.length() or text.substr(0, 1) == "0":
return 0
for index: int in text.length():
var code := text.unicode_at(index)
if code < 48 or code > 57: return 0
if text.length() == MAX_SIGNED_INT_TEXT.length() and text > MAX_SIGNED_INT_TEXT: return 0
return text.to_int()
static func valid_charge(quantity: int, unit_price: int, total_cost: int) -> bool:
# Existing purchase input ceiling; division avoids multiplication overflow.
@warning_ignore("integer_division")
return quantity > 0 and quantity <= 10000 and unit_price > 0 and total_cost > 0 and total_cost / quantity == unit_price and total_cost % quantity == 0
static func validate_intent(warehouse_id: StringName, item_id: StringName, enabled: bool,
target_stock: int, facilities: FacilityService) -> Dictionary:
var facility: FacilityInstance = facilities.get_instance(warehouse_id) if facilities != null else null
if facility == null or facility.definition_id != &"warehouse":
return {&"ok": false, &"reason": &"NOT_WAREHOUSE"}
var item := Catalog.get_item(item_id)
if item.is_empty() or not bool(item.get(&"purchasable", false)):
return {&"ok": false, &"reason": &"ITEM_NOT_PURCHASABLE"}
var maximum := maximum_target(warehouse_id, item_id, facilities)
if target_stock < 0 or target_stock > maximum or (enabled and target_stock == 0):
return {&"ok": false, &"reason": &"INVALID_TARGET", &"maximum_target": maximum}
return {&"ok": true, &"maximum_target": maximum}
static func maximum_target(warehouse_id: StringName, item_id: StringName, facilities: FacilityService) -> int:
var facility: FacilityInstance = facilities.get_instance(warehouse_id) if facilities != null else null
var item := Catalog.get_item(item_id)
if facility == null or facility.definition_id != &"warehouse" or item.is_empty() \
or not bool(item.get(&"purchasable", false)) or int(item.get(&"unit_weight", 0)) <= 0:
return 0
@warning_ignore("integer_division")
return Catalog.storage_capacity(facility.definition_id) / int(item[&"unit_weight"])
static func order_key(warehouse_id: StringName, item_id: StringName, minute: int) -> StringName:
# Length encoding avoids punctuation collisions. This is not a command ID.
return StringName("supply.auto.%d:%s.%d:%s.%d" % [
String(warehouse_id).length(), warehouse_id, String(item_id).length(), item_id, minute])
static func _empty_plan(warehouse_id: StringName, item_id: StringName) -> Dictionary:
return {&"warehouse_id": String(warehouse_id), &"item_id": String(item_id), &"enabled": false,
&"target_stock": 0, &"last_attempt_minute": -1, &"last_result": &"", &"last_transaction_id": "", &"last_quantity": 0, &"last_unit_price": 0, &"last_total_cost": 0}
static func _sorted_names(values: Dictionary) -> Array[StringName]:
var ids: Array[StringName] = []
for value: Variant in values: ids.append(StringName(value))
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
return ids
static func _keys_match(value: Dictionary, expected: Array[StringName]) -> bool:
if value.size() != expected.size(): return false
for key: Variant in value:
if typeof(key) != TYPE_STRING_NAME or key not in expected: return false
return true
static func _text(value: Variant) -> bool:
return typeof(value) in [TYPE_STRING, TYPE_STRING_NAME] and not String(value).strip_edges().is_empty()
func _restore_failure(message: String) -> bool:
last_error = message
return false
@@ -0,0 +1 @@
uid://dxvu3dy4bl6sj
+131 -7
View File
@@ -41,28 +41,149 @@ func purchase(runtime: Variant, command: SimulationCommand) -> CommandResult:
var warehouse_id := StringName(payload[&"warehouse_id"])
var item_id := StringName(payload[&"item_id"])
var quantity := int(payload[&"quantity"])
return _purchase_paid(runtime, warehouse_id, item_id, quantity,
runtime._runtime_event_minute(), StringName("supply.purchase.%d" % command.command_id), command.command_id)
func _purchase_paid(runtime: Variant, warehouse_id: StringName, item_id: StringName,
quantity: int, minute: int, ledger_key: StringName, command_id: int = 0, record_auto_attempt: bool = false) -> CommandResult:
# Shared transaction only; internal replenishment does not enqueue a command.
var quote := preview_purchase(runtime, warehouse_id, item_id, quantity)
if not bool(quote[&"ok"]):
return _failure(command, StringName(quote[&"reason"]), quote)
return _purchase_failure(command_id, StringName(quote[&"reason"]), quote)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
var item := SupplyCatalog.get_item(item_id)
var minute: int = runtime._runtime_event_minute()
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var plans_before: Dictionary = runtime.auto_purchase_plans.create_snapshot() if record_auto_attempt else {}
var stocked: Dictionary = runtime.inventory_service.add_stack(facility.local_inventory_id, item_id, quantity, 100, minute + int(item[&"shelf_life_minutes"]), minute, int(item[&"unit_weight"]))
if not bool(stocked.get(&"ok", false)):
return _failure(command, &"INVENTORY_ERROR")
var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, StringName("supply.purchase.%d" % command.command_id), &"supply_purchase", &"", false)
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"INVENTORY_ERROR")
var memo: StringName = SupplyAutoPurchasePlans.receipt_memo(item_id, quantity, int(item[&"unit_price"]), int(quote[&"total_cost"])) if record_auto_attempt else &"supply_purchase"
var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, ledger_key, memo, &"", false)
if not paid.is_success():
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _failure(command, &"PAYMENT_FAILED")
return _purchase_failure(command_id, &"PAYMENT_FAILED")
if record_auto_attempt and not runtime.auto_purchase_plans.record_attempt(warehouse_id, item_id, minute, CommandResult.succeeded(0, {&"transaction_id": paid.details[&"transaction_id"], &"quantity": quantity, &"unit_price": item[&"unit_price"], &"total_cost": quote[&"total_cost"]}), runtime.economy_service):
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"PAYMENT_FAILED")
if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])):
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _failure(command, &"PAYMENT_FAILED")
if record_auto_attempt:
runtime.auto_purchase_plans.restore_snapshot(plans_before, runtime.facility_service, runtime.economy_service, minute)
return _purchase_failure(command_id, &"PAYMENT_FAILED")
quote.merge({&"warehouse_id": warehouse_id, &"item_id": item_id, &"quantity": quantity, &"transaction_id": paid.details[&"transaction_id"]})
return CommandResult.succeeded(command.command_id, quote)
return CommandResult.succeeded(command_id, quote)
func set_auto_purchase(runtime: Variant, command: SimulationCommand) -> CommandResult:
var payload := command.payload
if not _exact_keys(payload, [&"warehouse_id", &"item_id", &"enabled", &"target_stock"]) \
or not _text(payload[&"warehouse_id"]) or not _text(payload[&"item_id"]) \
or typeof(payload[&"enabled"]) != TYPE_BOOL or typeof(payload[&"target_stock"]) != TYPE_INT:
return _failure(command, &"INVALID_PAYLOAD")
var changed: Dictionary = runtime.auto_purchase_plans.set_plan(
StringName(payload[&"warehouse_id"]), StringName(payload[&"item_id"]),
payload[&"enabled"], payload[&"target_stock"], runtime.facility_service)
if not bool(changed.get(&"ok", false)):
return _failure(command, StringName(changed.get(&"reason", &"INVALID_TARGET")), changed)
return CommandResult.succeeded(command.command_id, changed[&"plan"])
func auto_purchase_report(runtime: Variant, warehouse_id: StringName, item_id: StringName) -> Dictionary:
var row: Dictionary = runtime.auto_purchase_plans.get_plan(warehouse_id, item_id)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
row[&"maximum_target"] = SupplyAutoPurchasePlans.maximum_target(warehouse_id, item_id, runtime.facility_service)
row[&"physical_quantity"] = runtime.inventory_service.total_quantity(facility.local_inventory_id, item_id) if facility != null else 0
row[&"incoming_quantity"] = _incoming_quantity(runtime, facility.local_inventory_id, item_id) if facility != null else 0
row[&"deficit"] = maxi(int(row[&"target_stock"]) - int(row[&"physical_quantity"]) - int(row[&"incoming_quantity"]), 0)
row[&"quote"] = preview_purchase(runtime, warehouse_id, item_id, int(row[&"deficit"])) if int(row[&"deficit"]) > 0 else {}
row[&"reason"] = &""
if not bool(row[&"enabled"]):
row[&"state"] = &"DISABLED"
elif bool(runtime.get_operations_report().get(&"tick_blocked", false)):
row[&"state"] = &"BLOCKED"
row[&"reason"] = &"OPERATIONS_BLOCKED"
elif int(runtime._state.get_resource(&"mana")) <= 0:
row[&"state"] = &"BLOCKED"
row[&"reason"] = &"NO_POWER"
elif _receiver_will_close(facility, runtime._clock.absolute_minute):
row[&"state"] = &"BLOCKED"
row[&"reason"] = &"FACILITY_CLOSED"
elif int(row[&"last_attempt_minute"]) == runtime._clock.absolute_minute:
row[&"state"] = &"ORDERED_THIS_MINUTE" if row[&"last_result"] == SupplyAutoPurchasePlans.RESULT_PURCHASED else &"BLOCKED"
if row[&"state"] == &"BLOCKED": row[&"reason"] = row[&"last_result"]
elif int(row[&"deficit"]) == 0:
row[&"state"] = &"TARGET_REACHED"
elif bool((row[&"quote"] as Dictionary).get(&"ok", false)):
row[&"state"] = &"READY"
else:
row[&"state"] = &"BLOCKED"
row[&"reason"] = (row[&"quote"] as Dictionary).get(&"reason", &"UNAVAILABLE")
return row
func advance_auto_purchases(runtime: Variant, minute: int) -> Array[Dictionary]:
var results: Array[Dictionary] = []
if bool(runtime.get_operations_report().get(&"tick_blocked", false)) or int(runtime._state.get_resource(&"mana")) <= 0:
return results
for plan: Dictionary in runtime.auto_purchase_plans.get_plans():
if not bool(plan[&"enabled"]) or int(plan[&"last_attempt_minute"]) >= minute:
continue
var warehouse_id := StringName(plan[&"warehouse_id"])
var item_id := StringName(plan[&"item_id"])
var row := auto_purchase_report(runtime, warehouse_id, item_id)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
if int(row[&"deficit"]) <= 0 or facility == null or not facility.allows_logistics() or _receiver_will_close(facility, minute):
continue
var ledger_key := SupplyAutoPurchasePlans.order_key(warehouse_id, item_id, minute)
var prior_id: StringName = runtime.economy_service.get_transaction_id_for_idempotency_key(ledger_key)
var result: CommandResult
if prior_id.is_empty():
if row[&"state"] != &"READY" or not bool((row[&"quote"] as Dictionary).get(&"ok", false)):
continue
result = _purchase_paid(runtime, warehouse_id, item_id, int(row[&"deficit"]), minute, ledger_key, 0, true)
else:
# A committed key replays its original item/quantity/price, even if
# today's quote is unaffordable. It never stocks, charges or notifies.
var receipt := SupplyAutoPurchasePlans.receipt_from_ledger(runtime.economy_service, warehouse_id, item_id, minute, prior_id)
if receipt.is_empty():
result = _purchase_failure(0, &"REPLAY_MISMATCH")
else:
result = CommandResult.succeeded(0, {&"transaction_id": prior_id, &"quantity": receipt[&"quantity"], &"unit_price": receipt[&"unit_price"], &"total_cost": receipt[&"total_cost"], &"replayed": true})
if (not result.is_success() or not prior_id.is_empty()) and not runtime.auto_purchase_plans.record_attempt(warehouse_id, item_id, minute, result, runtime.economy_service):
push_error("Automatic purchase could not record its committed attempt.")
return results
results.append({&"warehouse_id": warehouse_id, &"item_id": item_id,
&"minute": minute, &"result": result.to_dictionary()})
return results
func _receiver_will_close(facility: FacilityInstance, minute: int) -> bool:
# LOGISTICS precedes FACILITY_COMPLETION. Honor an already scheduled close
# without applying policy early or changing manual purchase/phase ordering.
return facility != null and facility.policy_pending_effective_at >= 0 \
and facility.policy_pending_effective_at <= minute \
and facility.policy_pending.has(&"business_open") \
and not bool(facility.policy_pending[&"business_open"])
func _purchase_failure(command_id: int, reason: StringName, details: Dictionary = {}) -> CommandResult:
var result := details.duplicate(true)
result[&"reason"] = reason
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command_id, "Supply command rejected: %s" % reason, result)
func _exact_keys(value: Dictionary, expected: Array[StringName]) -> bool:
if value.size() != expected.size(): return false
for key: Variant in value:
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(key) not in expected: return false
return true
func set_production(runtime: Variant, command: SimulationCommand) -> CommandResult:
@@ -90,6 +211,9 @@ func get_report(runtime: Variant) -> Dictionary:
var quote := preview_purchase(runtime, facility.instance_id, SupplyCatalog.RAW_FOOD, 1)
row[&"can_receive"] = bool(quote[&"ok"])
row[&"reason_codes"] = [] if bool(quote[&"ok"]) else [quote[&"reason"]]
row[&"auto_purchase_plans"] = []
for item: Dictionary in SupplyCatalog.get_purchase_items():
(row[&"auto_purchase_plans"] as Array).append(auto_purchase_report(runtime, facility.instance_id, StringName(item[&"item_id"])))
warehouses.append(row)
&"kitchen":
row.merge(runtime.kitchen_production.get_report(facility.instance_id), true)