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)