575 lines
20 KiB
GDScript
575 lines
20 KiB
GDScript
class_name ABGFG3CampaignAdapter
|
|
extends RefCounted
|
|
|
|
const Contract1Battle = preload("res://fg3/contract1_battle_service.gd")
|
|
const Contract1Service = preload("res://fg3/contract1_service.gd")
|
|
const Contract2Activation = preload("res://fg3/contract2_service.gd")
|
|
const Contract2Battle = preload("res://fg3/contract2_battle_service.gd")
|
|
const Contract2Execution = preload("res://fg3/contract2_execution_service.gd")
|
|
const FG3Content = preload("res://fg3/content_registry.gd")
|
|
const P100Content = preload("res://p10_0/content_registry.gd")
|
|
const P103Content = preload("res://p10_3/content_registry.gd")
|
|
const State = preload("res://fg3/run_state.gd")
|
|
|
|
const CONTRACT1_PHASES := [
|
|
"market", "prepared", "recovery", "boss_muster", "boss_prepared",
|
|
"boss_recovery", "contract_result",
|
|
]
|
|
const CONTRACT2_PHASES := [
|
|
"contract2_market", "contract2_prepared", "contract2_recovery",
|
|
"contract2_boss_muster", "contract2_boss_prepared",
|
|
"contract2_boss_recovery", "region_result", "region_closed",
|
|
]
|
|
const PLAN_COPY := {
|
|
"hold_line": ["稳住战线", "HOLD LINE"],
|
|
"follow_tide": ["随潮换位", "FOLLOW TIDE"],
|
|
"focus_source": ["集火源头", "FOCUS SOURCE"],
|
|
"guard_center": ["护住中段", "GUARD CENTER"],
|
|
"follow_index_clear_eggs": ["跟随安全位并清卵", "FOLLOW INDEX & CLEAR EGGS"],
|
|
"shift_hatch_area": ["引导孵化进入预备区", "SHIFT HATCH INTO AREA"],
|
|
"interrupt_pulse": ["打断脉冲", "INTERRUPT PULSE"],
|
|
"fortify_links": ["加固链接位", "FORTIFY LINKS"],
|
|
"stagger_actions": ["错开行动", "STAGGER ACTIONS"],
|
|
"reorder_line": ["重排战线", "REORDER LINE"],
|
|
"interrupt_pulse_family": ["打断一组脉冲", "INTERRUPT PULSE FAMILY"],
|
|
"durable_links_stagger": ["耐受链接并错峰行动", "DURABLE LINKS & STAGGER"],
|
|
}
|
|
|
|
var service
|
|
var market
|
|
var valid := false
|
|
var error_code := "adapter_not_configured"
|
|
var current_battle: Dictionary = {}
|
|
var event_cursor := 0
|
|
var operation_serial := 0
|
|
var fg3_catalog: Dictionary = FG3Content.snapshot()
|
|
var p100_catalog: Dictionary = P100Content.snapshot()
|
|
var p103_catalog: Dictionary = P103Content.snapshot()
|
|
var profession_rows: Dictionary = P100Content.rows_by_id(p100_catalog, "professions")
|
|
var enemy_rows: Dictionary = P103Content.rows_by_id(p103_catalog, "enemies")
|
|
var boss_rows: Dictionary = FG3Content.rows_by_id(fg3_catalog, "bosses")
|
|
|
|
|
|
func configure(snapshot_value: Dictionary) -> Dictionary:
|
|
current_battle = {}
|
|
event_cursor = 0
|
|
operation_serial = 0
|
|
if not _bind(snapshot_value):
|
|
return _reject(error_code)
|
|
return {
|
|
"ok": true,
|
|
"code": "fg3_adapter_configured",
|
|
"snapshot_hash": snapshot_hash(),
|
|
"view": view_state(),
|
|
}
|
|
|
|
|
|
func snapshot() -> Dictionary:
|
|
return service.state.snapshot() if valid else {}
|
|
|
|
|
|
func snapshot_hash() -> String:
|
|
return hash_value(snapshot())
|
|
|
|
|
|
func view_state(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return {}
|
|
var run: Dictionary = service.state.current_run
|
|
var phase := String(run.phase)
|
|
return {
|
|
"region_id": String(run.region_id),
|
|
"contract_id": String(run.contract_id),
|
|
"phase": phase,
|
|
"mode": _mode(phase),
|
|
"market_index": int(run.market_index),
|
|
"round_index": int(run.round_index),
|
|
"cleared_rounds": int(run.cleared_rounds),
|
|
"coin": int(run.coin),
|
|
"replacement_credit": int(run.replacement_credit),
|
|
"line_capacity": int(run.line_capacity),
|
|
"company": _company_view(run, locale),
|
|
"routes": _route_view(run, locale),
|
|
"plans": _plan_view(run, locale),
|
|
"battle": _battle_view(run, locale),
|
|
"reward": _result_view(run, locale),
|
|
"recovery_required": bool(run.recovery_required),
|
|
"recovery_cost": int(run.recovery_cost),
|
|
"boss_id": String(run.boss_muster.get("boss_id", "")),
|
|
"records_count": service.state.guild_profile.company_records.size(),
|
|
"persistent_renown": int(service.state.guild_profile.persistent_renown),
|
|
"unlocked_region_ids": Array(service.state.guild_profile.unlocked_region_ids).duplicate(),
|
|
"source_revision": int(service.state.revision),
|
|
"source_snapshot_hash": snapshot_hash(),
|
|
}
|
|
|
|
|
|
func prepare_route(offer_id: String, locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
if phase not in ["market", "contract2_market"]:
|
|
return _reject("market_required")
|
|
var result: Dictionary = service.commit_route(
|
|
_operation("prepare", offer_id),
|
|
service.state.revision,
|
|
offer_id,
|
|
String(service.state.current_run.market_board.board_hash)
|
|
)
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func start_plan(plan_id: String, locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
if phase in ["boss_muster", "contract2_boss_muster"]:
|
|
var prepared: Dictionary = service.commit_boss_plan(
|
|
_operation("boss-plan", plan_id),
|
|
service.state.revision,
|
|
String(service.state.current_run.boss_muster.muster_hash),
|
|
plan_id
|
|
)
|
|
if not prepared.ok:
|
|
return prepared
|
|
if not _bind(prepared.snapshot):
|
|
return _reject(error_code)
|
|
phase = String(service.state.current_run.phase)
|
|
if phase not in ["prepared", "contract2_prepared", "boss_prepared", "contract2_boss_prepared"]:
|
|
return _reject("prepared_state_required")
|
|
var battle_service = (
|
|
Contract2Battle.new(snapshot())
|
|
if phase.begins_with("contract2_")
|
|
else Contract1Battle.new(snapshot())
|
|
)
|
|
var battle: Dictionary = battle_service.run_to_terminal(plan_id)
|
|
if not battle.ok:
|
|
return battle
|
|
current_battle = Dictionary(battle.snapshot).duplicate(true)
|
|
event_cursor = 0
|
|
return {
|
|
"ok": true,
|
|
"code": "fg3_battle_started",
|
|
"plan_id": plan_id,
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"view": view_state(locale),
|
|
}
|
|
|
|
|
|
func advance_battle_ticks(count: int, locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return _reject("battle_required")
|
|
var events: Array = current_battle.event_log
|
|
var next_cursor := mini(events.size(), event_cursor + maxi(1, count))
|
|
var fresh: Array = events.slice(event_cursor, next_cursor)
|
|
event_cursor = next_cursor
|
|
return {
|
|
"ok": true,
|
|
"code": "fg3_battle_terminal" if event_cursor == events.size() else "fg3_battle_advanced",
|
|
"events": normalize_events(fresh),
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"view": view_state(locale),
|
|
}
|
|
|
|
|
|
func run_battle_to_terminal(locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return _reject("battle_required")
|
|
return advance_battle_ticks(current_battle.event_log.size() - event_cursor + 1, locale)
|
|
|
|
|
|
func settle_terminal(locale := "zh_CN") -> Dictionary:
|
|
if current_battle.is_empty() or event_cursor < current_battle.event_log.size():
|
|
return _reject("visible_terminal_required")
|
|
var operation_id := _operation("settle", String(current_battle.snapshot_hash))
|
|
var result: Dictionary
|
|
if String(current_battle.kind) in ["fg3_contract1_boss", "fg3_contract2_boss"]:
|
|
result = service.settle_boss(operation_id, service.state.revision, current_battle)
|
|
else:
|
|
result = service.settle_route(operation_id, service.state.revision, current_battle)
|
|
if not result.ok:
|
|
return result
|
|
current_battle = {}
|
|
event_cursor = 0
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func continue_result(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var phase := String(service.state.current_run.phase)
|
|
var result: Dictionary
|
|
if phase == "contract_result":
|
|
result = service.close_contract(
|
|
_operation("close-contract1", "CLEAR-RG02-C01"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
elif phase == "contract2_handoff":
|
|
result = service.activate_contract(
|
|
_operation("activate-contract2", "CTR-R2-02"),
|
|
service.state.revision,
|
|
String(service.state.current_run.market_board.handoff_hash)
|
|
)
|
|
elif phase == "region_result":
|
|
result = service.close_region(
|
|
_operation("close-region", "RG-02"),
|
|
service.state.revision,
|
|
String(service.state.current_run.pending_boss_result.close_id)
|
|
)
|
|
else:
|
|
return _reject("result_state_required")
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func recover(locale := "zh_CN") -> Dictionary:
|
|
if not valid:
|
|
return _reject(error_code)
|
|
var run: Dictionary = service.state.current_run
|
|
var phase := String(run.phase)
|
|
var result: Dictionary
|
|
if phase in ["recovery", "contract2_recovery"]:
|
|
result = service.recover_route(
|
|
_operation("recover-route", String(run.last_battle_report.battle_hash)),
|
|
service.state.revision,
|
|
String(run.last_battle_report.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
elif phase in ["boss_recovery", "contract2_boss_recovery"]:
|
|
result = service.recover_boss(
|
|
_operation("recover-boss", String(run.pending_boss_result.battle_hash)),
|
|
service.state.revision,
|
|
String(run.pending_boss_result.battle_hash),
|
|
int(run.recovery_cost)
|
|
)
|
|
else:
|
|
return _reject("recovery_state_required")
|
|
return _replace_from_result(result, locale)
|
|
|
|
|
|
func commit_order(_ordered_recruit_ids: Array) -> Dictionary:
|
|
return _reject("formation_authority_frozen")
|
|
|
|
|
|
func normalize_events(events: Array) -> Array[Dictionary]:
|
|
var normalized: Array[Dictionary] = []
|
|
for raw_event in events:
|
|
if not raw_event is Dictionary:
|
|
continue
|
|
var event: Dictionary = raw_event
|
|
var authority_kind := String(event.get("kind", ""))
|
|
var kind: String = String({
|
|
"wave_entered": "wave_arrived",
|
|
"authoritative_damage": "damage",
|
|
"battle_terminal": "outcome",
|
|
"boss_terminal": "outcome",
|
|
}.get(authority_kind, authority_kind))
|
|
var detail: Dictionary = Dictionary(event.get("payload", {})).duplicate(true)
|
|
detail["authority_kind"] = authority_kind
|
|
if kind == "damage":
|
|
detail["raw"] = int(detail.get("amount", 0))
|
|
detail["effective"] = int(detail.get("amount", 0))
|
|
if kind == "outcome":
|
|
detail["outcome"] = String(detail.get("outcome", current_battle.get("outcome", "")))
|
|
normalized.append({
|
|
"event_id": String(event.get("event_id", "")),
|
|
"tick": int(event.get("tick", 0)),
|
|
"kind": String(kind),
|
|
"source_id": String(event.get("source_id", "")),
|
|
"target_id": String(event.get("target_id", "")),
|
|
"detail": detail,
|
|
})
|
|
return normalized
|
|
|
|
|
|
static func hash_value(value) -> String:
|
|
return JSON.stringify(_canonical(value)).sha256_text()
|
|
|
|
|
|
func _bind(snapshot_value: Dictionary) -> bool:
|
|
var probe := State.new()
|
|
if not probe.restore_snapshot(snapshot_value):
|
|
valid = false
|
|
error_code = "invalid_fg3_snapshot"
|
|
return false
|
|
var phase := String(probe.current_run.phase)
|
|
if phase == "contract2_handoff":
|
|
service = Contract2Activation.new(snapshot_value)
|
|
elif phase in CONTRACT2_PHASES:
|
|
service = Contract2Execution.new(snapshot_value)
|
|
elif phase in CONTRACT1_PHASES:
|
|
service = Contract1Service.new(snapshot_value)
|
|
else:
|
|
valid = false
|
|
error_code = "unsupported_fg3_phase:%s" % phase
|
|
return false
|
|
market = service
|
|
valid = bool(service.valid)
|
|
error_code = "" if valid else String(service.error_code)
|
|
return valid
|
|
|
|
|
|
func _replace_from_result(result: Dictionary, locale: String) -> Dictionary:
|
|
if not bool(result.get("ok", false)):
|
|
return result
|
|
if not _bind(Dictionary(result.snapshot)):
|
|
return _reject(error_code)
|
|
var forwarded := result.duplicate(true)
|
|
forwarded["view"] = view_state(locale)
|
|
return forwarded
|
|
|
|
|
|
func _mode(phase: String) -> String:
|
|
if phase in ["market", "contract2_market"]:
|
|
return "market"
|
|
if phase in ["prepared", "contract2_prepared"]:
|
|
return "plan"
|
|
if phase in ["boss_muster", "contract2_boss_muster"]:
|
|
return "boss_muster"
|
|
if phase in ["boss_prepared", "contract2_boss_prepared"]:
|
|
return "boss_plan"
|
|
if phase in ["recovery", "contract2_recovery", "boss_recovery", "contract2_boss_recovery"]:
|
|
return "recovery"
|
|
if phase == "contract_result":
|
|
return "contract_record"
|
|
if phase == "contract2_handoff":
|
|
return "contract_handoff"
|
|
if phase == "region_result":
|
|
return "region_record"
|
|
if phase == "region_closed":
|
|
return "region_closed"
|
|
return "battle" if not current_battle.is_empty() else "unknown"
|
|
|
|
|
|
func _company_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
var rows: Array[Dictionary] = []
|
|
for recruit_id_value in run.company:
|
|
var recruit_id := String(recruit_id_value)
|
|
var record: Dictionary = run.recruit_records[recruit_id]
|
|
var profession_id := String(record.profession_id)
|
|
var profession: Dictionary = profession_rows.get(profession_id, {})
|
|
rows.append({
|
|
"recruit_id": recruit_id,
|
|
"actor_id": recruit_id,
|
|
"identity_id": profession_id,
|
|
"profession_id": profession_id,
|
|
"profession_name": String(profession.get("name", profession_id)),
|
|
"trait_id": String(record.trait_id),
|
|
"level": int(record.level),
|
|
"hp": int(record.current_hp),
|
|
"max_hp": int(record.max_hp),
|
|
"guard": 0,
|
|
"deployed": recruit_id in run.deployed_recruit_ids,
|
|
"locale": locale,
|
|
})
|
|
return rows
|
|
|
|
|
|
func _route_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
if String(run.phase) not in ["market", "contract2_market"]:
|
|
return []
|
|
var rows: Array[Dictionary] = []
|
|
for offer_value in run.market_board.battle_offers:
|
|
var offer: Dictionary = offer_value
|
|
var enemy_names: Array[String] = []
|
|
for enemy_id in offer.enemy_ids:
|
|
var enemy: Dictionary = enemy_rows.get(String(enemy_id), {})
|
|
enemy_names.append(_localized(enemy, locale, String(enemy_id)))
|
|
rows.append({
|
|
"offer_id": String(offer.offer_id),
|
|
"route_id": String(offer.route_id),
|
|
"name": _localized(offer, locale, String(offer.route_id)),
|
|
"risk": int(offer.risk),
|
|
"wave_count": offer.enemy_ids.size(),
|
|
"enemy_ids": Array(offer.enemy_ids).duplicate(),
|
|
"enemy_names": enemy_names,
|
|
"preview": String(offer.get("preview", "")),
|
|
"failure_cause": String(offer.get("failure_cause", "")),
|
|
"reward_coin": int(offer.reward_coin),
|
|
"recovery_coin": int(offer.recovery_coin),
|
|
})
|
|
return rows
|
|
|
|
|
|
func _plan_view(run: Dictionary, locale: String) -> Array[Dictionary]:
|
|
var phase := String(run.phase)
|
|
var ids: Array = []
|
|
var primary := ""
|
|
var secondary := ""
|
|
if phase in ["prepared", "contract2_prepared"]:
|
|
if phase == "prepared":
|
|
ids = Array(Contract1Battle.PLAN_IDS).duplicate()
|
|
primary = String(Contract1Battle.PREFERRED_PLAN.get(String(run.pending_encounter.route_id), ""))
|
|
else:
|
|
ids = Array(Contract2Battle.PLAN_IDS).duplicate()
|
|
var market_index := int(run.pending_encounter.round_index)
|
|
primary = String(Contract2Battle.PRIMARY_PLAN[market_index])
|
|
secondary = String(Contract2Battle.SECONDARY_PLAN[market_index])
|
|
elif phase in ["boss_muster", "contract2_boss_muster"]:
|
|
ids = Array(run.boss_muster.plan_ids).duplicate()
|
|
else:
|
|
return []
|
|
var rows: Array[Dictionary] = []
|
|
for plan_id_value in ids:
|
|
var plan_id := String(plan_id_value)
|
|
var copy: Array = PLAN_COPY.get(plan_id, [plan_id, plan_id])
|
|
rows.append({
|
|
"plan_id": plan_id,
|
|
"offer_id": plan_id,
|
|
"name": String(copy[1] if locale == "en" else copy[0]),
|
|
"primary": plan_id == primary,
|
|
"secondary": plan_id == secondary,
|
|
})
|
|
return rows
|
|
|
|
|
|
func _battle_view(run: Dictionary, locale: String) -> Dictionary:
|
|
if current_battle.is_empty():
|
|
return {}
|
|
var allies: Array[Dictionary] = []
|
|
var hp_by_actor := {}
|
|
for recruit_id_value in current_battle.final_order:
|
|
var recruit_id := String(recruit_id_value)
|
|
var actor: Dictionary = current_battle.actors[recruit_id]
|
|
hp_by_actor[recruit_id] = int(actor.hp_before)
|
|
for event in Array(current_battle.event_log).slice(0, event_cursor):
|
|
if String(event.get("kind", "")) == "authoritative_damage":
|
|
hp_by_actor[String(event.target_id)] = int(event.payload.hp_after)
|
|
for index in range(current_battle.final_order.size()):
|
|
var recruit_id := String(current_battle.final_order[index])
|
|
var actor: Dictionary = current_battle.actors[recruit_id]
|
|
var profession_id := String(actor.profession_id)
|
|
var profession: Dictionary = profession_rows.get(profession_id, {})
|
|
allies.append({
|
|
"actor_id": recruit_id,
|
|
"identity_id": profession_id,
|
|
"profession_id": profession_id,
|
|
"profession_name": String(profession.get("name", profession_id)),
|
|
"hp": int(hp_by_actor[recruit_id]),
|
|
"max_hp": int(actor.max_hp),
|
|
"guard": 0,
|
|
"slot_index": index,
|
|
})
|
|
var enemy_ids: Array = []
|
|
if String(current_battle.get("boss_id", "")).is_empty():
|
|
enemy_ids = Array(run.pending_encounter.enemy_ids).duplicate()
|
|
else:
|
|
enemy_ids = [String(current_battle.boss_id)]
|
|
var enemies: Array[Dictionary] = []
|
|
for index in range(enemy_ids.size()):
|
|
var enemy_id := String(enemy_ids[index])
|
|
var row: Dictionary = boss_rows.get(enemy_id, enemy_rows.get(enemy_id, {}))
|
|
enemies.append({
|
|
"actor_id": enemy_id,
|
|
"identity_id": enemy_id,
|
|
"name": _localized(row, locale, enemy_id),
|
|
"hp": 1,
|
|
"max_hp": 1,
|
|
"guard": 0,
|
|
"slot_index": index,
|
|
})
|
|
var revealed: Array = Array(current_battle.event_log).slice(0, event_cursor)
|
|
var terminal: bool = event_cursor >= current_battle.event_log.size()
|
|
var decisive: Dictionary = {}
|
|
for event in normalize_events(revealed):
|
|
if bool(event.detail.get("decisive", false)) or String(event.kind) in ["outcome", "plan_response"]:
|
|
decisive = event
|
|
break
|
|
return {
|
|
"allies": allies,
|
|
"enemies": enemies,
|
|
"terminal": terminal,
|
|
"outcome": String(current_battle.outcome) if terminal else "",
|
|
"cause": String(current_battle.cause) if terminal else "",
|
|
"next_wave": mini(enemy_ids.size(), _event_count(revealed, "wave_entered") + 1),
|
|
"wave_count": enemy_ids.size(),
|
|
"first_decisive_event": decisive,
|
|
"fallen_recruit_ids": Array(current_battle.fallen_recruit_ids).duplicate() if terminal else [],
|
|
"event_count": event_cursor,
|
|
"event_log_hash": String(current_battle.event_log_hash),
|
|
"plan_id": String(current_battle.plan_id),
|
|
"boss_id": String(current_battle.get("boss_id", "")),
|
|
}
|
|
|
|
|
|
func _result_view(run: Dictionary, locale: String) -> Dictionary:
|
|
var phase := String(run.phase)
|
|
if phase == "contract_result":
|
|
return {
|
|
"kind": "contract_record",
|
|
"title": _text(locale, "第一份沼泽契约已记录", "FIRST SILTWAKE CONTRACT RECORDED"),
|
|
"body": _text(locale, "幸存队伍继续进入第二份契约。", "THE SURVIVING COMPANY CONTINUES INTO CONTRACT TWO."),
|
|
"renown": 16,
|
|
"action": _text(locale, "写入第一份记录", "WRITE FIRST RECORD"),
|
|
}
|
|
if phase == "contract2_handoff":
|
|
return {
|
|
"kind": "contract_handoff",
|
|
"title": _text(locale, "第二份契约已开放", "CONTRACT TWO IS OPEN"),
|
|
"body": _text(locale, "保留当前成员、生命、装备与神器,进入十二轮钟潮战线。", "KEEP COMPANY, HP, EQUIPMENT AND ARTIFACT FOR TWELVE BELL-TIDE ROUNDS."),
|
|
"action": _text(locale, "进入第二份契约", "ENTER CONTRACT TWO"),
|
|
}
|
|
if phase == "region_result":
|
|
return {
|
|
"kind": "region_record",
|
|
"title": _text(locale, "溺钟已沉默", "THE DROWNED BELL IS SILENT"),
|
|
"body": _text(locale, "第二份记录与 20 声望等待写入。", "THE SECOND RECORD AND 20 RENOWN ARE READY."),
|
|
"renown": 20,
|
|
"action": _text(locale, "完成淤潮沼泽", "COMPLETE SILTWAKE"),
|
|
}
|
|
if phase == "region_closed":
|
|
return {
|
|
"kind": "region_closed",
|
|
"title": _text(locale, "淤潮沼泽已修复", "SILTWAKE RESTORED"),
|
|
"body": _text(locale, "两份队伍记录已保存,余烬织境 RG-03 已开放。", "TWO COMPANY RECORDS SAVED; EMBERLOOM RG-03 UNLOCKED."),
|
|
"action": "",
|
|
}
|
|
return {}
|
|
|
|
|
|
func _operation(kind: String, target: String) -> String:
|
|
operation_serial += 1
|
|
return "fg3-runtime-%s-%d-%s" % [kind, operation_serial, target.sha256_text().substr(0, 8)]
|
|
|
|
|
|
func _localized(row: Dictionary, locale: String, fallback: String) -> String:
|
|
return String(row.get("name_en", fallback)) if locale == "en" else String(row.get("name_zh", fallback))
|
|
|
|
|
|
func _text(locale: String, zh: String, en: String) -> String:
|
|
return en if locale == "en" else zh
|
|
|
|
|
|
func _event_count(events: Array, kind: String) -> int:
|
|
var count := 0
|
|
for event in events:
|
|
if String(event.get("kind", "")) == kind:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _reject(code: String) -> Dictionary:
|
|
return {
|
|
"ok": false,
|
|
"code": code,
|
|
"view": view_state() if valid else {},
|
|
}
|
|
|
|
|
|
static 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
|