Hello World

This commit is contained in:
xa o committed 2026-10-30 23:54:07 +07:00
commit 7d342af29f
124 files changed
+627642

No files matched your search

+248
View File
@@ -0,0 +1,248 @@
import * as primitive from './extract.mjs';
import {
NativeCodec,
indexed_entries,
asset_hash
} from './native_codec.mjs';
export {
primitive,
asset_hash
};
const {
fs,
path,
fail,
sha256_file,
sha256,
atomic_json,
cat,
dict,
lookup,
need
} =primitive;
export class Codec extends NativeCodec {
constructor(d='', a='', ev=''){
super(d, a, ev, 'stages');
}
}
export const TABLES={
GridFightDivisionStageRow:['GameCore', 'GridFightDivisionStage'],
GridFightStageRouteConfigRow:['', 'GridFightStageRoute'],
GridFightNodeTemplateConfigRow:['', 'GridFightNodeTemplate'],
GridFightCampConfigRow:['', 'GridFightCamp'],
GridFightCampSeasonConfigRow:['', 'GridFightCampSeason'],
GridFightMonsterConfigRow:['GameCore', 'GridFightMonster'],
GridFightTutorialStageConfigRow:['GameCore', 'GridFightTutorialStage'],
GridFightTutorialStageNodeConfigRow:['GameCore', 'GridFightTutorialStageNode'],
GridFightPlayerLevelConfigRow:['GameCore', 'GridFightPlayerLevel'],
GridFightLevelConfigRow:['GameCore', 'GridFightLevelV2'],
GridFightStageInfoConfigRow:['GameCore', 'GridFightStage'],
GridFightStageNodeDisplayConfigRow:['GameCore', 'GridFightNodeTypeShow'],
StageRow:['GameCore', 'StageConfig'],
ConstValueCommonRow:['GameCore', 'GridFightConstCommon']
};
export class Assets {
constructor(client){
this.client=client;
this.layers=[];
this.catalogues=[];
this.blob_hashes={
};
for(const layer of ['Persistent', 'StreamingAssets']){
const dir=path.join(client, 'StarRail_Data', layer, 'DesignData/Windows'),
cats=primitive.globBytes(dir).filter(p=>path.basename(p).startsWith('DesignV_'));
if(cats.length!==1)fail('Expected exactly one DesignV in '+layer);
const data=fs.readFileSync(cats[0]),
entries=new Map();
for(const blob of primitive.globBytes(dir)){
const stem=path.basename(blob, '.bytes');
if(!/^[a-fA-F0-9]{32}$/.test(stem))continue;
for(const entry of indexed_entries(data, stem, fs.statSync(blob).size)){
if(entries.has(entry.hash))fail('Duplicate asset key');
entries.set(entry.hash, {
...entry,
blob:path.relative(client, blob)
});
}
}
this.layers.push(entries);
this.catalogues.push({
path:path.relative(client, cats[0]),
sha256:sha256_file(cats[0])
});
}
}
get(p){
const key=asset_hash(p);
for(const entries of this.layers)if(entries.has(key)){
const entry=entries.get(key),
blob=path.join(this.client, entry.blob),
fd=fs.openSync(blob, 'r'),
data=Buffer.alloc(entry.length);
try{
const n=fs.readSync(fd, data, 0, data.length, entry.offset);
if(n!==entry.length)fail('Truncated indexed asset');
} finally{
fs.closeSync(fd);
}
this.blob_hashes[entry.blob]??=sha256_file(blob);
return [data, {
asset:p,
...entry,
blob_sha256:this.blob_hashes[entry.blob],
asset_sha256:sha256(data)
}];
}
fail('Missing indexed asset: '+p);
}
}
export function table(assets, codec, name, suffix, stem){
const [raw, proof]=assets.get(`BakedConfig/ExcelOutput${suffix}/${stem}.bytes`),
prefix=name==='StageRow'?Buffer.alloc(0):Buffer.from([0]);
if(prefix.length&&raw[0]!==0)fail('Unsupported Excel frame: '+name);
const cur=new primitive.Cursor(raw, prefix.length),
count=cur.length(1000000),
typ='RPG.GameCore.'+name,
rows=Array.from({
length:count
}, ()=>codec.record(cur, typ)),
encoded=cat(prefix, primitive.encode_varint(count*2), rows.map(r=>codec.encode_record(r, typ)));
if(cur.at!==raw.length||!encoded.equals(raw))fail('Whole indexed table round-trip failed: '+name);
return [rows, {
table:name,
records:count,
round_trip:true,
...proof
}];
}
export function contracts(t){
const routes=t.GridFightStageRouteConfigRow,
nodes=new Map(t.GridFightNodeTemplateConfigRow.map(r=>[r.NodeTemplateID, r])),
stages=new Map(t.StageRow.map(r=>[r.StageID, r])),
displays=new Map(t.GridFightStageNodeDisplayConfigRow.map(r=>[r.NodeType, r]));
if(nodes.size!==t.GridFightNodeTemplateConfigRow.length)fail('Duplicate node template');
if(!primitive.uniqueValues(routes, r=>[r.ID, r.ChapterID, r.SectionID].join(':')))fail('Duplicate route/chapter/section');
const kinds={
1:'battle',
2:'battle',
3:'elite',
4:'elite',
5:'shop'
},
rounds=routes.map(route=>{
const node=need(nodes, route.NodeTemplateID),
stage=need(stages, node.StageID),
display=need(displays, node.NodeType);
if(!(node.NodeType in kinds))fail('Unknown native node type');
return {
stage:`${route.ChapterID}-${route.SectionID}`,
kind:kinds[node.NodeType],
monster_group:null,
route_id:route.ID,
chapter_id:route.ChapterID,
section_id:route.SectionID,
node_template_id:node.NodeTemplateID,
stage_id:node.StageID,
node_type:node.NodeType,
node_name:display.NodeName,
raw:{
GridFightStageRouteConfigRow:route,
GridFightNodeTemplateConfigRow:node,
StageRow:stage,
GridFightStageNodeDisplayConfigRow:display
}
};
});
const lr=t.GridFightPlayerLevelConfigRow,
levels=dict(lr.map(r=>[r.PlayerLevel, r.LevelUpExp??0]));
if(Object.keys(levels).map(Number).sort((a, b)=>a-b).some((k, i)=>k!==i+1)||Object.keys(levels).length!==lr.length)fail('Non-contiguous player level table');
const tutorialRows=t.GridFightTutorialStageConfigRow,
tutorialNodes=t.GridFightTutorialStageNodeConfigRow,
tutorial={
stages:tutorialRows.map(r=>({
division_id:r.DivisionID,
raw:{
GridFightTutorialStageConfigRow:r,
GridFightTutorialStageNodeConfigRow:tutorialNodes.filter(n=>n.DivisionID===r.DivisionID)
}
})),
raw:{
GridFightDivisionStageRow:t.GridFightDivisionStageRow.filter(r=>tutorialRows.some(s=>s.DivisionID===r.DivisionID))
},
observed_start_gameplay:{
values:[1, 1],
source:'user-provided client observation',
meaning:'Do not equate these values to route/chapter/section without protocol evidence.'
}
};
if(t.ConstValueCommonRow){
const constants=Object.fromEntries(t.ConstValueCommonRow.map(r=>[r.ConstValueName, r.Value]));
tutorial.raw.GridFightConstCommon=t.ConstValueCommonRow;
const first=constants.GridFight_FirstPlay_StageRouteID;
if(!first||first.wire_tag!==0)fail('Missing integer first-play route constant');
tutorial.first_play_route_id=first.value;
}
const shared=Object.fromEntries(Object.entries(t).filter(([n])=>n!=='StageRow'));
const ids=new Set([...nodes.values()].map(n=>n.StageID));
shared.StageRow=t.StageRow.filter(r=>ids.has(r.StageID));
return {
'rounds.json':rounds,
'tutorial.json':tutorial,
'levels.json':levels,
'rounds.raw.json':shared,
'levels.raw.json':lr
};
}
export function extractionContext(a){
const identity=primitive.loadJSON(a.identity);
if(sha256_file(a.dump)!==identity.dump_sha256||sha256_file(path.join(a.client, 'GameAssembly.dll'))!==identity.game_assembly_sha256)fail('Metadata or installed assembly does not match identity manifest');
return {
identity,
codec:new Codec(primitive.readText(a.dump), path.join(a.client, 'GameAssembly.dll'), path.join(a.output, 'evidence')),
assets:new Assets(a.client)
};
}
export function collect(a, TABLES, ctx){
const t={
},
proofs=[];
for(const [name, [suffix, stem]]of Object.entries(TABLES)){
const [rows, proof]=table(ctx.assets, ctx.codec, name, suffix, stem);
t[name]=rows;
proofs.push(proof);
console.log(`${name}: ${rows.length} whole-asset rows`);
}
return [t, proofs];
}
export function publish(a, ctx, t, proofs, outputs, prefix, extra){
for(const [n, v]of Object.entries(outputs))atomic_json(path.join(a.output, n), v);
atomic_json(path.join(a.output, prefix+'-schemas.json'), ctx.codec.schemas);
atomic_json(path.join(a.output, prefix+'-provenance.json'), {
source:'installed-client-files',
dump_sha256:ctx.identity.dump_sha256,
game_assembly_sha256:ctx.identity.game_assembly_sha256,
catalogues:ctx.assets.catalogues,
tables:proofs,
outputs:Object.fromEntries(Object.keys(outputs).map(n=>[n, sha256_file(path.join(a.output, n))])),
...extra
});
}
export function main(){
const a=primitive.cli(['client', 'dump', 'identity', 'output']),
ctx=extractionContext(a),
reader=ctx.codec.verify_dynamic_reader();
const [t, proofs]=collect(a, TABLES, ctx);
publish(a, ctx, t, proofs, contracts(t), 'stage', {
dynamic_reader:reader,
mapping:{
levels:'GridFightPlayerLevelConfigRow.LevelUpExp; absent scalar is zero',
'rounds.stage':'ChapterID-SectionID; route_id is required to disambiguate',
'rounds.kind':'Monster/CampMonster=battle; EliteBranch/Boss=elite; Supply=shop (shop icon)',
'rounds.node_name':'Native TextID pair from GridFightNodeTypeShow; client may override via plugins',
'rounds.monster_group':'null: ParamList is not proved to be one monster-group ID'
},
unresolved:['NodeTemplate.ParamList semantics and dynamic camp/branch selection', 'Tutorial JsonPath modifier assets and exact StartGamePlay(1,1) selector binding', 'JKFAMDHMLBC.GAJICDKPHPA/BDCOFIOCJFO and AOBAIBBHDEL.EEBLMLAEFHO/IMDPJFEGIDF', 'StageConfigData.BFLIFKBEOPJ/MNDFOPKBHKP; kept with native names'],
gameplay_verified:false
});
}
primitive.runMain(import.meta.url, main);