170 lines
5.4 KiB
GDScript
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
|