Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
172 lines
10 KiB
GDScript
172 lines
10 KiB
GDScript
class_name SupplyAutoPurchaseQuotePlans
|
|
extends "res://scripts/supply/supply_auto_purchase_plans.gd"
|
|
|
|
## v1 class remains byte-identical. Only explicit v2 success rows branch here.
|
|
const QuoteV2 = preload("res://scripts/supply/supply_purchase_quote_v2.gd")
|
|
const V1 = preload("res://scripts/supply/supply_auto_purchase_plans.gd")
|
|
const RUNTIME_KEYS: Array[StringName] = [&"runtime_schema_version", &"weather", &"world_seed", &"next_command_id", &"staffing_projection_enabled", &"save_lifecycle", &"command_queue", &"id_generator", &"domain_events", &"rng_streams", &"world", &"path", &"movement_credits", &"mana", &"economy", &"facility_upkeep", &"upkeep_settlement", &"inventory", &"kitchen_production", &"auto_purchase_plans", &"elevators", &"legacy_stairs", &"logistics_dispatcher", &"logistics_requests", &"facilities", &"facility_cleanliness", &"guests", &"staff", &"catalog_manifest", &"progression", &"hired_staff_candidate_ids", &"operations", &"relationships", &"event_definitions", &"events", &"authority", &"defense", &"construction", &"excavation", &"spatial_subslots", &"route_projection", &"facility_infrastructure"]
|
|
const V2_KEYS: Array[StringName] = [&"warehouse_id", &"item_id", &"enabled", &"target_stock",
|
|
&"last_attempt_minute", &"last_result", &"last_transaction_id", &"last_quantity", &"last_quote", &"last_total_cost"]
|
|
|
|
|
|
func record_attempt(warehouse_id: StringName, item_id: StringName, minute: int,
|
|
result: CommandResult, economy: EconomyService) -> bool:
|
|
if not result.is_success() or not result.details.has(&"purchase_quote"):
|
|
var previous := get_plan(warehouse_id, item_id)
|
|
# A later real failure/ordinary purchase replaces only the last attempt;
|
|
# append-only v2 payment history stays in EconomyService.
|
|
if previous.has(&"last_quote"):
|
|
var legacy := _empty_plan(warehouse_id, item_id)
|
|
for key: StringName in legacy:
|
|
if previous.has(key): legacy[key] = previous[key]
|
|
_plans[warehouse_id][item_id] = legacy
|
|
var accepted := super.record_attempt(warehouse_id, item_id, minute, result, economy)
|
|
if not accepted and previous.has(&"last_quote"):
|
|
_plans[warehouse_id][item_id] = previous
|
|
return accepted
|
|
if not result.is_success() or not _plans.has(warehouse_id) \
|
|
or not (_plans[warehouse_id] as Dictionary).has(item_id): return false
|
|
var plan := get_plan(warehouse_id, item_id)
|
|
if minute < 0 or minute <= int(plan[&"last_attempt_minute"]): return false
|
|
var quote: Variant = result.details[&"purchase_quote"]
|
|
if not quote is Dictionary or not QuoteV2.valid(quote) \
|
|
or quote[&"item_id"] != String(item_id) \
|
|
or typeof(result.details.get(&"transaction_id")) != TYPE_STRING_NAME \
|
|
or typeof(result.details.get(&"quantity")) != TYPE_INT \
|
|
or result.details[&"quantity"] != quote[&"quantity"] \
|
|
or typeof(result.details.get(&"total_cost")) != TYPE_INT \
|
|
or result.details[&"total_cost"] != quote[&"effective_total"]: return false
|
|
var receipt := QuoteV2.receipt_from_ledger(economy, warehouse_id, item_id, minute, result.details[&"transaction_id"])
|
|
if var_to_bytes(receipt) != var_to_bytes(quote): return false
|
|
var row := {&"warehouse_id": String(warehouse_id), &"item_id": String(item_id),
|
|
&"enabled": plan[&"enabled"], &"target_stock": plan[&"target_stock"],
|
|
&"last_attempt_minute": minute, &"last_result": &"PURCHASED",
|
|
&"last_transaction_id": String(result.details[&"transaction_id"]),
|
|
&"last_quantity": quote[&"quantity"], &"last_quote": quote.duplicate(true),
|
|
&"last_total_cost": quote[&"effective_total"]}
|
|
_plans[warehouse_id][item_id] = row
|
|
return true
|
|
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
var rows := get_plans()
|
|
for row: Dictionary in rows:
|
|
if row.has(&"last_quote"):
|
|
return {&"version": 2, &"plans": rows}
|
|
return super.create_snapshot()
|
|
|
|
|
|
func restore_snapshot(snapshot: Dictionary, facilities: FacilityService,
|
|
economy: EconomyService, minute: int) -> bool:
|
|
last_error = ""
|
|
if not QuoteV2.keys_match(snapshot, [&"version", &"plans"]) \
|
|
or typeof(snapshot[&"version"]) != TYPE_INT: return false
|
|
if snapshot[&"version"] == 1:
|
|
# The original v1 parser stays literal. A new paid v2 receipt cannot be
|
|
# represented as a REPLAY_MISMATCH failed old row.
|
|
var original_parser := V1.new()
|
|
if not original_parser.restore_snapshot(snapshot, facilities, economy, minute):
|
|
last_error = original_parser.last_error
|
|
return false
|
|
if not _v1_rows_have_no_valid_v2_payment(snapshot[&"plans"], economy): return false
|
|
_plans = original_parser._plans.duplicate(true)
|
|
last_error = original_parser.last_error
|
|
return true
|
|
if snapshot[&"version"] != 2 or not snapshot[&"plans"] is Array \
|
|
or facilities == null or economy == null or minute < 0: return false
|
|
if (snapshot[&"plans"] as Array).size() > facilities.get_instances().size() * Catalog.get_purchase_items().size(): return false
|
|
var candidate: Dictionary = {}
|
|
var saw_v2 := false
|
|
var old_rows: Array[Dictionary] = []
|
|
for value: Variant in snapshot[&"plans"]:
|
|
if not value is Dictionary: return false
|
|
var row: Dictionary = value
|
|
if row.has(&"last_quote"):
|
|
if not _valid_v2_row(row, facilities, economy, minute): return false
|
|
saw_v2 = true
|
|
else:
|
|
old_rows.append(row.duplicate(true))
|
|
if not saw_v2 or not _v1_rows_have_no_valid_v2_payment(old_rows, economy): return false
|
|
var original_parser := V1.new()
|
|
if not original_parser.restore_snapshot({&"version": 1, &"plans": old_rows}, facilities, economy, minute): return false
|
|
# Identifiers are accessed only after every old/new row passed its own
|
|
# strict alternative, avoiding casts of malformed old rows in a mixed table.
|
|
for row: Dictionary in snapshot[&"plans"]:
|
|
var warehouse := StringName(row[&"warehouse_id"])
|
|
var item := StringName(row[&"item_id"])
|
|
if candidate.has(warehouse) and (candidate[warehouse] as Dictionary).has(item): return false
|
|
if not candidate.has(warehouse): candidate[warehouse] = {}
|
|
candidate[warehouse][item] = row.duplicate(true)
|
|
var canonical := SupplyAutoPurchaseQuotePlans.new()
|
|
canonical._plans = candidate
|
|
if var_to_bytes(canonical.create_snapshot()) != var_to_bytes(snapshot): return false
|
|
# No live owner changes until both strict alternatives/crosslinks pass.
|
|
_plans = candidate
|
|
last_error = ""
|
|
return true
|
|
|
|
|
|
static func _valid_v2_row(row: Dictionary, facilities: FacilityService,
|
|
economy: EconomyService, minute: int) -> bool:
|
|
if not QuoteV2.keys_match(row, V2_KEYS): return false
|
|
for field: StringName in [&"warehouse_id", &"item_id", &"last_transaction_id"]:
|
|
if typeof(row[field]) != TYPE_STRING or String(row[field]).strip_edges().is_empty(): return false
|
|
for field: StringName in [&"target_stock", &"last_attempt_minute", &"last_quantity", &"last_total_cost"]:
|
|
if typeof(row[field]) != TYPE_INT: return false
|
|
if typeof(row[&"enabled"]) != TYPE_BOOL or typeof(row[&"last_result"]) != TYPE_STRING_NAME \
|
|
or row[&"last_result"] != &"PURCHASED" or not row[&"last_quote"] is Dictionary: return false
|
|
var warehouse := StringName(row[&"warehouse_id"])
|
|
var item := StringName(row[&"item_id"])
|
|
if not bool(V1.validate_intent(warehouse, item, row[&"enabled"], row[&"target_stock"], facilities)[&"ok"]): return false
|
|
var quote: Dictionary = row[&"last_quote"]
|
|
if not QuoteV2.valid(quote) or quote[&"item_id"] != String(item) \
|
|
or row[&"last_quantity"] != quote[&"quantity"] or row[&"last_total_cost"] != quote[&"effective_total"] \
|
|
or row[&"last_attempt_minute"] < 0 or row[&"last_attempt_minute"] > minute: return false
|
|
var receipt := QuoteV2.receipt_from_ledger(economy, warehouse, item, row[&"last_attempt_minute"], StringName(row[&"last_transaction_id"]))
|
|
return var_to_bytes(receipt) == var_to_bytes(quote)
|
|
|
|
|
|
static func _v1_rows_have_no_valid_v2_payment(rows: Variant, economy: EconomyService) -> bool:
|
|
if not rows is Array or economy == null: return false
|
|
for value: Variant in rows:
|
|
if not value is Dictionary: return false
|
|
var row: Dictionary = value
|
|
if row.has(&"last_quote"): return false
|
|
if typeof(row.get(&"last_attempt_minute")) != TYPE_INT \
|
|
or typeof(row.get(&"warehouse_id")) != TYPE_STRING or typeof(row.get(&"item_id")) != TYPE_STRING: continue
|
|
var minute: int = row[&"last_attempt_minute"]
|
|
if minute < 0: continue
|
|
var warehouse := StringName(row.get(&"warehouse_id", ""))
|
|
var item := StringName(row.get(&"item_id", ""))
|
|
var paid := economy.get_transaction_id_for_idempotency_key(V1.order_key(warehouse, item, minute))
|
|
if not paid.is_empty() and not QuoteV2.receipt_from_ledger(economy, warehouse, item, minute, paid).is_empty(): return false
|
|
return true
|
|
|
|
|
|
static func valid_runtime_format(runtime_data: Dictionary, minute: int) -> bool:
|
|
if not QuoteV2.keys_match(runtime_data, RUNTIME_KEYS) or typeof(runtime_data.get(&"runtime_schema_version")) != TYPE_INT \
|
|
or runtime_data[&"runtime_schema_version"] != 15 or not QuoteV2.has_history(runtime_data): return false
|
|
if not runtime_data.get(&"economy") is Dictionary or not runtime_data.get(&"facilities") is Dictionary \
|
|
or not runtime_data.get(&"auto_purchase_plans") is Dictionary: return false
|
|
var economy := EconomyService.new()
|
|
var facilities := FacilityService.new()
|
|
if not economy.restore_snapshot(runtime_data[&"economy"]) or not facilities.restore_snapshot(runtime_data[&"facilities"]): return false
|
|
if not runtime_data.get(&"command_queue") is Dictionary or not runtime_data.get(&"domain_events") is Dictionary: return false
|
|
var queue := CommandQueue.new()
|
|
var stream := DomainEventStream.new()
|
|
if not queue.restore_snapshot(runtime_data[&"command_queue"]) \
|
|
or not stream.restore_snapshot(runtime_data[&"domain_events"]): return false
|
|
# Match Runtime's existing domain-minute rule, including held/terminal Ops.
|
|
var operations: Variant = runtime_data.get(&"operations")
|
|
if not operations is Dictionary or typeof(operations.get(&"enabled")) != TYPE_BOOL \
|
|
or typeof(operations.get(&"deferred_runtime_minute")) != TYPE_INT \
|
|
or not operations.get(&"coordinator") is Dictionary: return false
|
|
var domain_minute := minute
|
|
if operations[&"enabled"]:
|
|
var coordinator := OperationsCoordinator.new()
|
|
if not coordinator.restore_snapshot(operations[&"coordinator"]): return false
|
|
if operations[&"deferred_runtime_minute"] == minute or coordinator.is_terminal(): domain_minute = minute - 1
|
|
var plans := SupplyAutoPurchaseQuotePlans.new()
|
|
return QuoteV2.valid_history(runtime_data, economy) \
|
|
and plans.restore_snapshot(runtime_data[&"auto_purchase_plans"], facilities, economy, domain_minute)
|