123 lines
4.7 KiB
JavaScript
123 lines
4.7 KiB
JavaScript
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<count; i++){
|
|
if(cursor.at>=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);
|