import * as extract from './extract.mjs'; import * as stages from './stages.mjs'; const e=extract; export function text_rows(blob){ const cursor=new e.Cursor(blob), count=cursor.length(1000000), result=e.dict(); for(let i=0; i=blob.length)e.fail('Truncated TextMap flags'); const flags=blob[cursor.at++]; if(flags&~7)e.fail('Invalid TextMap row flags'); if(flags&1)cursor.varint(); const key=flags&2?cursor.varint():0, size=Number(cursor.varint()); if(cursor.at+size>blob.length)e.fail('Truncated TextMap text'); const text=new TextDecoder('utf-8', { fatal:true }).decode(blob.subarray(cursor.at, cursor.at+size)); cursor.at+=size; if(flags&4)cursor.varint(); if(key){ if(key in result&&result[key]!==text)e.fail('Conflicting TextMap key'); result[key]=text; } } if(cursor.length(1000000)!==count)e.fail('Invalid TextMap index count'); return result; } export function main(){ const a=e.cli(['client', 'dump', 'resources', 'english', 'thai', 'output', 'evidence']), load=name=>e.loadJSON(e.path.join(a.resources, name)), grid=name=>load('grid_fight/'+name+'.json'), codec=new stages.Codec(e.readText(a.dump), e.path.join(a.client, 'GameAssembly.dll'), a.evidence), [divisions, proof]=stages.table(new stages.Assets(a.client), codec, 'GridFightDivisionLevelShowRow', '', 'GridFightDivisionLevelShow'), blobs={ en:e.fs.readFileSync(a.english), th:e.fs.readFileSync(a.thai) }, maps=Object.fromEntries(Object.entries(blobs).map(([lang, b])=>[lang, text_rows(b)])), keys=e.dict(); function take(value){ if(Array.isArray(value)){ value.forEach(take); return; } if(value&&typeof value==='object'){ if(value.hash&&value.hash64){ const k=String(value.hash); if(k in keys&&e.json(keys[k])!==e.json(value))e.fail('TextID hash32 collision; use full keys before exporting'); keys[k]=value; } else Object.values(value).forEach(take); } } take(divisions); take(grid('season').raw.divisions.map(r=>r.DivisionName)); take(grid('rounds').map(r=>r.node_name)); take(grid('preparation').camps.map(r=>r.raw.GridFightCampConfigRow.CampName)); take(grid('petpal').map(r=>r.raw.Name)); take(grid('traits').map(r=>r.raw.base.TraitName)); const itemRaw=grid('items.raw'); take(itemRaw); const hashes=(rows,id,name)=>e.dict(rows.filter(r=>r[name]?.hash).map(r=>[r[id],r[name].hash])), item_hashes=hashes(itemRaw.GridFightItemConfigRow,'ID','ItemName'), augment_hashes=hashes(itemRaw.GridFightAugmentConfigRow,'ID','HexName'), portal_hashes=hashes(itemRaw.GridFightPortalBuffConfigRow,'ID','PortalBuffTitle'), trait_hashes=e.dict(grid('traits').map(r=>[r.id,r.raw.base.TraitName.hash])); const avatars=load('AvatarConfig.json').avatar_config, avatarIds=new Set(grid('roles').map(r=>r.avatar_id)), avatarHashes=e.dict(); for(const avatar of avatars){ const id=avatar.AvatarName??{ }; if(avatarIds.has(avatar.AvatarID)&&id.hash){ take(id); avatarHashes[avatar.AvatarID]=id.hash; } } const texts=e.dict(); for(const [key, id]of Object.entries(keys)){ const values=Object.fromEntries(Object.entries(maps).filter(([, rows])=>rows[id.hash64]).map(([lang, rows])=>[lang, rows[id.hash64]])); if(Object.keys(values).length)texts[key]=values; } const bosses=new Set(grid('preparation').camps.flatMap(c=>c.monsters.filter(r=>r.tier===5).map(r=>r.id))), monsters=e.dict(), reverse=new Map(); for(const [key, text]of Object.entries(maps.en))if(key in maps.th){ if(!reverse.has(text))reverse.set(text, new Set()); reverse.get(text).add(maps.th[key]); } for(const monster of load('AeonCatalog.json').monsters){ if(!bosses.has(monster.id))continue; const translations=reverse.get(monster.name)??new Set(); monsters[monster.id]={ en:monster.name, ...(translations.size===1?{ th:[...translations][0] } :{ }) }; } const output={ source:{ text_blobs:Object.fromEntries([['en', a.english], ['th', a.thai]].map(([lang, p])=>[lang, { file:e.path.basename(p), sha256:e.sha256(blobs[lang]) }])) }, division_show_proof:proof, rank_templates:e.dict(divisions.map(r=>[`${r.SeasonID}:${r.DivisionLevel??0}`, { name:r.DivisionNameWithNum.hash, abbr:r.DivisionAbbr?.hash??0 }])), texts, monsters, avatar_hashes:avatarHashes, item_hashes, augment_hashes, portal_hashes, trait_hashes }; e.atomic_json(a.output, output); console.log(`Resolved ${Object.keys(texts).length}/${Object.keys(keys).length} names; ${Object.values(texts).filter(r=>'th'in r).length} Thai names; division asset round-trip verified.`); } e.runMain(import.meta.url, main);