Hello World
This commit is contained in:
commit
7d342af29f
124 files changed
+627642
No files matched your search
@@ -0,0 +1,122 @@
|
||||
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);
|
||||
Reference in new issue
Block a user