Files
GaCy-GO/tools/cw_native_level_capture.js

56 lines
3.3 KiB
JavaScript

// Research-only, source assembly SHA256:
// c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// PE exception table proves both entry RVAs. No dictionary/schema writes.
// Query one named Excel table on a managed caller's thread, at most two rows
// per handbook call. Its native getter can lazily initialize this Excel table;
// the verified loader only reads table rows, and no level graph is invoked.
const plan = globalThis.CW_CAPTURE_PLAN;
if (!plan || !Number.isInteger(plan.maxLevel) || plan.maxLevel <= 0 || plan.maxLevel > 256) {
throw new Error('Supply the captured default maximum player level');
}
const module = Process.getModuleByName('GameAssembly.dll');
const check = (rva, expected) => {
const actual = Array.from(new Uint8Array(module.base.add(rva).readByteArray(expected.length)));
if (actual.some((v, i) => v !== expected[i])) throw new Error('Native entry bytes differ from captured assembly');
};
check(0xdb5f860, [0x55,0x41,0x56,0x56,0x57,0x53]);
check(0x1e3a7880, [0x56,0x48,0x83,0xec,0x60,0x89,0xce]);
// RuntimeMethodHandle.Value + 8 resolved this exact serializer entry; the
// earlier metadata-token match was a different function and is not used.
check(0x1d3efb60, [0x56,0x57,0x48,0x83,0xec,0x28]);
let next = 1, done = false;
const readLevel = new NativeFunction(module.base.add(0x1e3a7880), 'pointer', ['uint32', 'pointer']);
const serialize = new NativeFunction(module.base.add(0x1d3efb60), 'pointer', ['pointer', 'pointer']);
const hook = Interceptor.attach(module.base.add(0xdb5f860), {
onLeave() {
if (done) return;
try {
if (next === 1) send({kind:'TABLE_STATE', initiallyLoaded:module.base.add(0x9869370).readU8() === 1});
for (let n = 0; n < 2 && next <= plan.maxLevel; n++) {
const level = next++;
const row = readLevel(level, ptr(0));
if (row.isNull()) throw new Error('Native player level row is missing: ' + level);
const record = {
PlayerLevel: row.add(0x2c).readU32(), LevelUpExp: row.add(0x20).readU32(),
Rarity1Weight: row.add(0x1c).readU32(), Rarity2Weight: row.add(0x28).readU32(),
Rarity3Weight: row.add(0x18).readU32(), Rarity4Weight: row.add(0x30).readU32(),
Rarity5Weight: row.add(0x24).readU32(), AvatarMaxNumber: row.add(0x34).readU32()
};
if (record.PlayerLevel !== level) throw new Error('Row layout/identity mismatch at level ' + level);
const encoded = serialize(row, ptr(0));
if (encoded.isNull()) throw new Error('Native serializer returned null');
const length = encoded.add(0x10).readS32();
if (length < 2 || length > 65536) throw new Error('Serialized level row exceeds capture bound');
const json = encoded.add(0x14).readUtf16String(length);
const parsed = JSON.parse(json);
for (const key of Object.keys(record)) {
if (parsed[key] !== record[key]) throw new Error('Serialized row disagrees with scalar layout: '+key);
}
send({kind:'ROW', json, thread:Process.getCurrentThreadId()});
}
if (next > plan.maxLevel) { done = true; send({kind:'END', count:plan.maxLevel}); }
} catch (e) { done = true; send({kind:'ERROR', error:String(e)}); }
}
});
send({kind:'BEGIN', count:plan.maxLevel, scope:'native player-level full rows serialized on managed caller thread'});