Add supply auto-purchase and preserve save recovery state
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user