Files
TheTree_mock/scripts/supply/supply_purchase_quote_v2.gd
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
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.
2026-10-11 15:58:00 +09:00

298 lines
15 KiB
GDScript

class_name SupplyPurchaseQuoteV2
extends RefCounted
## Explicit total-order quote. v1 unit receipts are never reinterpreted.
## Placement uses the existing staff UI plan-first/legacy-live fallback rule.
## Total-order rounding reuses Pricing; attendance/role/floor multipliers are not added.
const Pricing = preload("res://scripts/facility/facility_pricing.gd")
const V1Plans = preload("res://scripts/supply/supply_auto_purchase_plans.gd")
const PREFIX := "supply_purchase.quote.v2:"
const MIMI := &"heroine_mimi"
const MULTIPLIER_BP := 8000
const MAX_INT_TEXT := "9223372036854775807"
const MAX_INT := 9223372036854775807
const KEYS: Array[StringName] = [&"version", &"item_id", &"quantity", &"base_unit_price",
&"base_total", &"multiplier_bp", &"effective_total", &"applied_unique_id"]
static func eligible(runtime: Variant) -> bool:
var mimi: StaffRuntime = runtime.staff_service.get_staff(MIMI)
if mimi == null or mimi.employment_kind != StaffRuntime.EmploymentKind.UNIQUE: return false
var assignment: Dictionary = runtime.staff_service.get_work_assignment(MIMI)
var facility_id := mimi.assigned_facility_id
if not assignment.is_empty(): facility_id = StringName(assignment.get(&"facility_id", &""))
return not facility_id.is_empty() and runtime.facility_service.get_instance(facility_id) != null
static func make(item_id: StringName, quantity: int, base_unit_price: int) -> Dictionary:
@warning_ignore("integer_division")
var maximum_base := (MAX_INT - 5000) / MULTIPLIER_BP
if String(item_id).strip_edges().is_empty() or quantity <= 0 or quantity > 10000 or base_unit_price <= 0:
return {}
@warning_ignore("integer_division")
var maximum_unit := maximum_base / quantity
if base_unit_price > maximum_unit: return {}
var base_total := quantity * base_unit_price
return {&"version": 2, &"item_id": String(item_id), &"quantity": quantity,
&"base_unit_price": base_unit_price, &"base_total": base_total,
&"multiplier_bp": MULTIPLIER_BP, &"effective_total": Pricing.apply_multiplier(base_total, MULTIPLIER_BP),
&"applied_unique_id": String(MIMI)}
static func valid(quote: Dictionary) -> bool:
if not keys_match(quote, KEYS): return false
for key: StringName in [&"version", &"quantity", &"base_unit_price", &"base_total", &"multiplier_bp", &"effective_total"]:
if typeof(quote[key]) != TYPE_INT: return false
if typeof(quote[&"item_id"]) != TYPE_STRING or typeof(quote[&"applied_unique_id"]) != TYPE_STRING:
return false
if quote[&"version"] != 2 or quote[&"multiplier_bp"] != MULTIPLIER_BP \
or quote[&"applied_unique_id"] != String(MIMI): return false
var canonical := make(StringName(quote[&"item_id"]), quote[&"quantity"], quote[&"base_unit_price"])
# This also preserves canonical key/container/type order without normalizing
# corrupt saved fields or consulting today's catalog/personnel state.
return not canonical.is_empty() and var_to_bytes(canonical) == var_to_bytes(quote)
static func affordable_quantity(cash: int, space: int, base_unit_price: int) -> int:
var lower := 0
var upper := mini(maxi(space, 0), 10000)
while lower < upper:
@warning_ignore("integer_division")
var middle := (lower + upper + 1) / 2
var quote := make(&"affordability", middle, base_unit_price)
if not quote.is_empty() and quote[&"effective_total"] <= cash:
lower = middle
else:
upper = middle - 1
return lower
static func receipt_memo(quote: Dictionary) -> StringName:
if not valid(quote): return &""
var item: String = quote[&"item_id"]
var actor: String = quote[&"applied_unique_id"]
return StringName(PREFIX + "%d:%s:%d:%s:%d:%d:%d:%d:%d" % [item.length(), item,
actor.length(), actor, quote[&"quantity"], quote[&"base_unit_price"], quote[&"base_total"],
quote[&"multiplier_bp"], quote[&"effective_total"]])
static func decode_receipt_memo(memo: StringName) -> Dictionary:
var text := String(memo)
if not text.begins_with(PREFIX): return {}
var item := _take_text(text.substr(PREFIX.length()))
if item.is_empty(): return {}
var actor := _take_text(item[&"rest"])
if actor.is_empty(): return {}
var fields: PackedStringArray = String(actor[&"rest"]).split(":", true)
if fields.size() != 5: return {}
var quote := {&"version": 2, &"item_id": item[&"text"], &"quantity": _positive(fields[0]),
&"base_unit_price": _positive(fields[1]), &"base_total": _positive(fields[2]),
&"multiplier_bp": _positive(fields[3]), &"effective_total": _positive(fields[4]),
&"applied_unique_id": actor[&"text"]}
return quote if valid(quote) and receipt_memo(quote) == memo else {}
static func receipt_from_ledger(economy: EconomyService, warehouse_id: StringName,
item_id: StringName, minute: int, transaction_id: StringName,
automatic := true, command_id := 0) -> Dictionary:
var key := V1Plans.order_key(warehouse_id, item_id, minute) if automatic \
else StringName("supply.purchase.%d" % command_id)
if economy == null or transaction_id.is_empty() or (not automatic and command_id <= 0) \
or economy.get_transaction_id_for_idempotency_key(key) != transaction_id:
return {}
var entries := economy.get_transaction_entries(transaction_id)
if entries.size() != 1: return {}
var entry: LedgerEntry = entries[0]
var quote := decode_receipt_memo(entry.memo_key)
if quote.is_empty() or quote[&"item_id"] != String(item_id) \
or entry.amount != quote[&"effective_total"] or entry.absolute_minute != minute \
or entry.source_entity_id != warehouse_id or entry.category != &"OPERATING_EXPENSE" \
or entry.credit_account != &"PLAYER_CASH" or entry.debit_account != &"OPERATING_EXPENSE" \
or not entry.related_transaction_id.is_empty(): return {}
return quote
static func has_history(runtime_data: Dictionary) -> bool:
var economy: Variant = runtime_data.get(&"economy")
if economy is Dictionary and economy.get(&"entries") is Array:
for entry: Variant in economy[&"entries"]:
if entry is Dictionary and typeof(entry.get(&"memo_key")) in [TYPE_STRING, TYPE_STRING_NAME] \
and String(entry[&"memo_key"]).begins_with(PREFIX): return true
var plans: Variant = runtime_data.get(&"auto_purchase_plans")
if plans is Dictionary:
if plans.get(&"version") == 2: return true
if plans.get(&"plans") is Array:
for row: Variant in plans[&"plans"]:
if row is Dictionary and row.has(&"last_quote"): return true
var queue: Variant = runtime_data.get(&"command_queue")
if queue is Dictionary and queue.get("processed") is Array:
for result: Variant in queue["processed"]:
if result is Dictionary and _result_has_quote(result): return true
var stream: Variant = runtime_data.get(&"domain_events")
if stream is Dictionary and stream.get("recent_events") is Array:
for event: Variant in stream["recent_events"]:
if event is Dictionary and event.get("event_type") in ["COMMAND_COMPLETED", "COMMAND_FAILED"] \
and event.get("payload") is Dictionary and _result_has_quote(event["payload"]): return true
# The uncapped processed queue also keeps history after recent-event eviction.
return false
static func valid_history(runtime_data: Dictionary, economy: EconomyService) -> bool:
if economy == null: return false
var raw_economy: Variant = runtime_data.get(&"economy")
if not raw_economy is Dictionary or not raw_economy.get(&"entries") is Array \
or not raw_economy.get(&"idempotency") is Array: return false
var entries := economy.get_entries()
if entries.size() != (raw_economy[&"entries"] as Array).size(): return false
var v2_transactions: Dictionary = {}
for index: int in entries.size():
var entry: LedgerEntry = entries[index]
if not String(entry.memo_key).begins_with(PREFIX): continue
# The old Economy parser permits legacy coercions. New v2 payment fields
# must retain the complete actual nine-field producer representation.
var raw: Variant = raw_economy[&"entries"][index]
if not raw is Dictionary or var_to_bytes(raw) != var_to_bytes(entry.to_dictionary()): return false
v2_transactions[String(entry.transaction_id)] = true
for value: Variant in raw_economy[&"idempotency"]:
if not value is Dictionary: return false
if not v2_transactions.has(value.get(&"transaction_id")): continue
if not keys_match(value, [&"key", &"transaction_id"]) \
or typeof(value[&"key"]) != TYPE_STRING or typeof(value[&"transaction_id"]) != TYPE_STRING \
or var_to_bytes(value) != var_to_bytes({&"key": value[&"key"], &"transaction_id": value[&"transaction_id"]}): return false
var required_manual: Dictionary = {}
for entry: LedgerEntry in economy.get_entries():
if not String(entry.memo_key).begins_with(PREFIX): continue
var quote := decode_receipt_memo(entry.memo_key)
if quote.is_empty() or entry.source_entity_id.is_empty(): return false
var item := StringName(quote[&"item_id"])
var automatic := receipt_from_ledger(economy, entry.source_entity_id, item, entry.absolute_minute, entry.transaction_id)
if not automatic.is_empty(): continue
var command_id := _manual_command_id_for_transaction(economy, entry.transaction_id)
if receipt_from_ledger(economy, entry.source_entity_id, item, entry.absolute_minute, entry.transaction_id, false, command_id).is_empty(): return false
required_manual[command_id] = quote
var queue: Variant = runtime_data.get(&"command_queue")
if not queue is Dictionary or not queue.get("processed") is Array: return false
var processed: Dictionary = {}
for result: Variant in queue["processed"]:
if not result is Dictionary or not _valid_result_quotes(result, economy): return false
if typeof(result.get("command_id")) != TYPE_INT: return false
processed[result["command_id"]] = result
# Manual payments have uncapped processed results; removing/replacing their
# frozen quote is invalid. Auto plans may legitimately be replaced/pruned,
# so their append-only ledger receipt remains sufficient historical proof.
for command_id: int in required_manual:
if not processed.has(command_id): return false
var result: Dictionary = processed[command_id]
if result.get("code") != "OK" or not result.get("details") is Dictionary \
or not result["details"].get(&"purchase_quote") is Dictionary \
or var_to_bytes(result["details"][&"purchase_quote"]) != var_to_bytes(required_manual[command_id]): return false
var stream: Variant = runtime_data.get(&"domain_events")
if not stream is Dictionary or not stream.get("recent_events") is Array: return false
for event: Variant in stream["recent_events"]:
if not event is Dictionary: return false
if event.get("event_type") not in ["COMMAND_COMPLETED", "COMMAND_FAILED"]: continue
var payload: Variant = event.get("payload")
if not payload is Dictionary or not _result_has_quote(payload): continue
var command_id: Variant = event.get("causation_command_id")
if typeof(command_id) != TYPE_INT or not processed.has(command_id) \
or typeof(event.get("source_service")) != TYPE_STRING or event["source_service"] != "simulation_runtime" \
or var_to_bytes(payload) != var_to_bytes(processed[command_id]): return false
return true
static func _result_has_quote(result: Dictionary) -> bool:
# Only the owned original_result chain is traversed; arbitrary dictionaries
# and schema-looking strings elsewhere are not version authorities.
var current: Dictionary = result
var seen: Array[Dictionary] = []
while true:
for visited: Dictionary in seen:
if is_same(visited, current): return false
seen.append(current)
var details: Variant = current.get("details")
if not details is Dictionary: return false
if details.has(&"purchase_quote"): return true
var original: Variant = details.get("original_result")
if not original is Dictionary: return false
current = original
return false
static func _valid_result_quotes(result: Dictionary, economy: EconomyService) -> bool:
var current: Dictionary = result
var seen: Array[Dictionary] = []
while true:
for visited: Dictionary in seen:
if is_same(visited, current): return false
seen.append(current)
var details: Variant = current.get("details")
if not details is Dictionary: return false
if details.has(&"purchase_quote"):
# Existing CommandQueue owns all seven result fields; quote results
# additionally retain their exact producer string/container types.
if not _canonical_result(current): return false
var quote: Variant = details[&"purchase_quote"]
if not quote is Dictionary or not valid(quote): return false
if current["code"] == "OK":
if typeof(details.get(&"quantity")) != TYPE_INT or details[&"quantity"] != quote[&"quantity"] \
or typeof(details.get(&"total_cost")) != TYPE_INT or details[&"total_cost"] != quote[&"effective_total"] \
or typeof(details.get(&"warehouse_id")) != TYPE_STRING_NAME or typeof(details.get(&"item_id")) != TYPE_STRING_NAME \
or typeof(details.get(&"transaction_id")) != TYPE_STRING_NAME: return false
var entries := economy.get_transaction_entries(details[&"transaction_id"])
if entries.size() != 1: return false
var receipt := receipt_from_ledger(economy, details[&"warehouse_id"], details[&"item_id"],
(entries[0] as LedgerEntry).absolute_minute, details[&"transaction_id"], false, current["command_id"])
if var_to_bytes(receipt) != var_to_bytes(quote): return false
var original: Variant = details.get("original_result")
if not original is Dictionary: return true
current = original
return false
static func _canonical_result(result: Dictionary) -> bool:
if result.size() != 7: return false
for key: Variant in result:
if typeof(key) != TYPE_STRING or key not in ["code", "command_id", "target_id", "message", "resolution_hints", "details", "is_replay"]: return false
if typeof(result["code"]) != TYPE_STRING or String(result["code"]) not in CommandResult.Code.keys() \
or typeof(result["command_id"]) != TYPE_INT or result["command_id"] <= 0 \
or typeof(result["target_id"]) != TYPE_STRING or typeof(result["message"]) != TYPE_STRING \
or not result["resolution_hints"] is Array or not result["details"] is Dictionary \
or typeof(result["is_replay"]) != TYPE_BOOL: return false
for hint: Variant in result["resolution_hints"]:
if typeof(hint) != TYPE_STRING: return false
return true
static func _manual_command_id_for_transaction(economy: EconomyService, transaction_id: StringName) -> int:
for row: Dictionary in economy.create_snapshot()[&"idempotency"]:
var key: String = row[&"key"]
if row[&"transaction_id"] != String(transaction_id) or not key.begins_with("supply.purchase."): continue
var number := _positive(key.substr("supply.purchase.".length()))
if key == "supply.purchase.%d" % number: return number
return 0
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 _take_text(text: String) -> Dictionary:
var delimiter := text.find(":")
if delimiter <= 0: return {}
var length := _positive(text.substr(0, delimiter))
var rest := text.substr(delimiter + 1)
if length <= 0 or length >= rest.length() or rest.substr(length, 1) != ":": return {}
return {&"text": rest.substr(0, length), &"rest": rest.substr(length + 1)}
static func _positive(text: String) -> int:
if text.is_empty() or text.length() > MAX_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_INT_TEXT.length() and text > MAX_INT_TEXT: return 0
return text.to_int()