Files
GaCy-GO/tools/merge_captured_eidolon.py
T
2026-10-28 18:54:43 +07:00

98 lines
4.5 KiB
Python

"""Merge exact Client Rank/Item capture into existing Eidolon resource.
Both inputs are read-only bounded probe logs. Rank IDs are joined against the
captured AvatarConfig; each cost must exist as a captured Client ItemRow. The
existing entries and unknown document fields are retained. No player data is
opened or modified.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
def capture(path, prefix):
raw = path.read_bytes()
values = []
for line in raw.decode('utf-8-sig').splitlines():
if prefix in line:
values.append(json.loads(line.split(prefix, 1)[1]))
if not values:
raise ValueError(f'No captured rows found in {path}')
return values, hashlib.sha256(raw).hexdigest()
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--rank-log', required=True, type=Path)
p.add_argument('--item-log', required=True, type=Path)
p.add_argument('--avatar-config', required=True, type=Path)
p.add_argument('--preview-catalog', required=True, type=Path)
p.add_argument('--eidolon-config', required=True, type=Path)
args = p.parse_args()
rankrows, rankhash = capture(args.rank_log, '[GAC_RANK_COST] ROW ')
itemrows, itemhash = capture(args.item_log, '[GAC_RANK_ITEM] ROW ')
avatars = json.loads(args.avatar_config.read_text(encoding='utf8'))['avatar_config']
catalog = json.loads(args.preview_catalog.read_text(encoding='utf8'))
identity = catalog.get('client_assembly_sha256', '')
if len(bytes.fromhex(identity)) != 32 or not catalog.get('evidence_id'):
raise ValueError('Captured Client identity is missing')
by_avatar = {}
for avatar in avatars:
aid = int(avatar['AvatarID'])
if aid in by_avatar:
raise ValueError(f'Duplicate AvatarID {aid}')
by_avatar[aid] = avatar
items = {}
for value in itemrows:
iid = int(value['requested_id'])
row = value['row']
if iid in items or int(row['ID']) != iid or int(row['Rarity']) <= 0:
raise ValueError(f'Invalid or duplicate captured ItemRow {iid}')
items[iid] = row
grouped = {}
for row in rankrows:
aid = int(row['avatar_id'])
avatar = by_avatar.get(aid)
if avatar is None:
raise ValueError(f'Avatar {aid} missing from current config')
rank = int(row['rank'])
expected_ids = avatar.get('RankIDList', [])
if rank < 1 or rank > len(expected_ids) or int(row['rank_id']) != int(expected_ids[rank-1]):
raise ValueError(f'Rank ID mismatch for {aid}/{rank}')
cost = row.get('cost', [])
if not cost or any(int(c['ItemID']) not in items or int(c['ItemNum']) <= 0 for c in cost):
raise ValueError(f'Cost item missing from Client capture for {aid}/{rank}')
if rank in grouped.setdefault(aid, {}):
raise ValueError(f'Duplicate captured rank for {aid}/{rank}')
grouped[aid][rank] = {'rank': rank, 'cost': [{'ItemID': int(c['ItemID']), 'ItemNum': int(c['ItemNum'])} for c in cost]}
data = json.loads(args.eidolon_config.read_text(encoding='utf8'))
table = data['avatars']
for aid, ranks in grouped.items():
maximum = int(by_avatar[aid]['MaxRank'])
if set(ranks) != set(range(1, maximum + 1)):
raise ValueError(f'Rank capture incomplete for {aid}')
entry = {'max_rank': maximum, 'ranks': [ranks[i] for i in sorted(ranks)]}
if str(aid) in table and table[str(aid)] != entry:
raise ValueError(f'Existing Eidolon entry {aid} differs; refusing replacement')
table[str(aid)] = entry
if set(items) != {int(c['ItemID']) for r in rankrows for c in r['cost']}:
raise ValueError('Captured item list differs from rank costs')
data.setdefault('source', {})['current_client_additions'] = {
'client_assembly_sha256': identity,
'client_catalog_evidence': catalog['evidence_id'],
'rank_capture_sha256': rankhash,
'item_capture_sha256': itemhash,
'method': 'AvatarRankConfigExcelTable.GetData by AvatarConfig RankIDList; ItemHelper.GetItem exact cost IDs',
'avatar_ids': sorted(grouped),
}
temp = args.eidolon_config.with_name(args.eidolon_config.name + '.tmp')
temp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf8')
os.replace(temp, args.eidolon_config)
print(f'Merged {len(grouped)} avatars and {len(rankrows)} verified rank rows')
if __name__ == '__main__':
main()