Files
Currency-War-SR/tools/gridfight-extract-node/ui_names.mjs
T
2026-10-30 23:54:07 +07:00

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);