98 lines
4.5 KiB
Python
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()
|