56 lines
3.3 KiB
JavaScript
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'});
|