Files
aetherbound-guild/runtime/entry/initial_company_draft_service.gd
T

1453 lines
65 KiB
GDScript

class_name ABGInitialCompanyDraftService
extends RefCounted
const ContentRegistry = preload("res://p10_0/content_registry.gd")
const OfferGenerator = preload("res://p10_0/offer_generator.gd")
const DRAFT_VERSION := 1
const DRAFT_KIND := "aetherbound_initial_company_draft"
const SOURCE_REVISION := "dev2-persistent-initial-company-r1"
const DEFAULT_PATH := "user://aetherbound_initial_company_draft.json"
const COMPANY_VERSION := 2
const LEGACY_COMPANY_VERSION := 1
const COMPANY_KIND := "aetherbound_expedition_company"
const DEFAULT_COMPANY_PATH := "user://aetherbound_expedition_company.json"
const PROVIDER_CONTRACT_ID := "CTR-R1-01"
const ATTRIBUTE_IDS := ["might", "finesse", "insight", "vigor", "resolve"]
var path := DEFAULT_PATH
var company_path := DEFAULT_COMPANY_PATH
var _content: Dictionary
var _generator: RefCounted
func _init() -> void:
_content = ContentRegistry.snapshot()
_generator = OfferGenerator.new(_content)
func begin_or_load(seed_override := "") -> Dictionary:
var company := load_company()
if company.ok:
return {"ok": false, "code": "COMPANY_ALREADY_CREATED", "company": company.company}
if String(company.code) != "COMPANY_MISSING":
return company
var loaded := load_draft()
if loaded.ok:
return loaded
if String(loaded.code) != "DRAFT_MISSING":
return loaded
var seed := String(seed_override)
if seed.is_empty():
seed = Crypto.new().generate_random_bytes(16).hex_encode()
if not _is_hex(seed, 32):
return {"ok": false, "code": "DRAFT_SEED_INVALID"}
var created := _create_draft(seed)
if not created.ok:
return created
var saved := _save_draft(created.draft)
if not saved.ok:
return saved
return {"ok": true, "code": "created", "draft": saved.draft}
func load_draft() -> Dictionary:
var primary := _read_valid(path)
if primary.ok:
return {"ok": true, "code": "loaded", "draft": primary.draft}
var backup_path := _backup_path()
var backup := _read_valid(backup_path)
if backup.ok:
var restored := _restore_primary(backup.draft, false)
if not restored.ok:
return restored
return {"ok": true, "code": "recovered_backup", "draft": backup.draft}
var candidate := _read_valid(_candidate_path())
if candidate.ok:
var restored := _restore_primary(candidate.draft, true)
if not restored.ok:
return restored
return {"ok": true, "code": "recovered_candidate", "draft": candidate.draft}
if (
not FileAccess.file_exists(path)
and not FileAccess.file_exists(backup_path)
and not FileAccess.file_exists(_candidate_path())
and not FileAccess.file_exists(_backup_candidate_path())
):
return {"ok": false, "code": "DRAFT_MISSING"}
return {"ok": false, "code": "DRAFT_CORRUPT"}
func set_selection(selected_offer_ids: Array) -> Dictionary:
var loaded := load_draft()
if not loaded.ok:
return loaded
var draft: Dictionary = loaded.draft.duplicate(true)
var normalized := _normalize_selection(selected_offer_ids, draft.offers)
if not normalized.ok:
return normalized
if draft.selected_offer_ids == normalized.ids:
return {"ok": true, "code": "selection_unchanged", "draft": draft}
draft.selected_offer_ids = normalized.ids
draft.selection_revision = int(draft.selection_revision) + 1
draft.status = "selection_ready" if normalized.ids.size() == 2 else "offers_ready"
_refresh_draft_hash(draft)
var saved := _save_draft(draft)
if not saved.ok:
return saved
return {"ok": true, "code": "selection_saved", "draft": saved.draft}
func commit_company() -> Dictionary:
var existing := load_company()
if existing.ok:
var lingering := load_draft()
if lingering.ok and String(existing.company.created_from_draft_id) != String(lingering.draft.draft_id):
return {"ok": false, "code": "COMPANY_DRAFT_CONFLICT"}
if lingering.ok:
_remove_draft_files()
return {"ok": true, "code": "already_committed", "company": existing.company}
if String(existing.code) != "COMPANY_MISSING":
return existing
var loaded := load_draft()
if not loaded.ok:
return loaded
var draft: Dictionary = loaded.draft
if draft.selected_offer_ids.size() != 2 or String(draft.status) != "selection_ready":
return {"ok": false, "code": "COMPANY_SELECTION_INCOMPLETE"}
var created := _create_company(draft)
if not created.ok:
return created
var saved := _save_company(created.company)
if not saved.ok:
return saved
var cleanup_ok := _remove_draft_files()
return {
"ok": true,
"code": "committed" if cleanup_ok else "committed_cleanup_pending",
"company": saved.company,
}
func load_company() -> Dictionary:
var primary := _read_valid_company(company_path)
if primary.ok:
if bool(primary.get("migrated", false)):
var migration_save := _save_company(primary.company)
if not migration_save.ok:
return migration_save
return {"ok": true, "code": "migrated_v1", "company": migration_save.company}
return {"ok": true, "code": "loaded", "company": primary.company}
var backup_path := _company_backup_path()
var backup := _read_valid_company(backup_path)
if backup.ok:
var restored := _restore_company_primary(backup.company, false)
if not restored.ok:
return restored
return {"ok": true, "code": "recovered_backup", "company": backup.company}
var candidate := _read_valid_company(_company_candidate_path())
if candidate.ok:
var restored := _restore_company_primary(candidate.company, not bool(candidate.get("migrated", false)))
if not restored.ok:
return restored
return {"ok": true, "code": "recovered_candidate", "company": candidate.company}
if (
not FileAccess.file_exists(company_path)
and not FileAccess.file_exists(backup_path)
and not FileAccess.file_exists(_company_candidate_path())
and not FileAccess.file_exists(_company_backup_candidate_path())
):
return {"ok": false, "code": "COMPANY_MISSING"}
return {"ok": false, "code": "COMPANY_CORRUPT"}
func save_company(company: Dictionary) -> Dictionary:
return _save_company(company)
func validate_company(value) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "COMPANY_NOT_DICTIONARY"}
var company: Dictionary = value
var required := [
"company_version", "company_kind", "company_id", "created_from_draft_id",
"source_offer_set_hash", "source_revision", "owned_recruits",
"creation_receipt", "company_revision", "next_contract_ordinal",
"active_contract", "contract_receipts", "company_hash",
]
for field in required:
if not company.has(field):
return {"ok": false, "code": "COMPANY_FIELD_MISSING", "field": field}
if int(company.company_version) != COMPANY_VERSION or String(company.company_kind) != COMPANY_KIND:
return {"ok": false, "code": "COMPANY_VERSION_UNSUPPORTED"}
if not _is_hex(String(company.company_id), 32) or not _is_hex(String(company.created_from_draft_id), 32):
return {"ok": false, "code": "COMPANY_IDENTITY_INVALID"}
if not _is_hex(String(company.source_offer_set_hash), 64) or String(company.source_revision) != SOURCE_REVISION:
return {"ok": false, "code": "COMPANY_SOURCE_INVALID"}
if int(company.company_revision) < 0 or int(company.next_contract_ordinal) < 0:
return {"ok": false, "code": "COMPANY_REVISION_INVALID"}
if not company.owned_recruits is Array or company.owned_recruits.size() < 1:
return {"ok": false, "code": "COMPANY_ROSTER_COUNT_INVALID"}
var recruit_ids: Array[String] = []
for recruit in company.owned_recruits:
var validation := _validate_owned_recruit(recruit, String(company.created_from_draft_id))
if not validation.ok:
return validation
var recruit_id := String(recruit.recruit_id)
if recruit_id in recruit_ids:
return {"ok": false, "code": "COMPANY_RECRUIT_ID_DUPLICATE"}
recruit_ids.append(recruit_id)
var receipt_validation := _validate_company_receipt(company.creation_receipt, company)
if not receipt_validation.ok:
return receipt_validation
if not company.active_contract is Dictionary or not company.contract_receipts is Array:
return {"ok": false, "code": "COMPANY_CONTRACT_STATE_INVALID"}
if company.active_contract.is_empty():
if not company.contract_receipts.is_empty() or int(company.next_contract_ordinal) != 0:
return {"ok": false, "code": "COMPANY_CONTRACT_STATE_INVALID"}
else:
if String(company.active_contract.get("company_id", "")) != String(company.company_id):
return {"ok": false, "code": "COMPANY_CONTRACT_IDENTITY_INVALID"}
var contract_validation := _validate_active_contract(company.active_contract, company)
if not contract_validation.ok:
return contract_validation
var hire_count: int = company.active_contract.hire_receipts.size()
var equipment_count: int = company.active_contract.get("equipment_receipts", []).size()
var dismissal_count: int = company.active_contract.get("dismissal_receipts", []).size()
var return_count: int = company.active_contract.get("contract_return_receipts", []).size()
if int(company.next_contract_ordinal) != 1 or int(company.company_revision) != 1 + hire_count + equipment_count + dismissal_count + return_count:
return {"ok": false, "code": "COMPANY_CONTRACT_REVISION_INVALID"}
if company.contract_receipts.size() != 1 or company.contract_receipts[0] != company.active_contract.creation_receipt:
return {"ok": false, "code": "COMPANY_CONTRACT_RECEIPT_INVALID"}
if company.owned_recruits.size() != 2 + hire_count - dismissal_count:
return {"ok": false, "code": "COMPANY_HIRE_ROSTER_INVALID"}
var dismissed_ids: Array[String] = []
for dismissal in company.active_contract.get("dismissal_receipts", []):
dismissed_ids.append(String(dismissal.get("recruit_id", "")))
for hire_receipt in company.active_contract.hire_receipts:
var hired_recruit: Dictionary = {}
for recruit in company.owned_recruits:
if String(recruit.recruit_id) == String(hire_receipt.recruit_id):
hired_recruit = recruit
break
if hired_recruit.is_empty():
if String(hire_receipt.recruit_id) not in dismissed_ids:
return {"ok": false, "code": "COMPANY_HIRE_ROSTER_INVALID"}
elif String(hired_recruit.get("market_offer_id", "")) != String(hire_receipt.offer_id):
return {"ok": false, "code": "COMPANY_HIRE_ROSTER_INVALID"}
var unhashed := company.duplicate(true)
unhashed.erase("company_hash")
if String(company.company_hash) != _hash(unhashed):
return {"ok": false, "code": "COMPANY_HASH_INVALID"}
for forbidden in ["offers", "unchosen_offer_ids", "current_run"]:
if company.has(forbidden):
return {"ok": false, "code": "COMPANY_FORBIDDEN_STATE"}
return {"ok": true, "code": "valid"}
func validate_draft(value) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "DRAFT_NOT_DICTIONARY"}
var draft: Dictionary = value
var required := [
"draft_version", "draft_kind", "draft_id", "seed", "status",
"source_revision", "provider_revision", "offers", "offer_set_hash",
"selected_offer_ids", "selection_revision", "draft_hash",
]
for field in required:
if not draft.has(field):
return {"ok": false, "code": "DRAFT_FIELD_MISSING", "field": field}
if int(draft.draft_version) != DRAFT_VERSION or String(draft.draft_kind) != DRAFT_KIND:
return {"ok": false, "code": "DRAFT_VERSION_UNSUPPORTED"}
if not _is_hex(String(draft.draft_id), 32) or not _is_hex(String(draft.seed), 32):
return {"ok": false, "code": "DRAFT_IDENTITY_INVALID"}
if String(draft.source_revision) != SOURCE_REVISION:
return {"ok": false, "code": "DRAFT_SOURCE_UNSUPPORTED"}
if String(draft.provider_revision) != String(_content.source.authority_revision):
return {"ok": false, "code": "DRAFT_PROVIDER_UNSUPPORTED"}
if String(draft.status) not in ["offers_ready", "selection_ready"]:
return {"ok": false, "code": "DRAFT_STATUS_INVALID"}
if not draft.offers is Array or draft.offers.size() != 4:
return {"ok": false, "code": "DRAFT_OFFER_COUNT_INVALID"}
var offer_ids: Array[String] = []
var recruit_ids: Array[String] = []
for index in range(4):
var offer_validation := _validate_offer(draft.offers[index], index, String(draft.draft_id))
if not offer_validation.ok:
return offer_validation
var offer: Dictionary = draft.offers[index]
var offer_id := String(offer.offer_id)
var recruit_id := String(offer.recruit_id)
if offer_id in offer_ids:
return {"ok": false, "code": "DRAFT_OFFER_ID_DUPLICATE"}
if recruit_id in recruit_ids:
return {"ok": false, "code": "DRAFT_RECRUIT_ID_DUPLICATE"}
offer_ids.append(offer_id)
recruit_ids.append(recruit_id)
if String(draft.offer_set_hash) != _hash(draft.offers):
return {"ok": false, "code": "DRAFT_OFFER_SET_HASH_INVALID"}
if not draft.selected_offer_ids is Array:
return {"ok": false, "code": "DRAFT_SELECTION_TYPE_INVALID"}
var selection := _normalize_selection(draft.selected_offer_ids, draft.offers)
if not selection.ok or selection.ids != draft.selected_offer_ids:
return {"ok": false, "code": String(selection.get("code", "DRAFT_SELECTION_INVALID"))}
if String(draft.status) == "selection_ready" and selection.ids.size() != 2:
return {"ok": false, "code": "DRAFT_SELECTION_STATUS_INVALID"}
if String(draft.status) == "offers_ready" and selection.ids.size() == 2:
return {"ok": false, "code": "DRAFT_SELECTION_STATUS_INVALID"}
if int(draft.selection_revision) < 0:
return {"ok": false, "code": "DRAFT_SELECTION_REVISION_INVALID"}
var unhashed := draft.duplicate(true)
unhashed.erase("draft_hash")
if String(draft.draft_hash) != _hash(unhashed):
return {"ok": false, "code": "DRAFT_HASH_INVALID"}
for forbidden in ["company", "expedition_company", "current_run", "active_contract"]:
if draft.has(forbidden):
return {"ok": false, "code": "DRAFT_PRECOMMIT_STATE_INVALID"}
return {"ok": true, "code": "valid"}
func _create_draft(seed: String) -> Dictionary:
var content_validation := ContentRegistry.validate(_content)
if not content_validation.ok:
return {"ok": false, "code": "DRAFT_CONTENT_INVALID"}
var draft_id := _low128(_digest(seed, "initial-company-draft"))
var role_weights := {}
for role in [
"front-hold", "single-pressure", "reach", "reorder", "projectile",
"area", "restore", "enable", "construct", "impair", "tempo", "supply",
]:
role_weights[role] = 1
var provider_draft := {
"draft_id": draft_id,
"run_seed": seed,
"contract_id": PROVIDER_CONTRACT_ID,
"difficulty": "standard",
"ruleset": SOURCE_REVISION,
"content_revision": String(_content.source.authority_revision),
"role_weights": role_weights,
"region_floor": 1,
"seed_reservation_applied": true,
"read_back_validated": true,
}
var generated: Dictionary = _generator.opening_offers(provider_draft)
if not generated.ok or generated.offers.size() != 4:
return {"ok": false, "code": "DRAFT_GENERATION_FAILED"}
var offers: Array[Dictionary] = []
for index in range(4):
offers.append(_project_offer(generated.offers[index], index, draft_id))
var draft := {
"draft_version": DRAFT_VERSION,
"draft_kind": DRAFT_KIND,
"draft_id": draft_id,
"seed": seed,
"status": "offers_ready",
"source_revision": SOURCE_REVISION,
"provider_revision": String(_content.source.authority_revision),
"offers": offers,
"offer_set_hash": _hash(offers),
"selected_offer_ids": [],
"selection_revision": 0,
}
_refresh_draft_hash(draft)
var validation := validate_draft(draft)
if not validation.ok:
return validation
return {"ok": true, "code": "draft_materialized", "draft": draft}
func _create_company(draft: Dictionary) -> Dictionary:
var owned_recruits: Array[Dictionary] = []
for offer in draft.offers:
if String(offer.offer_id) not in draft.selected_offer_ids:
continue
var recruit: Dictionary = Dictionary(offer).duplicate(true)
recruit.erase("offer_id")
recruit.erase("canonical_output_hash")
recruit["initial_offer_index"] = int(recruit.get("offer_index", -1))
recruit.erase("offer_index")
recruit.erase("source_draft_id")
recruit["joined_from_draft_id"] = String(draft.draft_id)
recruit["equipped_item_id"] = String(recruit.starting_item_id)
recruit["state"] = "owned"
recruit["battle_history"] = {
"battles_entered": 0,
"victories": 0,
"knockouts": 0,
}
recruit["recruit_hash"] = _hash(recruit)
owned_recruits.append(recruit)
if owned_recruits.size() != 2:
return {"ok": false, "code": "COMPANY_SELECTION_MATERIALIZATION_FAILED"}
var selected_recruit_ids: Array[String] = []
for recruit in owned_recruits:
selected_recruit_ids.append(String(recruit.recruit_id))
var intent := {
"draft_id": String(draft.draft_id),
"offer_set_hash": String(draft.offer_set_hash),
"selected_recruit_ids": selected_recruit_ids,
}
var intent_hash := _hash(intent)
var company_id := _low128(_digest(String(draft.draft_id), "persistent-expedition-company", [intent_hash]))
var receipt := {
"operation_id": _low128(_digest(String(draft.draft_id), "create-company-operation", [intent_hash])),
"intent_hash": intent_hash,
"draft_id": String(draft.draft_id),
"company_id": company_id,
"selected_recruit_ids": selected_recruit_ids,
}
receipt["receipt_hash"] = _hash(receipt)
var company := {
"company_version": COMPANY_VERSION,
"company_kind": COMPANY_KIND,
"company_id": company_id,
"created_from_draft_id": String(draft.draft_id),
"source_offer_set_hash": String(draft.offer_set_hash),
"source_revision": SOURCE_REVISION,
"owned_recruits": owned_recruits,
"equipment_inventory": [],
"creation_receipt": receipt,
"company_revision": 0,
"next_contract_ordinal": 0,
"active_contract": {},
"contract_receipts": [],
}
company["company_hash"] = _hash(company)
var validation := validate_company(company)
if not validation.ok:
return validation
return {"ok": true, "code": "company_materialized", "company": company}
func _validate_owned_recruit(value, draft_id: String) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "COMPANY_RECRUIT_TYPE_INVALID"}
var recruit: Dictionary = value
var required := [
"recruit_id", "generation_seed", "name_component_ids",
"rendered_name_snapshot", "appearance_seed", "profession_id",
"profession_name", "profession_job", "attributes", "trait_id",
"trait_name", "trait_benefit", "trait_cost", "starting_item_id",
"starting_item_name", "starting_item_state", "equipped_item_id",
"level", "xp", "state", "battle_history", "recruit_hash",
]
for field in required:
if not recruit.has(field):
return {"ok": false, "code": "COMPANY_RECRUIT_FIELD_MISSING", "field": field}
if not _is_hex(String(recruit.recruit_id), 32) or not _is_hex(String(recruit.generation_seed), 32):
return {"ok": false, "code": "COMPANY_RECRUIT_ID_INVALID"}
if String(recruit.state) != "owned":
return {"ok": false, "code": "COMPANY_RECRUIT_STATE_INVALID"}
if recruit.has("joined_from_market_id"):
for field in ["joined_from_market_id", "market_offer_id", "market_offer_hash"]:
if not recruit.has(field):
return {"ok": false, "code": "COMPANY_RECRUIT_FIELD_MISSING", "field": field}
if (
String(recruit.joined_from_market_id).is_empty()
or not _is_hex(String(recruit.market_offer_id), 32)
or not _is_hex(String(recruit.market_offer_hash), 64)
or recruit.has("joined_from_draft_id")
or recruit.has("initial_offer_index")
):
return {"ok": false, "code": "COMPANY_RECRUIT_SOURCE_INVALID"}
else:
if not recruit.has("joined_from_draft_id") or not recruit.has("initial_offer_index"):
return {"ok": false, "code": "COMPANY_RECRUIT_SOURCE_INVALID"}
if String(recruit.joined_from_draft_id) != draft_id or int(recruit.initial_offer_index) < 0 or int(recruit.initial_offer_index) > 3:
return {"ok": false, "code": "COMPANY_RECRUIT_SOURCE_INVALID"}
var starting_item_state := String(recruit.starting_item_state)
if starting_item_state not in ["equipped", "inventory"] or not _catalog_id(String(recruit.equipped_item_id), "EQ-", 3):
return {"ok": false, "code": "COMPANY_RECRUIT_ITEM_INVALID"}
if (starting_item_state == "equipped") != (String(recruit.equipped_item_id) == String(recruit.starting_item_id)):
return {"ok": false, "code": "COMPANY_RECRUIT_ITEM_INVALID"}
var recruit_level := int(recruit.level)
var recruit_xp := int(recruit.xp)
if recruit_level < 1 or recruit_level > 10 or recruit_xp < 0:
return {"ok": false, "code": "COMPANY_RECRUIT_GROWTH_INVALID"}
if recruit_level < 10 and recruit_xp >= 20 + 10 * recruit_level:
return {"ok": false, "code": "COMPANY_RECRUIT_GROWTH_INVALID"}
if not recruit.attributes is Dictionary:
return {"ok": false, "code": "COMPANY_RECRUIT_ATTRIBUTES_INVALID"}
var total := 0
for attribute_id in ATTRIBUTE_IDS:
if not recruit.attributes.has(attribute_id):
return {"ok": false, "code": "COMPANY_RECRUIT_ATTRIBUTES_INVALID"}
total += int(recruit.attributes[attribute_id])
if total != 35:
return {"ok": false, "code": "COMPANY_RECRUIT_ATTRIBUTES_INVALID"}
if not recruit.battle_history is Dictionary:
return {"ok": false, "code": "COMPANY_RECRUIT_HISTORY_INVALID"}
for field in ["battles_entered", "victories", "knockouts"]:
if int(recruit.battle_history.get(field, -1)) < 0:
return {"ok": false, "code": "COMPANY_RECRUIT_HISTORY_INVALID"}
var unhashed := recruit.duplicate(true)
unhashed.erase("recruit_hash")
if String(recruit.recruit_hash) != _hash(unhashed):
return {"ok": false, "code": "COMPANY_RECRUIT_HASH_INVALID"}
for forbidden in ["offer_id", "source_offer_id", "source_draft_id", "canonical_output_hash"]:
if recruit.has(forbidden):
return {"ok": false, "code": "COMPANY_RECRUIT_OFFER_STATE_RETAINED"}
return {"ok": true, "code": "valid"}
func _validate_company_receipt(value, company: Dictionary) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "COMPANY_RECEIPT_TYPE_INVALID"}
var receipt: Dictionary = value
for field in ["operation_id", "intent_hash", "draft_id", "company_id", "selected_recruit_ids", "receipt_hash"]:
if not receipt.has(field):
return {"ok": false, "code": "COMPANY_RECEIPT_FIELD_MISSING", "field": field}
if not _is_hex(String(receipt.operation_id), 32) or not _is_hex(String(receipt.intent_hash), 64):
return {"ok": false, "code": "COMPANY_RECEIPT_ID_INVALID"}
if String(receipt.draft_id) != String(company.created_from_draft_id) or String(receipt.company_id) != String(company.company_id):
return {"ok": false, "code": "COMPANY_RECEIPT_SOURCE_INVALID"}
if company.owned_recruits.size() < 1:
return {"ok": false, "code": "COMPANY_RECEIPT_ROSTER_INVALID"}
var expected_ids: Array[String] = []
for selected_id in receipt.selected_recruit_ids:
expected_ids.append(String(selected_id))
if expected_ids.size() != 2:
return {"ok": false, "code": "COMPANY_RECEIPT_ROSTER_INVALID"}
var current_ids: Array[String] = []
for recruit in company.owned_recruits:
current_ids.append(String(recruit.recruit_id))
var dismissed_ids: Array[String] = []
for dismissal in company.get("dismissal_receipts", []):
dismissed_ids.append(String(dismissal.get("recruit_id", "")))
for selected_id in expected_ids:
if selected_id not in current_ids and selected_id not in dismissed_ids:
return {"ok": false, "code": "COMPANY_RECEIPT_ROSTER_INVALID"}
var expected_intent := _hash({
"draft_id": String(company.created_from_draft_id),
"offer_set_hash": String(company.source_offer_set_hash),
"selected_recruit_ids": expected_ids,
})
if String(receipt.intent_hash) != expected_intent:
return {"ok": false, "code": "COMPANY_RECEIPT_INTENT_INVALID"}
var unhashed := receipt.duplicate(true)
unhashed.erase("receipt_hash")
if String(receipt.receipt_hash) != _hash(unhashed):
return {"ok": false, "code": "COMPANY_RECEIPT_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_active_contract(value: Dictionary, company: Dictionary) -> Dictionary:
var required := [
"contract_version", "company_id", "contract_id", "run_id", "run_seed", "difficulty",
"status", "market_index", "coin", "line_capacity", "market_board",
"available_recruit_offer_ids", "available_equipment_offer_ids",
"available_battle_offer_ids", "hire_receipts", "creation_receipt",
"contract_hash",
]
for field in required:
if not value.has(field):
return {"ok": false, "code": "CONTRACT_FIELD_MISSING", "field": field}
if int(value.contract_version) != 1:
return {"ok": false, "code": "CONTRACT_VERSION_UNSUPPORTED"}
if String(value.contract_id) != "CTR-R1-01" or not _is_hex(String(value.run_id), 32) or not _is_hex(String(value.run_seed), 32):
return {"ok": false, "code": "CONTRACT_IDENTITY_INVALID"}
if String(value.difficulty) not in ["wayfinder", "standard", "stormbound", "iron_oath"]:
return {"ok": false, "code": "CONTRACT_DIFFICULTY_INVALID"}
if String(value.status) not in ["market", "battle", "result", "recovery", "returned"] or int(value.market_index) != 1:
return {"ok": false, "code": "CONTRACT_PHASE_INVALID"}
if int(value.coin) < 0 or int(value.coin) > 999 or int(value.line_capacity) != 4:
return {"ok": false, "code": "CONTRACT_RESOURCE_INVALID"}
if not value.market_board is Dictionary:
return {"ok": false, "code": "CONTRACT_MARKET_INVALID"}
var board_validation := _validate_first_market_board(value.market_board, value)
if not board_validation.ok:
return board_validation
for field in ["available_recruit_offer_ids", "available_equipment_offer_ids", "available_battle_offer_ids", "hire_receipts"]:
if not value[field] is Array:
return {"ok": false, "code": "CONTRACT_COLLECTION_INVALID", "field": field}
var id_fields := {
"available_recruit_offer_ids": [value.market_board.recruit_offers, "source_offer_id"],
"available_equipment_offer_ids": [value.market_board.equipment_offers, "offer_id"],
"available_battle_offer_ids": [value.market_board.battle_offers, "offer_id"],
}
for field in id_fields:
var authority: Array[String] = []
for offer in id_fields[field][0]:
authority.append(String(offer.get(id_fields[field][1], "")))
var seen: Array[String] = []
for offer_id in value[field]:
if String(offer_id) not in authority or String(offer_id) in seen:
return {"ok": false, "code": "CONTRACT_AVAILABLE_OFFER_INVALID", "field": field}
seen.append(String(offer_id))
if value.hire_receipts.size() > 2:
return {"ok": false, "code": "CONTRACT_HIRE_LIMIT_INVALID"}
var hired_offer_ids: Array[String] = []
var hired_recruit_ids: Array[String] = []
for index in range(value.hire_receipts.size()):
var hire_validation := _validate_hire_receipt(value.hire_receipts[index], value, index)
if not hire_validation.ok:
return hire_validation
var receipt: Dictionary = value.hire_receipts[index]
if String(receipt.offer_id) in hired_offer_ids or String(receipt.recruit_id) in hired_recruit_ids:
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_DUPLICATE"}
if String(receipt.offer_id) in value.available_recruit_offer_ids:
return {"ok": false, "code": "CONTRACT_HIRED_OFFER_AVAILABLE"}
hired_offer_ids.append(String(receipt.offer_id))
hired_recruit_ids.append(String(receipt.recruit_id))
var opening_coin: int = 52 - 14 * value.hire_receipts.size()
var equipment_receipts: Array = value.get("equipment_receipts", [])
var equipment_spend := 0
for equipment_receipt in equipment_receipts:
equipment_spend += int(equipment_receipt.get("list_price", 0))
var market_floor_coin := opening_coin - equipment_spend
var has_battle_progress: bool = value.has("battle_state") or (value.has("battle_settlement_receipts") and not value.battle_settlement_receipts.is_empty())
if (
value.available_recruit_offer_ids.size() != 3 - value.hire_receipts.size()
or value.available_equipment_offer_ids.size() != 4 - equipment_receipts.size()
or value.available_battle_offer_ids.size() != 3
or market_floor_coin < 0
or (not has_battle_progress and int(value.coin) != market_floor_coin)
or (has_battle_progress and int(value.coin) < market_floor_coin)
):
return {"ok": false, "code": "CONTRACT_OPENING_STATE_INVALID"}
var receipt_validation := _validate_contract_creation_receipt(value.creation_receipt, value)
if not receipt_validation.ok:
return receipt_validation
if value.has("contract_return_receipts") and not value.contract_return_receipts is Array:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPTS_INVALID"}
var return_receipts: Array = Array(value.get("contract_return_receipts", []))
if return_receipts.size() > 1:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_DUPLICATE"}
for return_receipt in return_receipts:
var return_validation := _validate_contract_return_receipt(return_receipt, value, company)
if not return_validation.ok:
return return_validation
if String(value.status) == "returned":
if return_receipts.size() != 1:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_MISSING"}
elif not return_receipts.is_empty():
return {"ok": false, "code": "CONTRACT_RETURN_STATUS_INVALID"}
if value.has("formation"):
var formation_validation := _validate_formation(value.formation, value, company)
if not formation_validation.ok:
return formation_validation
var unhashed := value.duplicate(true)
unhashed.erase("contract_hash")
if String(value.contract_hash) != _hash(unhashed):
return {"ok": false, "code": "CONTRACT_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_formation(value, contract: Dictionary, company: Dictionary) -> Dictionary:
if not value is Dictionary or value.is_empty():
return {"ok": false, "code": "FORMATION_STATE_INVALID"}
var formation: Dictionary = value
var required := [
"foundation_version", "line_capacity", "orientation", "front_position",
"status", "base_front_to_rear_ids", "draft_front_to_rear_ids",
"saved_front_to_rear_ids", "order_revision", "relations", "coverage",
"first_threat", "risk_options", "order_receipts", "readiness_receipts",
"accepted_party_snapshot", "formation_hash",
]
for field in required:
if not formation.has(field):
return {"ok": false, "code": "FORMATION_FIELD_MISSING", "field": field}
if (
int(formation.foundation_version) != 1
or int(formation.line_capacity) != int(contract.line_capacity)
or String(formation.orientation) != "rear_left_front_right"
or int(formation.front_position) != 0
or String(formation.status) not in ["draft", "ordered", "ready"]
or int(formation.order_revision) < 0
):
return {"ok": false, "code": "FORMATION_HEADER_INVALID"}
if company.owned_recruits.size() != int(contract.line_capacity) or contract.hire_receipts.size() != 2:
return {"ok": false, "code": "FORMATION_ROSTER_STATE_INVALID"}
var owned_ids: Array[String] = []
for recruit in company.owned_recruits:
owned_ids.append(String(recruit.recruit_id))
for field in ["base_front_to_rear_ids", "draft_front_to_rear_ids"]:
if not _is_exact_id_permutation(formation[field], owned_ids):
return {"ok": false, "code": "FORMATION_ORDER_INVALID", "field": field}
if not formation.saved_front_to_rear_ids is Array:
return {"ok": false, "code": "FORMATION_ORDER_INVALID", "field": "saved_front_to_rear_ids"}
if not formation.saved_front_to_rear_ids.is_empty() and not _is_exact_id_permutation(formation.saved_front_to_rear_ids, owned_ids):
return {"ok": false, "code": "FORMATION_ORDER_INVALID", "field": "saved_front_to_rear_ids"}
if not formation.relations is Array or formation.relations.size() != int(contract.line_capacity):
return {"ok": false, "code": "FORMATION_RELATIONS_INVALID"}
var relation_ids: Array[String] = []
for position in range(formation.relations.size()):
var relation = formation.relations[position]
if not relation is Dictionary:
return {"ok": false, "code": "FORMATION_RELATION_INVALID"}
for field in ["position", "recruit_id", "profession_id", "exposure_id", "expected_target_rank", "protected_by_recruit_ids", "protects_recruit_ids", "reach_id", "can_reach_first_threat", "first_threat_target", "relation_hash"]:
if not relation.has(field):
return {"ok": false, "code": "FORMATION_RELATION_FIELD_MISSING", "field": field}
if int(relation.position) != position or String(relation.recruit_id) != String(formation.draft_front_to_rear_ids[position]):
return {"ok": false, "code": "FORMATION_RELATION_POSITION_INVALID"}
if String(relation.recruit_id) in relation_ids:
return {"ok": false, "code": "FORMATION_RELATION_DUPLICATE"}
relation_ids.append(String(relation.recruit_id))
var unhashed_relation: Dictionary = relation.duplicate(true)
unhashed_relation.erase("relation_hash")
if String(relation.relation_hash) != _hash(unhashed_relation):
return {"ok": false, "code": "FORMATION_RELATION_HASH_INVALID"}
if not formation.coverage is Dictionary:
return {"ok": false, "code": "FORMATION_COVERAGE_INVALID"}
for field in ["protection", "long_reach", "sustain", "control", "coverage_hash"]:
if not formation.coverage.has(field):
return {"ok": false, "code": "FORMATION_COVERAGE_FIELD_MISSING", "field": field}
var unhashed_coverage: Dictionary = formation.coverage.duplicate(true)
unhashed_coverage.erase("coverage_hash")
if String(formation.coverage.coverage_hash) != _hash(unhashed_coverage):
return {"ok": false, "code": "FORMATION_COVERAGE_HASH_INVALID"}
if not formation.first_threat is Dictionary or not formation.risk_options is Array or formation.risk_options.size() != 3:
return {"ok": false, "code": "FORMATION_RISK_STATE_INVALID"}
for risk in range(formation.risk_options.size()):
var option = formation.risk_options[risk]
if not option is Dictionary:
return {"ok": false, "code": "FORMATION_RISK_OPTION_INVALID"}
for field in ["risk_option_version", "risk_option_id", "battle_offer_hash", "risk", "tier_id", "enemy_ids", "known_threat", "uncertainty_id", "entry_cost", "reward_coin", "reward_tier", "reward_class_id", "recovery_exposure_id", "knockout_rule_id", "option_hash"]:
if not option.has(field):
return {"ok": false, "code": "FORMATION_RISK_FIELD_MISSING", "field": field}
var battle_offer: Dictionary = contract.market_board.battle_offers[risk]
if (
int(option.risk_option_version) != 1
or int(option.risk) != risk
or String(option.risk_option_id) != String(battle_offer.offer_id)
or String(option.battle_offer_hash) != String(battle_offer.offer_hash)
or String(option.knockout_rule_id) != "returns_next_stage"
):
return {"ok": false, "code": "FORMATION_RISK_IDENTITY_INVALID"}
var unhashed_option: Dictionary = option.duplicate(true)
unhashed_option.erase("option_hash")
if String(option.option_hash) != _hash(unhashed_option):
return {"ok": false, "code": "FORMATION_RISK_HASH_INVALID"}
if formation.first_threat != formation.risk_options[0].known_threat:
return {"ok": false, "code": "FORMATION_FIRST_THREAT_INVALID"}
for field in ["order_receipts", "readiness_receipts"]:
if not formation[field] is Array:
return {"ok": false, "code": "FORMATION_RECEIPTS_INVALID", "field": field}
for receipt in formation.order_receipts:
if not _validate_formation_receipt(receipt, company, contract, "order").ok:
return {"ok": false, "code": "FORMATION_ORDER_RECEIPT_INVALID"}
for receipt in formation.readiness_receipts:
if not _validate_formation_receipt(receipt, company, contract, "readiness").ok:
return {"ok": false, "code": "FORMATION_READINESS_RECEIPT_INVALID"}
if String(formation.status) == "draft":
if not formation.saved_front_to_rear_ids.is_empty() or not formation.order_receipts.is_empty() or not formation.readiness_receipts.is_empty() or not formation.accepted_party_snapshot.is_empty():
return {"ok": false, "code": "FORMATION_DRAFT_STATE_INVALID"}
elif formation.saved_front_to_rear_ids.is_empty() or formation.order_receipts.is_empty():
return {"ok": false, "code": "FORMATION_SAVED_STATE_INVALID"}
if String(formation.status) == "ready":
if formation.readiness_receipts.size() != 1 or not _validate_party_snapshot(formation.accepted_party_snapshot, formation, company, contract).ok:
return {"ok": false, "code": "FORMATION_READY_STATE_INVALID"}
elif not formation.readiness_receipts.is_empty() or not formation.accepted_party_snapshot.is_empty():
return {"ok": false, "code": "FORMATION_PREMATURE_READY_STATE"}
var unhashed: Dictionary = formation.duplicate(true)
unhashed.erase("formation_hash")
if String(formation.formation_hash) != _hash(unhashed):
return {"ok": false, "code": "FORMATION_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_formation_receipt(value, company: Dictionary, contract: Dictionary, kind: String) -> Dictionary:
if not value is Dictionary:
return {"ok": false}
var receipt: Dictionary = value
var fields := ["operation_id", "intent_hash", "company_id", "run_id", "order_hash", "order_revision", "receipt_hash"]
if kind == "order":
fields.append_array(["front_to_rear_ids", "line_capacity", "relations_hash", "coverage_hash"])
else:
fields.append_array(["snapshot_id", "snapshot_hash"])
for field in fields:
if not receipt.has(field):
return {"ok": false}
if (
not _is_hex(String(receipt.operation_id), 32)
or not _is_hex(String(receipt.intent_hash), 64)
or not _is_hex(String(receipt.order_hash), 64)
or String(receipt.company_id) != String(company.company_id)
or String(receipt.run_id) != String(contract.run_id)
):
return {"ok": false}
var unhashed: Dictionary = receipt.duplicate(true)
unhashed.erase("receipt_hash")
return {"ok": String(receipt.receipt_hash) == _hash(unhashed)}
func _validate_party_snapshot(value, formation: Dictionary, company: Dictionary, contract: Dictionary) -> Dictionary:
if not value is Dictionary:
return {"ok": false}
var snapshot: Dictionary = value
for field in ["snapshot_id", "company_id", "run_id", "front_to_rear_ids", "order_hash", "order_revision", "line_capacity", "orientation", "front_position", "relations", "coverage", "risk_option_ids", "snapshot_hash"]:
if not snapshot.has(field):
return {"ok": false}
if (
not _is_hex(String(snapshot.snapshot_id), 32)
or String(snapshot.company_id) != String(company.company_id)
or String(snapshot.run_id) != String(contract.run_id)
or snapshot.front_to_rear_ids != formation.saved_front_to_rear_ids
or String(snapshot.order_hash) != _hash(snapshot.front_to_rear_ids)
or int(snapshot.order_revision) != int(formation.order_revision)
or int(snapshot.line_capacity) != int(contract.line_capacity)
or String(snapshot.orientation) != "rear_left_front_right"
or int(snapshot.front_position) != 0
):
return {"ok": false}
var unhashed: Dictionary = snapshot.duplicate(true)
unhashed.erase("snapshot_hash")
return {"ok": String(snapshot.snapshot_hash) == _hash(unhashed)}
func _is_exact_id_permutation(value, expected: Array[String]) -> bool:
if not value is Array or value.size() != expected.size():
return false
var actual: Array[String] = []
for item in value:
var recruit_id := String(item)
if recruit_id in actual:
return false
actual.append(recruit_id)
actual.sort()
var sorted_expected := expected.duplicate()
sorted_expected.sort()
return actual == sorted_expected
func _validate_first_market_board(board: Dictionary, contract: Dictionary) -> Dictionary:
var required := [
"board_id", "board_hash", "run_seed", "contract_id", "market_index",
"refresh_index", "recruit_offers", "equipment_offers", "battle_offers",
]
for field in required:
if not board.has(field):
return {"ok": false, "code": "CONTRACT_MARKET_FIELD_MISSING", "field": field}
if (
String(board.run_seed) != String(contract.run_seed)
or String(board.contract_id) != String(contract.contract_id)
or int(board.market_index) != 1
or int(board.refresh_index) != 0
or not _is_hex(String(board.board_id), 32)
):
return {"ok": false, "code": "CONTRACT_MARKET_IDENTITY_INVALID"}
if not board.recruit_offers is Array or board.recruit_offers.size() != 3:
return {"ok": false, "code": "CONTRACT_RECRUIT_OFFER_COUNT_INVALID"}
if not board.equipment_offers is Array or board.equipment_offers.size() != 4:
return {"ok": false, "code": "CONTRACT_EQUIPMENT_OFFER_COUNT_INVALID"}
if not board.battle_offers is Array or board.battle_offers.size() != 3:
return {"ok": false, "code": "CONTRACT_BATTLE_OFFER_COUNT_INVALID"}
var ids: Array[String] = []
for offer in board.recruit_offers:
for field in ["source_offer_id", "recruit_id", "profession_id", "trait_id", "starting_equipment_id", "canonical_output_hash", "list_price", "offer_level"]:
if not offer.has(field):
return {"ok": false, "code": "CONTRACT_RECRUIT_OFFER_INVALID", "field": field}
var offer_id := String(offer.source_offer_id)
if not _is_hex(offer_id, 32) or offer_id in ids or int(offer.list_price) != 14 or int(offer.offer_level) != 1:
return {"ok": false, "code": "CONTRACT_RECRUIT_OFFER_INVALID"}
ids.append(offer_id)
for collection in [board.equipment_offers, board.battle_offers]:
for offer in collection:
var offer_id := String(offer.get("offer_id", ""))
if not _is_hex(offer_id, 32) or offer_id in ids:
return {"ok": false, "code": "CONTRACT_OFFER_ID_INVALID"}
ids.append(offer_id)
var unhashed := board.duplicate(true)
unhashed.erase("board_hash")
if String(board.board_hash) != _hash(unhashed):
return {"ok": false, "code": "CONTRACT_MARKET_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_contract_creation_receipt(value, contract: Dictionary) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "CONTRACT_RECEIPT_TYPE_INVALID"}
var receipt: Dictionary = value
for field in ["operation_id", "intent_hash", "company_id", "contract_ordinal", "contract_id", "difficulty", "run_id", "run_seed", "board_hash", "coin", "line_capacity", "receipt_hash"]:
if not receipt.has(field):
return {"ok": false, "code": "CONTRACT_RECEIPT_FIELD_MISSING", "field": field}
if not _is_hex(String(receipt.operation_id), 32) or not _is_hex(String(receipt.intent_hash), 64):
return {"ok": false, "code": "CONTRACT_RECEIPT_ID_INVALID"}
if (
String(receipt.company_id) != String(contract.company_id)
or int(receipt.contract_ordinal) != 0
or String(receipt.contract_id) != String(contract.contract_id)
or String(receipt.difficulty) != String(contract.difficulty)
or String(receipt.run_id) != String(contract.run_id)
or String(receipt.run_seed) != String(contract.run_seed)
or String(receipt.board_hash) != String(contract.market_board.board_hash)
or int(receipt.coin) != 52
or int(receipt.line_capacity) != 4
):
return {"ok": false, "code": "CONTRACT_RECEIPT_STATE_INVALID"}
var expected_intent := _hash({
"company_id": String(contract.company_id),
"contract_ordinal": 0,
"contract_id": String(contract.contract_id),
"difficulty": String(contract.difficulty),
})
if String(receipt.intent_hash) != expected_intent:
return {"ok": false, "code": "CONTRACT_RECEIPT_INTENT_INVALID"}
var unhashed: Dictionary = receipt.duplicate(true)
unhashed.erase("receipt_hash")
if String(receipt.receipt_hash) != _hash(unhashed):
return {"ok": false, "code": "CONTRACT_RECEIPT_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_hire_receipt(value, contract: Dictionary, index: int) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_TYPE_INVALID"}
var receipt: Dictionary = value
var required := [
"operation_id", "intent_hash", "company_id", "run_id", "board_hash",
"offer_id", "recruit_id", "expected_price", "list_price",
"coin_before", "coin_after", "company_before", "company_after",
"line_capacity", "receipt_hash",
]
for field in required:
if not receipt.has(field):
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_FIELD_MISSING", "field": field}
if not _is_hex(String(receipt.operation_id), 32) or not _is_hex(String(receipt.intent_hash), 64):
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_ID_INVALID"}
var offer: Dictionary = {}
for candidate in contract.market_board.recruit_offers:
if String(candidate.source_offer_id) == String(receipt.offer_id):
offer = candidate
break
if offer.is_empty() or String(offer.recruit_id) != String(receipt.recruit_id):
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_OFFER_INVALID"}
if (
String(receipt.company_id) != String(contract.company_id)
or String(receipt.run_id) != String(contract.run_id)
or String(receipt.board_hash) != String(contract.market_board.board_hash)
or int(receipt.expected_price) != 14
or int(receipt.list_price) != 14
or int(receipt.coin_before) != 52 - 14 * index
or int(receipt.coin_after) != 38 - 14 * index
or int(receipt.company_before) != 2 + index
or int(receipt.company_after) != 3 + index
or int(receipt.line_capacity) != 4
):
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_STATE_INVALID"}
var expected_intent := _hash({
"company_id": String(contract.company_id),
"run_id": String(contract.run_id),
"board_hash": String(contract.market_board.board_hash),
"offer_id": String(receipt.offer_id),
"expected_price": 14,
})
if String(receipt.intent_hash) != expected_intent:
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_INTENT_INVALID"}
var unhashed: Dictionary = receipt.duplicate(true)
unhashed.erase("receipt_hash")
if String(receipt.receipt_hash) != _hash(unhashed):
return {"ok": false, "code": "CONTRACT_HIRE_RECEIPT_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _validate_contract_return_receipt(value, contract: Dictionary, company: Dictionary) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_TYPE_INVALID"}
var receipt: Dictionary = value
for field in [
"operation_id", "intent_hash", "company_id", "run_id", "contract_id",
"retained_coin", "retained_recruit_ids", "destination", "receipt_hash",
]:
if not receipt.has(field):
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_FIELD_MISSING", "field": field}
if (
not _is_hex(String(receipt.operation_id), 32)
or not _is_hex(String(receipt.intent_hash), 64)
or not _is_hex(String(receipt.receipt_hash), 64)
):
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_ID_INVALID"}
if (
String(receipt.company_id) != String(company.company_id)
or String(receipt.run_id) != String(contract.run_id)
or String(receipt.contract_id) != String(contract.contract_id)
or String(receipt.destination) != "guild"
or int(receipt.retained_coin) != int(contract.coin)
or int(receipt.retained_coin) < 0
):
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_STATE_INVALID"}
if not receipt.retained_recruit_ids is Array:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_ROSTER_INVALID"}
var expected_ids: Array[String] = []
for recruit in company.owned_recruits:
expected_ids.append(String(recruit.recruit_id))
var retained_ids: Array[String] = []
for retained_id in receipt.retained_recruit_ids:
var recruit_id := String(retained_id)
if not _is_hex(recruit_id, 32) or recruit_id in retained_ids:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_ROSTER_INVALID"}
retained_ids.append(recruit_id)
if retained_ids != expected_ids:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_ROSTER_INVALID"}
var expected_intent := _hash({
"company_id": String(company.company_id),
"run_id": String(contract.run_id),
"contract_id": String(contract.contract_id),
})
if String(receipt.intent_hash) != expected_intent:
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_INTENT_INVALID"}
var unhashed: Dictionary = receipt.duplicate(true)
unhashed.erase("receipt_hash")
if String(receipt.receipt_hash) != _hash(unhashed):
return {"ok": false, "code": "CONTRACT_RETURN_RECEIPT_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _project_offer(provider: Dictionary, index: int, draft_id: String) -> 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_id := String(provider.profession_id)
var trait_id := String(provider.trait_id)
var item_id := String(provider.starting_equipment_id)
var profession: Dictionary = professions.get(profession_id, {})
var trait_row: Dictionary = traits.get(trait_id, {})
var item: Dictionary = equipment.get(item_id, {})
var name_tokens: Array = Array(provider.display_name_tokens).duplicate()
var display_name := "%s %s" % [_name_token(name_tokens[0]), _name_token(name_tokens[1])]
var offer := {
"offer_id": String(provider.source_offer_id),
"offer_index": index,
"recruit_id": String(provider.recruit_id),
"source_draft_id": draft_id,
"generation_seed": String(provider.generation_seed),
"name_component_ids": name_tokens,
"rendered_name_snapshot": {"en": display_name, "zh_CN": display_name},
"appearance_seed": String(provider.appearance_seed),
"profession_id": 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_attributes(String(provider.generation_seed)),
"trait_id": 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": item_id,
"starting_item_name": {
"en": String(item.get("name_en", item.get("name", ""))),
"zh_CN": String(item.get("name_zh", "")),
},
"starting_item_state": "equipped",
"level": 1,
"xp": 0,
"state": "provisional",
}
offer["canonical_output_hash"] = _hash(offer)
return offer
func _validate_offer(value, expected_index: int, draft_id: String) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "DRAFT_OFFER_TYPE_INVALID"}
var offer: Dictionary = value
var required := [
"offer_id", "offer_index", "recruit_id", "source_draft_id",
"generation_seed", "name_component_ids", "rendered_name_snapshot",
"appearance_seed", "profession_id", "profession_name", "profession_job",
"attributes", "trait_id", "trait_name", "trait_benefit", "trait_cost",
"starting_item_id", "starting_item_name", "starting_item_state", "level",
"xp", "state", "canonical_output_hash",
]
for field in required:
if not offer.has(field):
return {"ok": false, "code": "DRAFT_OFFER_FIELD_MISSING", "field": field}
if int(offer.offer_index) != expected_index:
return {"ok": false, "code": "DRAFT_OFFER_POSITION_INVALID"}
if not _is_hex(String(offer.offer_id), 32) or not _is_hex(String(offer.recruit_id), 32):
return {"ok": false, "code": "DRAFT_OFFER_ID_INVALID"}
if String(offer.source_draft_id) != draft_id or not _is_hex(String(offer.generation_seed), 32):
return {"ok": false, "code": "DRAFT_OFFER_SOURCE_INVALID"}
if not offer.name_component_ids is Array or offer.name_component_ids.size() != 2:
return {"ok": false, "code": "DRAFT_OFFER_NAME_INVALID"}
for localized_field in ["rendered_name_snapshot", "profession_name", "trait_name", "starting_item_name"]:
var localized = offer[localized_field]
if (
not localized is Dictionary
or String(localized.get("en", "")).strip_edges().is_empty()
or String(localized.get("zh_CN", "")).strip_edges().is_empty()
):
return {"ok": false, "code": "DRAFT_OFFER_LOCALIZATION_INVALID", "field": localized_field}
var professions := ContentRegistry.rows_by_id(_content, "professions")
var traits := ContentRegistry.rows_by_id(_content, "traits")
var equipment := ContentRegistry.rows_by_id(_content, "equipment")
if not _catalog_id(String(offer.profession_id), "PF-B", 2) or not professions.has(String(offer.profession_id)):
return {"ok": false, "code": "DRAFT_OFFER_PROFESSION_INVALID"}
if not _catalog_id(String(offer.trait_id), "TR-", 3) or not traits.has(String(offer.trait_id)):
return {"ok": false, "code": "DRAFT_OFFER_TRAIT_INVALID"}
if not _catalog_id(String(offer.starting_item_id), "EQ-", 3) or not equipment.has(String(offer.starting_item_id)) or String(offer.starting_item_state) != "equipped":
return {"ok": false, "code": "DRAFT_OFFER_ITEM_INVALID"}
if String(offer.profession_job).strip_edges().is_empty() or String(offer.trait_benefit).strip_edges().is_empty() or String(offer.trait_cost).strip_edges().is_empty():
return {"ok": false, "code": "DRAFT_OFFER_MECHANIC_INVALID"}
if not offer.attributes is Dictionary:
return {"ok": false, "code": "DRAFT_OFFER_ATTRIBUTES_INVALID"}
var attribute_keys: Array = offer.attributes.keys()
attribute_keys.sort()
var expected_keys := ATTRIBUTE_IDS.duplicate()
expected_keys.sort()
if attribute_keys != expected_keys:
return {"ok": false, "code": "DRAFT_OFFER_ATTRIBUTES_INVALID"}
var total := 0
for attribute_id in ATTRIBUTE_IDS:
var amount := int(offer.attributes[attribute_id])
if amount < 4 or amount > 15:
return {"ok": false, "code": "DRAFT_OFFER_ATTRIBUTE_RANGE_INVALID"}
total += amount
if total != 35:
return {"ok": false, "code": "DRAFT_OFFER_ATTRIBUTE_BUDGET_INVALID"}
if int(offer.level) != 1 or int(offer.xp) != 0 or String(offer.state) != "provisional":
return {"ok": false, "code": "DRAFT_OFFER_STATE_INVALID"}
var unhashed := offer.duplicate(true)
unhashed.erase("canonical_output_hash")
if String(offer.canonical_output_hash) != _hash(unhashed):
return {"ok": false, "code": "DRAFT_OFFER_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _normalize_selection(values: Array, offers: Array) -> Dictionary:
if values.size() > 2:
return {"ok": false, "code": "DRAFT_SELECTION_LIMIT"}
var positions := {}
for offer in offers:
positions[String(offer.offer_id)] = int(offer.offer_index)
var ids: Array[String] = []
for value in values:
var offer_id := String(value)
if not positions.has(offer_id):
return {"ok": false, "code": "DRAFT_SELECTION_UNKNOWN_ID"}
if offer_id in ids:
return {"ok": false, "code": "DRAFT_SELECTION_DUPLICATE_ID"}
ids.append(offer_id)
ids.sort_custom(func(left: String, right: String): return int(positions[left]) < int(positions[right]))
return {"ok": true, "code": "selection_valid", "ids": ids}
func _numeric_attributes(seed: String) -> Dictionary:
var result := {}
for attribute_id in ATTRIBUTE_IDS:
result[attribute_id] = 4
for point in range(15):
var candidates := 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 _save_draft(draft: Dictionary) -> Dictionary:
_refresh_draft_hash(draft)
var validation := validate_draft(draft)
if not validation.ok:
return validation
var candidate_path := _candidate_path()
var write := _write_verified(candidate_path, draft)
if not write.ok:
return write
if FileAccess.file_exists(path):
if FileAccess.file_exists(_backup_path()):
DirAccess.remove_absolute(_backup_path())
var backup_error := DirAccess.rename_absolute(path, _backup_path())
if backup_error != OK:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "DRAFT_BACKUP_FAILED"}
var promote_error := DirAccess.rename_absolute(candidate_path, path)
if promote_error != OK:
if FileAccess.file_exists(_backup_path()) and not FileAccess.file_exists(path):
DirAccess.rename_absolute(_backup_path(), path)
return {"ok": false, "code": "DRAFT_PROMOTE_FAILED"}
var mirror := _write_verified(_backup_candidate_path(), draft)
if mirror.ok:
if FileAccess.file_exists(_backup_path()):
DirAccess.remove_absolute(_backup_path())
DirAccess.rename_absolute(_backup_candidate_path(), _backup_path())
return {"ok": true, "code": "saved", "draft": draft.duplicate(true)}
func _restore_primary(draft: Dictionary, use_existing_candidate: bool) -> Dictionary:
var candidate_path := _candidate_path()
if not use_existing_candidate or not FileAccess.file_exists(candidate_path):
var write := _write_verified(candidate_path, draft)
if not write.ok:
return {"ok": false, "code": "DRAFT_BACKUP_RESTORE_FAILED"}
var corrupt_path := "%s.corrupt" % path
if FileAccess.file_exists(corrupt_path):
DirAccess.remove_absolute(corrupt_path)
if FileAccess.file_exists(path):
if DirAccess.rename_absolute(path, corrupt_path) != OK:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "DRAFT_BACKUP_RESTORE_FAILED"}
if DirAccess.rename_absolute(candidate_path, path) != OK:
if FileAccess.file_exists(corrupt_path):
DirAccess.rename_absolute(corrupt_path, path)
return {"ok": false, "code": "DRAFT_BACKUP_RESTORE_FAILED"}
return {"ok": true, "code": "restored"}
func _read_valid(file_path: String) -> Dictionary:
if not FileAccess.file_exists(file_path):
return {"ok": false, "code": "missing"}
var parser := JSON.new()
if parser.parse(FileAccess.get_file_as_string(file_path)) != OK:
return {"ok": false, "code": "invalid_json"}
var validation := validate_draft(parser.data)
if not validation.ok:
return validation
return {"ok": true, "code": "valid", "draft": Dictionary(parser.data).duplicate(true)}
func _write_verified(file_path: String, draft: Dictionary) -> Dictionary:
var file := FileAccess.open(file_path, FileAccess.WRITE)
if file == null:
return {"ok": false, "code": "DRAFT_WRITE_UNAVAILABLE"}
file.store_string(JSON.stringify(_canonical(draft)))
file.close()
var verified := _read_valid(file_path)
if not verified.ok:
DirAccess.remove_absolute(file_path)
return {
"ok": false,
"code": "DRAFT_WRITE_VERIFY_FAILED",
"reason": String(verified.get("code", "unknown")),
}
return {"ok": true, "code": "written"}
func _save_company(company: Dictionary) -> Dictionary:
var validation := validate_company(company)
if not validation.ok:
return validation
var candidate_path := _company_candidate_path()
var write := _write_verified_company(candidate_path, company)
if not write.ok:
return write
if FileAccess.file_exists(company_path):
if FileAccess.file_exists(_company_backup_path()):
DirAccess.remove_absolute(_company_backup_path())
var backup_error := DirAccess.rename_absolute(company_path, _company_backup_path())
if backup_error != OK:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "COMPANY_BACKUP_FAILED"}
var promote_error := DirAccess.rename_absolute(candidate_path, company_path)
if promote_error != OK:
if FileAccess.file_exists(_company_backup_path()) and not FileAccess.file_exists(company_path):
DirAccess.rename_absolute(_company_backup_path(), company_path)
return {"ok": false, "code": "COMPANY_PROMOTE_FAILED"}
var mirror := _write_verified_company(_company_backup_candidate_path(), company)
if mirror.ok:
if FileAccess.file_exists(_company_backup_path()):
DirAccess.remove_absolute(_company_backup_path())
DirAccess.rename_absolute(_company_backup_candidate_path(), _company_backup_path())
return {"ok": true, "code": "saved", "company": company.duplicate(true)}
func _read_valid_company(file_path: String) -> Dictionary:
if not FileAccess.file_exists(file_path):
return {"ok": false, "code": "missing"}
var parser := JSON.new()
if parser.parse(FileAccess.get_file_as_string(file_path)) != OK:
return {"ok": false, "code": "invalid_json"}
if parser.data is Dictionary and int(parser.data.get("company_version", -1)) == LEGACY_COMPANY_VERSION:
var legacy_validation := _validate_legacy_company(parser.data)
if not legacy_validation.ok:
return legacy_validation
var migrated := _migrate_company_v1(parser.data)
var migrated_validation := validate_company(migrated)
if not migrated_validation.ok:
return migrated_validation
return {"ok": true, "code": "valid_migrated", "company": migrated, "migrated": true}
var validation := validate_company(parser.data)
if not validation.ok:
return validation
return {"ok": true, "code": "valid", "company": Dictionary(parser.data).duplicate(true)}
func _validate_legacy_company(value) -> Dictionary:
if not value is Dictionary:
return {"ok": false, "code": "COMPANY_NOT_DICTIONARY"}
var company: Dictionary = value
var required := [
"company_version", "company_kind", "company_id", "created_from_draft_id",
"source_offer_set_hash", "source_revision", "owned_recruits",
"creation_receipt", "company_hash",
]
for field in required:
if not company.has(field):
return {"ok": false, "code": "COMPANY_FIELD_MISSING", "field": field}
if int(company.company_version) != LEGACY_COMPANY_VERSION or String(company.company_kind) != COMPANY_KIND:
return {"ok": false, "code": "COMPANY_VERSION_UNSUPPORTED"}
if not _is_hex(String(company.company_id), 32) or not _is_hex(String(company.created_from_draft_id), 32):
return {"ok": false, "code": "COMPANY_IDENTITY_INVALID"}
if not _is_hex(String(company.source_offer_set_hash), 64) or String(company.source_revision) != SOURCE_REVISION:
return {"ok": false, "code": "COMPANY_SOURCE_INVALID"}
if not company.owned_recruits is Array or company.owned_recruits.size() != 2:
return {"ok": false, "code": "COMPANY_ROSTER_COUNT_INVALID"}
var recruit_ids: Array[String] = []
for recruit in company.owned_recruits:
var recruit_validation := _validate_owned_recruit(recruit, String(company.created_from_draft_id))
if not recruit_validation.ok:
return recruit_validation
if String(recruit.recruit_id) in recruit_ids:
return {"ok": false, "code": "COMPANY_RECRUIT_ID_DUPLICATE"}
recruit_ids.append(String(recruit.recruit_id))
var receipt_validation := _validate_company_receipt(company.creation_receipt, company)
if not receipt_validation.ok:
return receipt_validation
for forbidden in ["offers", "unchosen_offer_ids", "current_run", "active_contract"]:
if company.has(forbidden):
return {"ok": false, "code": "COMPANY_FORBIDDEN_STATE"}
var unhashed := company.duplicate(true)
unhashed.erase("company_hash")
if String(company.company_hash) != _hash(unhashed):
return {"ok": false, "code": "COMPANY_HASH_INVALID"}
return {"ok": true, "code": "valid"}
func _migrate_company_v1(value: Dictionary) -> Dictionary:
var migrated := value.duplicate(true)
migrated.company_version = COMPANY_VERSION
migrated.company_revision = 0
migrated.next_contract_ordinal = 0
migrated.active_contract = {}
migrated.contract_receipts = []
migrated.erase("company_hash")
migrated.company_hash = _hash(migrated)
return migrated
func _write_verified_company(file_path: String, company: Dictionary) -> Dictionary:
var file := FileAccess.open(file_path, FileAccess.WRITE)
if file == null:
return {"ok": false, "code": "COMPANY_WRITE_UNAVAILABLE"}
file.store_string(JSON.stringify(_canonical(company)))
file.close()
var verified := _read_valid_company(file_path)
if not verified.ok:
DirAccess.remove_absolute(file_path)
return {
"ok": false,
"code": "COMPANY_WRITE_VERIFY_FAILED",
"reason": String(verified.get("code", "unknown")),
}
return {"ok": true, "code": "written"}
func _restore_company_primary(company: Dictionary, use_existing_candidate: bool) -> Dictionary:
var candidate_path := _company_candidate_path()
if not use_existing_candidate or not FileAccess.file_exists(candidate_path):
var write := _write_verified_company(candidate_path, company)
if not write.ok:
return {"ok": false, "code": "COMPANY_BACKUP_RESTORE_FAILED"}
var corrupt_path := "%s.corrupt" % company_path
if FileAccess.file_exists(corrupt_path):
DirAccess.remove_absolute(corrupt_path)
if FileAccess.file_exists(company_path):
if DirAccess.rename_absolute(company_path, corrupt_path) != OK:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "COMPANY_BACKUP_RESTORE_FAILED"}
if DirAccess.rename_absolute(candidate_path, company_path) != OK:
if FileAccess.file_exists(corrupt_path):
DirAccess.rename_absolute(corrupt_path, company_path)
return {"ok": false, "code": "COMPANY_BACKUP_RESTORE_FAILED"}
return {"ok": true, "code": "restored"}
func _remove_draft_files() -> bool:
var ok := true
for file_path in [path, _backup_path(), _candidate_path(), _backup_candidate_path(), "%s.corrupt" % path]:
if FileAccess.file_exists(file_path) and DirAccess.remove_absolute(file_path) != OK:
ok = false
return ok
func _refresh_draft_hash(draft: Dictionary) -> void:
draft.offer_set_hash = _hash(draft.offers)
draft.erase("draft_hash")
draft.draft_hash = _hash(draft)
func _name_token(token) -> String:
var parts := String(token).split(".")
return String(parts[parts.size() - 1])
func _catalog_id(value: String, prefix: String, digits: int) -> bool:
if not value.begins_with(prefix) or value.length() != prefix.length() + digits:
return false
return String(value.substr(prefix.length())).is_valid_int()
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 _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
func _candidate_path() -> String:
return "%s.candidate" % path
func _backup_path() -> String:
return "%s.bak" % path
func _backup_candidate_path() -> String:
return "%s.bak.candidate" % path
func _company_candidate_path() -> String:
return "%s.candidate" % company_path
func _company_backup_path() -> String:
return "%s.bak" % company_path
func _company_backup_candidate_path() -> String:
return "%s.bak.candidate" % company_path