Files
2026-10-30 23:54:07 +07:00

105 lines
4.4 KiB
JavaScript

import * as e from './extract.mjs';
import * as s from './stages.mjs';
import {table_bindings} from './rounds.mjs';
// Client tables behind the Elite branch (binary node rule, enemy difficulty) and
// the SpecialGoods-gated projections. Every table is decoded byte-exactly.
export const TYPES = [
'GridFightBinaryNodeRuleConfigRow',
'GridFightBinaryDiffAddRuleConfigRow',
'GridFightSpecialGoodsConfigRow',
'GridFightProjectionConfigRow',
'GridFightEnemyDifficultyLvConfigRow'
];
// GridFightProjectionUnlockType: 0 TraitUnlock, 1 SpecialGoods.
export const UNLOCK_SPECIAL_GOODS = 1;
// Update-union case 1684 (CDKJHJNFDIH) -> GridFightModule.UpdateUnlockProjectionID (DC17B10).
export const UNLOCK_PROJECTION_CASE = 1684;
// SpecialGoods JsonPath folder names, keyed by GroupID (1 Cyrene, 2 Aha, 3 Aha upper).
export const SERIES = ['Cyrene', 'Aha'];
// FixPoint wire is signed Q32.32.
export function fixed(p) {
return Math.round(Number(BigInt.asIntN(64, BigInt(p?.wire ?? 0))) / 4294967296 * 1e6) / 1e6;
}
export function eliteResource(t) {
const node_rules = t.GridFightBinaryNodeRuleConfigRow.map(r => ({
id: r.ID,
// Elite nodes carry ParamList[0] = family; option id = family * 100 + quality.
family: r.ID >= 100 ? Math.floor(r.ID / 100) : 0,
quality: r.Quality ?? 0,
perform_level: r.PerformLevel ?? 0
})).sort((a, b) => a.id - b.id);
for (const r of node_rules) {
if (r.id >= 100 && r.id !== r.family * 100 + r.quality) e.fail('Binary node rule id does not follow family*100+quality: ' + r.id);
}
const diff_add = t.GridFightBinaryDiffAddRuleConfigRow.map(r => ({
rule: r.ID, quality: r.Quality, add: r.EnemyDifficultyAddValue ?? 0
})).sort((a, b) => a.rule - b.rule || a.quality - b.quality);
const difficulty = t.GridFightEnemyDifficultyLvConfigRow.map(r => ({
chapter: r.ChapterID ?? 0,
level: r.EnemyDifficultyLevel ?? 0,
attack: fixed(r.AttackRatio),
defence: fixed(r.DefenceRatio),
hp: fixed(r.HPRatio),
speed: fixed(r.SpeedRatio),
stance: fixed(r.StanceRatio),
damage_reduce: fixed(r.AllDamageReduce)
})).sort((a, b) => a.chapter - b.chapter || a.level - b.level);
return {schema: 1, node_rules, diff_add, difficulty, extra_rewards: []};
}
export function goodsResource(t) {
const projections = t.GridFightProjectionConfigRow.filter(r => r.UnlockType === UNLOCK_SPECIAL_GOODS).map(r => ({
id: r.ID,
role_id: r.RoleID,
rarity: r.Rarity,
mazebuff_id: r.MazebuffID,
params: (r.ParamList ?? []).map(fixed)
})).sort((a, b) => a.id - b.id);
const special_goods = t.GridFightSpecialGoodsConfigRow.map(g => {
const series = SERIES.find(n => g.JsonPath.includes('/' + n + '/'));
if (!series) e.fail('Unknown SpecialGoods series: ' + g.JsonPath);
return {
id: g.ID,
group: g.GroupID,
series,
cost: g.Cost ?? 0,
quality: g.Quality ?? 0,
lower_good_id: g.LowerGoodID ?? 0,
params: (g.EffectParamList ?? []).map(fixed)
};
}).sort((a, b) => a.id - b.id);
// The good -> projection entitlement lives in the SpecialGoods JsonPath assets,
// which no installed table or tool decodes. Kept empty until proven.
return {schema: 1, unlock_case: UNLOCK_PROJECTION_CASE, projections, special_goods, grants: []};
}
export function main() {
const a = e.cli(['client', 'dump', 'identity', 'literals', 'output']);
const ctx = s.extractionContext(a);
const literals = new Map(e.loadJSON(a.literals).map(r => [Number(BigInt(r.Address)), r.Value]));
const tables = {}, proofs = [];
for (const name of TYPES) {
const assets = table_bindings(ctx.codec, literals, 'RPG.GameCore.' + name, null);
tables[name] = [];
for (const asset of assets) {
const match = asset.match(/^BakedConfig\/ExcelOutput(GameCore)?\/(\w+)\.bytes$/);
if (!match) e.fail('Unsupported table path: ' + asset);
const [rows, proof] = s.table(ctx.assets, ctx.codec, name, match[1] ?? '', match[2]);
tables[name].push(...rows);
proofs.push(proof);
console.log(`${name}: ${rows.length} byte-exact rows`);
}
}
const source = {dump_sha256: ctx.identity.dump_sha256, game_assembly_sha256: ctx.identity.game_assembly_sha256, tables: TYPES};
s.publish(a, ctx, tables, proofs, {
'rewards.raw.json': tables,
'elite_rewards.json': {...eliteResource(tables), source},
'projection_goods.json': {...goodsResource(tables), source}
}, 'rewards', {gameplay_verified: false});
}
e.runMain(import.meta.url, main);