class_name ABGGuildContractService extends RefCounted const DraftService = preload("res://entry/initial_company_draft_service.gd") const ContentRegistry = preload("res://p10_0/content_registry.gd") const OfferGenerator = preload("res://p10_0/offer_generator.gd") const DEFAULT_COMPANY_PATH := "user://aetherbound_expedition_company.json" const FIRST_CONTRACT_ID := "CTR-R1-01" const DIFFICULTIES := ["wayfinder", "standard", "stormbound", "iron_oath"] const STARTING_COIN := 52 const STARTING_LINE_CAPACITY := 4 const COMPANY_ATTRIBUTE_IDS := ["might", "finesse", "insight", "vigor", "resolve"] var company_path := DEFAULT_COMPANY_PATH var _content: Dictionary var _generator: RefCounted func _init() -> void: _content = ContentRegistry.snapshot() _generator = OfferGenerator.new(_content) func load_company() -> Dictionary: return _store().load_company() func create_contract(operation_id: String, contract_id: String, difficulty: String) -> Dictionary: if not _is_hex(operation_id, 32): return {"ok": false, "code": "CONTRACT_OPERATION_ID_INVALID"} if contract_id != FIRST_CONTRACT_ID: return {"ok": false, "code": "CONTRACT_LOCKED"} if difficulty not in DIFFICULTIES: return {"ok": false, "code": "CONTRACT_DIFFICULTY_INVALID"} var store := _store() var loaded: Dictionary = store.load_company() if not loaded.ok: return loaded var company: Dictionary = loaded.company.duplicate(true) if not company.active_contract.is_empty(): var existing_receipt: Dictionary = company.active_contract.creation_receipt var same_intent := String(existing_receipt.contract_id) == contract_id and String(existing_receipt.difficulty) == difficulty if String(existing_receipt.operation_id) == operation_id and not same_intent: return {"ok": false, "code": "CONTRACT_OPERATION_CONFLICT"} if same_intent: return { "ok": true, "code": "already_created", "company": company, "contract": company.active_contract, "receipt": existing_receipt, } return {"ok": false, "code": "CONTRACT_ALREADY_ACTIVE"} var contract_ordinal := int(company.next_contract_ordinal) var intent := { "company_id": String(company.company_id), "contract_ordinal": contract_ordinal, "contract_id": contract_id, "difficulty": difficulty, } var intent_hash := _hash(intent) if contract_ordinal != 0: return {"ok": false, "code": "CONTRACT_ORDINAL_INVALID"} var run_seed := _low128(_digest(String(company.company_id), "dev3-contract-run", [contract_ordinal])) var run_id := _low128(_digest(run_seed, "dev3-contract-id", [contract_id])) var generated: Dictionary = _generator.market_board(run_seed, contract_id, 1, 0) if not generated.ok: return {"ok": false, "code": "CONTRACT_MARKET_GENERATION_FAILED"} var board := _price_first_board(generated.board) var receipt := { "operation_id": operation_id, "intent_hash": intent_hash, "company_id": String(company.company_id), "contract_ordinal": contract_ordinal, "contract_id": contract_id, "difficulty": difficulty, "run_id": run_id, "run_seed": run_seed, "board_hash": String(board.board_hash), "coin": STARTING_COIN, "line_capacity": STARTING_LINE_CAPACITY, } receipt.receipt_hash = _hash(receipt) var contract := { "contract_version": 1, "company_id": String(company.company_id), "contract_id": contract_id, "run_id": run_id, "run_seed": run_seed, "difficulty": difficulty, "status": "market", "market_index": 1, "coin": STARTING_COIN, "line_capacity": STARTING_LINE_CAPACITY, "market_board": board, "available_recruit_offer_ids": _offer_ids(board.recruit_offers, "source_offer_id"), "available_equipment_offer_ids": _offer_ids(board.equipment_offers, "offer_id"), "available_battle_offer_ids": _offer_ids(board.battle_offers, "offer_id"), "hire_receipts": [], "creation_receipt": receipt, } contract.contract_hash = _hash(contract) company.company_revision = int(company.company_revision) + 1 company.next_contract_ordinal = contract_ordinal + 1 company.active_contract = contract company.contract_receipts = [receipt] company.erase("company_hash") company.company_hash = _hash(company) var saved: Dictionary = store.save_company(company) if not saved.ok: return saved return { "ok": true, "code": "created", "company": saved.company, "contract": saved.company.active_contract, "receipt": saved.company.active_contract.creation_receipt, } func hire_recruit(operation_id: String, offer_id: String, expected_price: int) -> Dictionary: if not _is_hex(operation_id, 32): return {"ok": false, "code": "HIRE_OPERATION_ID_INVALID"} if not _is_hex(offer_id, 32): return {"ok": false, "code": "HIRE_OFFER_ID_INVALID"} var store := _store() var loaded: Dictionary = store.load_company() if not loaded.ok: return loaded var company: Dictionary = loaded.company.duplicate(true) if company.active_contract.is_empty() or String(company.active_contract.get("status", "")) != "market": return {"ok": false, "code": "HIRE_MARKET_REQUIRED"} var contract: Dictionary = company.active_contract var intent := { "company_id": String(company.company_id), "run_id": String(contract.run_id), "board_hash": String(contract.market_board.board_hash), "offer_id": offer_id, "expected_price": expected_price, } var intent_hash := _hash(intent) for stored_receipt in contract.hire_receipts: var prior_receipt: Dictionary = stored_receipt if String(prior_receipt.operation_id) == operation_id: if String(prior_receipt.intent_hash) != intent_hash: return {"ok": false, "code": "HIRE_OPERATION_CONFLICT"} return {"ok": true, "code": "already_hired", "company": company, "receipt": prior_receipt} if String(prior_receipt.offer_id) == offer_id: if String(prior_receipt.intent_hash) == intent_hash: return {"ok": true, "code": "already_hired", "company": company, "receipt": prior_receipt} return {"ok": false, "code": "HIRE_OFFER_ALREADY_APPLIED_CONFLICT"} if contract.hire_receipts.size() >= 2: return {"ok": false, "code": "OPENING_HIRES_COMPLETE"} var offer: Dictionary = _find_recruit_offer(contract, offer_id) if offer.is_empty() or offer_id not in contract.available_recruit_offer_ids: return {"ok": false, "code": "HIRE_OFFER_UNAVAILABLE"} var list_price: int = int(offer.get("list_price", -1)) if expected_price != list_price: return {"ok": false, "code": "HIRE_PRICE_MISMATCH"} if int(contract.coin) < list_price: return {"ok": false, "code": "HIRE_INSUFFICIENT_COIN"} var recruit: Dictionary = _materialize_market_recruit(offer) var company_before: int = company.owned_recruits.size() var coin_before: int = int(contract.coin) var receipt: Dictionary = { "operation_id": operation_id, "intent_hash": intent_hash, "company_id": String(company.company_id), "run_id": String(contract.run_id), "board_hash": String(contract.market_board.board_hash), "offer_id": offer_id, "recruit_id": String(recruit.recruit_id), "expected_price": expected_price, "list_price": list_price, "coin_before": coin_before, "coin_after": coin_before - list_price, "company_before": company_before, "company_after": company_before + 1, "line_capacity": int(contract.line_capacity), } receipt.receipt_hash = _hash(receipt) company.owned_recruits.append(recruit) contract.coin = coin_before - list_price contract.available_recruit_offer_ids.erase(offer_id) contract.hire_receipts.append(receipt) contract.erase("contract_hash") contract.contract_hash = _hash(contract) company.active_contract = contract company.company_revision = int(company.company_revision) + 1 company.erase("company_hash") company.company_hash = _hash(company) var saved: Dictionary = store.save_company(company) if not saved.ok: return saved return {"ok": true, "code": "hired", "company": saved.company, "receipt": receipt} func purchase_equipment(operation_id: String, offer_id: String, recruit_id: String, expected_price: int) -> Dictionary: if not _is_hex(operation_id, 32): return {"ok": false, "code": "EQUIPMENT_OPERATION_ID_INVALID"} if not _is_hex(offer_id, 32) or not _is_hex(recruit_id, 32): return {"ok": false, "code": "EQUIPMENT_TARGET_ID_INVALID"} var store := _store() var loaded: Dictionary = store.load_company() if not loaded.ok: return loaded var company: Dictionary = loaded.company.duplicate(true) if company.active_contract.is_empty() or String(company.active_contract.get("status", "")) != "market": return {"ok": false, "code": "EQUIPMENT_MARKET_REQUIRED"} var contract: Dictionary = company.active_contract var intent := { "company_id": String(company.company_id), "run_id": String(contract.run_id), "offer_id": offer_id, "recruit_id": recruit_id, "expected_price": expected_price, "board_hash": String(contract.market_board.board_hash), } var intent_hash := _hash(intent) var equipment_receipts: Array = contract.get("equipment_receipts", []) for stored_receipt in equipment_receipts: var prior: Dictionary = stored_receipt if String(prior.operation_id) == operation_id: if String(prior.intent_hash) != intent_hash: return {"ok": false, "code": "EQUIPMENT_OPERATION_CONFLICT"} return {"ok": true, "code": "already_equipped", "company": company, "receipt": prior} if String(prior.offer_id) == offer_id: if String(prior.intent_hash) == intent_hash: return {"ok": true, "code": "already_equipped", "company": company, "receipt": prior} return {"ok": false, "code": "EQUIPMENT_OFFER_ALREADY_APPLIED_CONFLICT"} var offer: Dictionary = {} for candidate in contract.market_board.get("equipment_offers", []): if String(candidate.get("offer_id", "")) == offer_id: offer = Dictionary(candidate).duplicate(true) break if offer.is_empty() or offer_id not in contract.available_equipment_offer_ids: return {"ok": false, "code": "EQUIPMENT_OFFER_UNAVAILABLE"} var list_price := int(offer.get("list_price", -1)) if expected_price != list_price: return {"ok": false, "code": "EQUIPMENT_PRICE_MISMATCH"} if int(contract.coin) < list_price: return {"ok": false, "code": "EQUIPMENT_INSUFFICIENT_COIN"} var target: Dictionary = {} for recruit in company.owned_recruits: if String(recruit.get("recruit_id", "")) == recruit_id: target = recruit break if target.is_empty(): return {"ok": false, "code": "EQUIPMENT_RECRUIT_UNAVAILABLE"} var previous_item_id := String(target.get("equipped_item_id", "")) var previous_item_name := Dictionary(target.get("starting_item_name", {})).duplicate(true) var item_record := { "catalog_id": String(offer.get("catalog_id", "")), "instance_id": String(offer.get("instance_id", offer.get("offer_id", ""))), "source_offer_id": offer_id, "list_price": list_price, "acquired_operation_id": operation_id, } var inventory: Array = company.get("equipment_inventory", []).duplicate(true) if not previous_item_id.is_empty(): inventory.append({"catalog_id": previous_item_id, "instance_id": ("%s|returned|%s" % [recruit_id, previous_item_id]).sha256_text().substr(0, 32), "source": "replaced_equipment", "name": previous_item_name}) var coin_before := int(contract.coin) var receipt := { "operation_id": operation_id, "intent_hash": intent_hash, "company_id": String(company.company_id), "run_id": String(contract.run_id), "offer_id": offer_id, "catalog_id": String(offer.catalog_id), "instance_id": String(item_record.instance_id), "recruit_id": recruit_id, "previous_equipped_item_id": previous_item_id, "new_equipped_item_id": String(offer.catalog_id), "coin_before": coin_before, "coin_after": coin_before - list_price, "list_price": list_price, } receipt.receipt_hash = _hash(receipt) target.equipped_item_id = String(offer.catalog_id) target.starting_item_state = "inventory" target.recruit_hash = _hash_without(target, "recruit_hash") company.equipment_inventory = inventory contract.coin = coin_before - list_price contract.available_equipment_offer_ids.erase(offer_id) equipment_receipts.append(receipt) contract.equipment_receipts = equipment_receipts contract.erase("contract_hash") contract.contract_hash = _hash(contract) company.active_contract = contract company.company_revision = int(company.company_revision) + 1 company.erase("company_hash") company.company_hash = _hash(company) var saved: Dictionary = store.save_company(company) if not saved.ok: return saved return {"ok": true, "code": "equipment_equipped", "company": saved.company, "receipt": receipt, "item": item_record} func dismiss_recruit(operation_id: String, recruit_id: String) -> Dictionary: if not _is_hex(operation_id, 32) or not _is_hex(recruit_id, 32): return {"ok": false, "code": "DISMISS_OPERATION_ID_INVALID"} var store := _store() var loaded: Dictionary = store.load_company() if not loaded.ok: return loaded var company: Dictionary = loaded.company.duplicate(true) var contract: Dictionary = company.active_contract if contract.is_empty() or String(contract.get("status", "")) != "market": return {"ok": false, "code": "DISMISS_MARKET_REQUIRED"} if contract.has("formation"): return {"ok": false, "code": "DISMISS_FORMATION_LOCKED"} var receipts: Array = company.get("dismissal_receipts", []) for stored in receipts: var prior: Dictionary = stored if String(prior.operation_id) == operation_id: if String(prior.recruit_id) != recruit_id: return {"ok": false, "code": "DISMISS_OPERATION_CONFLICT"} return {"ok": true, "code": "already_dismissed", "company": company, "receipt": prior} if company.owned_recruits.size() <= 1: return {"ok": false, "code": "FINAL_LIVE_RECRUIT"} var removed_index := -1 var removed: Dictionary = {} for index in range(company.owned_recruits.size()): var recruit: Dictionary = company.owned_recruits[index] if String(recruit.recruit_id) == recruit_id: removed_index = index removed = recruit break if removed_index < 0: return {"ok": false, "code": "DISMISS_RECRUIT_UNAVAILABLE"} var inventory: Array = company.get("equipment_inventory", []).duplicate(true) if not String(removed.get("equipped_item_id", "")).is_empty(): inventory.append({ "catalog_id": String(removed.equipped_item_id), "instance_id": ("%s|dismissed|%s" % [recruit_id, removed.equipped_item_id]).sha256_text().substr(0, 32), "source": "dismissed_recruit", }) var receipt := { "operation_id": operation_id, "company_id": String(company.company_id), "run_id": String(contract.run_id), "recruit_id": recruit_id, "removed_index": removed_index, "returned_equipped_item_id": String(removed.get("equipped_item_id", "")), "coin_delta": 0, "company_before": company.owned_recruits.size(), "company_after": company.owned_recruits.size() - 1, } receipt.intent_hash = _hash({"company_id": receipt.company_id, "run_id": receipt.run_id, "recruit_id": recruit_id, "coin_delta": 0}) receipt.receipt_hash = _hash(receipt) company.owned_recruits.remove_at(removed_index) company.equipment_inventory = inventory receipts.append(receipt) company.dismissal_receipts = receipts company.company_revision = int(company.company_revision) + 1 contract.dismissal_receipts = receipts.duplicate(true) contract.erase("contract_hash") contract.contract_hash = _hash(contract) company.active_contract = contract company.erase("company_hash") company.company_hash = _hash(company) var saved: Dictionary = store.save_company(company) if not saved.ok: return saved return {"ok": true, "code": "dismissed", "company": saved.company, "receipt": receipt} func return_contract(operation_id: String) -> Dictionary: if not _is_hex(operation_id, 32): return {"ok": false, "code": "CONTRACT_RETURN_OPERATION_ID_INVALID"} var store := _store() var loaded: Dictionary = store.load_company() if not loaded.ok: return loaded var company: Dictionary = loaded.company.duplicate(true) var contract: Dictionary = company.active_contract if contract.is_empty(): return {"ok": false, "code": "CONTRACT_RETURN_MARKET_REQUIRED"} var returns: Array = Array(contract.get("contract_return_receipts", [])).duplicate(true) var intent_hash := _hash({ "company_id": String(company.company_id), "run_id": String(contract.run_id), "contract_id": String(contract.contract_id), }) for stored in returns: var prior: Dictionary = stored if String(prior.operation_id) == operation_id: if String(prior.get("intent_hash", "")) != intent_hash: return {"ok": false, "code": "CONTRACT_RETURN_OPERATION_CONFLICT"} return {"ok": true, "code": "contract_return_already_applied", "company": company, "receipt": prior} if String(contract.get("status", "")) != "market": return {"ok": false, "code": "CONTRACT_RETURN_ALREADY_APPLIED" if String(contract.get("status", "")) == "returned" else "CONTRACT_RETURN_MARKET_REQUIRED"} if contract.has("formation"): return {"ok": false, "code": "CONTRACT_RETURN_FORMATION_LOCKED"} var receipt := { "operation_id": operation_id, "intent_hash": intent_hash, "company_id": String(company.company_id), "run_id": String(contract.run_id), "contract_id": String(contract.contract_id), "retained_coin": int(contract.coin), "retained_recruit_ids": _owned_recruit_ids(company), "destination": "guild", } receipt.receipt_hash = _hash(receipt) returns.append(receipt) contract.contract_return_receipts = returns contract.status = "returned" contract.erase("contract_hash") contract.contract_hash = _hash(contract) company.active_contract = contract company.company_revision = int(company.company_revision) + 1 company.erase("company_hash") company.company_hash = _hash(company) var saved: Dictionary = store.save_company(company) if not saved.ok: return saved return {"ok": true, "code": "contract_returned", "company": saved.company, "receipt": receipt} func _store() -> RefCounted: var store := DraftService.new() store.company_path = company_path return store func _find_recruit_offer(contract: Dictionary, offer_id: String) -> Dictionary: for offer in contract.market_board.recruit_offers: if String(offer.source_offer_id) == offer_id: return Dictionary(offer).duplicate(true) return {} func _materialize_market_recruit(offer: Dictionary) -> Dictionary: var professions := ContentRegistry.rows_by_id(_content, "professions") var traits := ContentRegistry.rows_by_id(_content, "traits") var equipment := ContentRegistry.rows_by_id(_content, "equipment") var profession: Dictionary = professions.get(String(offer.profession_id), {}) var trait_row: Dictionary = traits.get(String(offer.trait_id), {}) var item: Dictionary = equipment.get(String(offer.starting_equipment_id), {}) var name_tokens: Array = Array(offer.display_name_tokens).duplicate() var display_name := "%s %s" % [_name_token(name_tokens[0]), _name_token(name_tokens[1])] var recruit := { "recruit_id": String(offer.recruit_id), "generation_seed": String(offer.generation_seed), "name_component_ids": name_tokens, "rendered_name_snapshot": {"en": display_name, "zh_CN": display_name}, "appearance_seed": String(offer.appearance_seed), "profession_id": String(offer.profession_id), "profession_name": { "en": String(profession.get("name_en", profession.get("name", ""))), "zh_CN": String(profession.get("name_zh", "")), }, "profession_job": String(profession.get("role", "")), "attributes": _numeric_company_attributes(String(offer.generation_seed)), "trait_id": String(offer.trait_id), "trait_name": { "en": String(trait_row.get("name_en", trait_row.get("name", ""))), "zh_CN": String(trait_row.get("name_zh", "")), }, "trait_benefit": String(trait_row.get("benefit", "")), "trait_cost": String(trait_row.get("cost", "")), "starting_item_id": String(offer.starting_equipment_id), "starting_item_name": { "en": String(item.get("name_en", item.get("name", ""))), "zh_CN": String(item.get("name_zh", "")), }, "starting_item_state": "equipped", "equipped_item_id": String(offer.starting_equipment_id), "level": 1, "xp": 0, "state": "owned", "joined_from_market_id": String(offer.market_id), "market_offer_id": String(offer.source_offer_id), "market_offer_hash": String(offer.canonical_output_hash), "battle_history": {"battles_entered": 0, "victories": 0, "knockouts": 0}, } recruit.recruit_hash = _hash(recruit) return recruit func _numeric_company_attributes(seed: String) -> Dictionary: var result := {} for attribute_id in COMPANY_ATTRIBUTE_IDS: result[attribute_id] = 4 for point in range(15): var candidates := COMPANY_ATTRIBUTE_IDS.duplicate() candidates.sort_custom(func(left: String, right: String): return _digest(seed, "attribute-point", [point, left]) < _digest(seed, "attribute-point", [point, right]) ) for attribute_id in candidates: if int(result[attribute_id]) < 15: result[attribute_id] = int(result[attribute_id]) + 1 break return result func _name_token(token) -> String: var parts := String(token).split(".") return String(parts[parts.size() - 1]) func _price_first_board(raw: Dictionary) -> Dictionary: var board := raw.duplicate(true) for offer in board.recruit_offers: offer.offer_level = 1 offer.profession_tier = 0 offer.list_price = 14 for offer in board.equipment_offers: var ordinal := int(String(offer.catalog_id).trim_prefix("EQ-")) var item_tier := clampi(int((ordinal - 1) / 10), 0, 3) offer.item_tier = item_tier offer.region_band = 0 offer.list_price = 12 + 6 * item_tier for offer in board.battle_offers: offer.entry_cost = 0 board.erase("board_hash") board.board_hash = _hash(board) return board func _offer_ids(offers: Array, id_field: String) -> Array[String]: var result: Array[String] = [] for offer in offers: result.append(String(offer.get(id_field, ""))) return result func _is_hex(value: String, length: int) -> bool: if value.length() != length: return false for character in value: if character not in "0123456789abcdef": return false return true func _digest(seed: String, label: String, parts: Array = []) -> String: var framed: Array[String] = ["%d:%s" % [seed.length(), seed], "%d:%s" % [label.length(), label]] for part in parts: var value := str(part) framed.append("%d:%s" % [value.length(), value]) return "|".join(framed).sha256_text() func _low128(value: String) -> String: return value.substr(0, 32) func _hash(value) -> String: return JSON.stringify(_canonical(value)).sha256_text() func _hash_without(value: Dictionary, field: String) -> String: var copy := value.duplicate(true) copy.erase(field) return _hash(copy) func _owned_recruit_ids(company: Dictionary) -> Array[String]: var ids: Array[String] = [] for recruit in company.owned_recruits: ids.append(String(recruit.recruit_id)) return ids 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