105 lines
4.4 KiB
JavaScript
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);
|