Files
aetherbound-guild/runtime/fg3/contract2_service.gd
T

170 lines
5.4 KiB
GDScript

class_name FG3Contract2Service
extends RefCounted
const MarketGenerator = preload("res://fg3/contract2_market_generator.gd")
const State = preload("res://fg3/run_state.gd")
var state := State.new()
var valid := false
var error_code := "invalid_state"
func _init(snapshot_value: Dictionary) -> void:
valid = state.restore_snapshot(snapshot_value)
if valid:
error_code = ""
func activate_contract(
operation_id: String,
expected_revision: int,
expected_handoff_hash: String
) -> Dictionary:
var intent := {
"expected_revision": expected_revision,
"expected_handoff_hash": expected_handoff_hash,
}
var prior := _prior_operation(operation_id, "activate_contract2", intent)
if prior.known:
return prior.result
if not valid:
return _reject(error_code)
if expected_revision != state.revision:
return _reject("stale_revision")
if String(state.current_run.phase) != "contract2_handoff":
return _reject("contract2_handoff_required")
if expected_handoff_hash != String(state.current_run.market_board.get("handoff_hash", "")):
return _reject("handoff_hash_mismatch")
if "CLEAR-RG02-C01" not in state.guild_profile.contract_clear_ids:
return _reject("contract1_clear_required")
var generated := MarketGenerator.new().generate(String(state.current_run.run_seed), 1, 0)
if not generated.ok:
return _reject("contract2_market_generation_failed")
var before := state.snapshot()
var company_hash := _hash({
"company": Array(state.current_run.company).duplicate(),
"records": Dictionary(state.current_run.recruit_records).duplicate(true),
"inventory": Dictionary(state.current_run.inventory).duplicate(true),
"equipped": Dictionary(state.current_run.equipped_by_recruit).duplicate(true),
})
var board: Dictionary = generated.board
state.current_run.contract_id = "CTR-R2-02"
state.current_run.phase = "contract2_market"
state.current_run.market_index = 1
state.current_run.round_index = 1
state.current_run.line_capacity = 10
state.current_run.refresh_count = 0
state.current_run.market_board = board.duplicate(true)
state.current_run.available_recruit_offer_ids = _offer_ids(board.recruit_slots, "slot_id")
state.current_run.available_equipment_offer_ids = _offer_ids(board.equipment_offers, "offer_id")
state.current_run.available_battle_offer_ids = _offer_ids(board.battle_offers, "offer_id")
state.current_run.locked_offer_ids = []
state.current_run.selected_route_id = ""
state.current_run.pending_encounter = {}
state.current_run.pre_battle_checkpoint_id = ""
state.current_run.recovery_required = false
state.current_run.recovery_cost = 0
state.current_run.boss_muster = {}
state.current_run.selected_boss_plan = ""
state.current_run.pending_boss_result = {}
state.current_run.next_contract_id = ""
return _commit(operation_id, "activate_contract2", intent, "contract2_activated", {
"operation_id": operation_id,
"contract_id": "CTR-R2-02",
"market_index": 1,
"board_hash": String(board.board_hash),
"source_handoff_hash": expected_handoff_hash,
"company_hash": company_hash,
"company_ids": Array(state.current_run.company).duplicate(),
"cleared_rounds": int(state.current_run.cleared_rounds),
"line_capacity": int(state.current_run.line_capacity),
}, before)
func _prior_operation(operation_id: String, kind: String, intent: Dictionary) -> Dictionary:
if operation_id.is_empty():
return {"known": true, "result": _reject("operation_id_required")}
if not valid or not state.operation_receipts.has(operation_id):
return {"known": false}
var stored: Dictionary = state.operation_receipts[operation_id]
if String(stored.kind) != kind or String(stored.intent_hash) != _hash(intent):
return {"known": true, "result": _reject("operation_conflict")}
return {"known": true, "result": {
"ok": true,
"code": String(stored.code),
"duplicate": true,
"revision": int(stored.revision),
"receipt": Dictionary(stored.receipt).duplicate(true),
"snapshot": state.snapshot(),
}}
func _commit(
operation_id: String,
kind: String,
intent: Dictionary,
code: String,
receipt: Dictionary,
before: Dictionary
) -> Dictionary:
state.revision += 1
state.operation_receipts[operation_id] = {
"operation_id": operation_id,
"kind": kind,
"intent_hash": _hash(intent),
"status": "applied",
"code": code,
"revision": state.revision,
"receipt": receipt.duplicate(true),
}
if not state.invariants_hold():
state.restore_snapshot(before)
return _reject("postcondition_failed")
return {
"ok": true,
"code": code,
"duplicate": false,
"revision": state.revision,
"receipt": receipt.duplicate(true),
"snapshot": state.snapshot(),
}
func _reject(code: String) -> Dictionary:
return {
"ok": false,
"code": code,
"duplicate": false,
"revision": state.revision if valid else -1,
}
func _offer_ids(offers: Array, field: String) -> Array[String]:
var result: Array[String] = []
for offer in offers:
result.append(String(offer.get(field, "")))
return result
func _hash(value) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
func _canonical(value):
if value is float and is_equal_approx(value, round(value)):
return int(round(value))
if value is Dictionary:
var result := {}
var keys: Array = value.keys()
keys.sort_custom(func(left, right): return String(left) < String(right))
for key in keys:
result[key] = _canonical(value[key])
return result
if value is Array:
var result: Array = []
for item in value:
result.append(_canonical(item))
return result
return value