// dispatch_dump.js - walks the game's msg_id -> protobuf-factory hash table // and emits every (msg_id, factory_ptr) pair to Python. // // Reverse-engineered from sub_1449BD0F0 (packet reader). Lookup code: // // v16 = &unk_1498F2D88; // inline fallback slots // if (qword_1498F2D90) v16 = qword_1498F2D90; // or heap index array // idx = v16[msg_id & (dword_1498F2D98 - 1)]; // hash to bucket head // while (idx != -1) { // entry = qword_1498F2D50 + 32 * idx; // 32B per entry // if (*(uint32_t*)entry == msg_id) break; // matched // idx = *(int32_t*)(entry + 24); // chain next // } // factory = *(void**)(entry + 8); // msg = (*(void**)(*factory + 16))(factory, 0); // New() // // Table is populated at app startup; we walk once ~2s after attach. (function () { 'use strict'; var MOD = 'Paralogue-Win64-Shipping.exe'; var mod = Process.getModuleByName(MOD); if (!mod) { send({t:'error', msg:'module not found'}); return; } // Image base in IDA = 0x140000000; global addresses shown are absolute. // Subtract that to get module-relative offsets. var OFF = { buckets_ptr: 0x98F2D50, // qword: &entries[0] used_count: 0x98F2D58, // dword empty_marker: 0x98F2D84, // dword index_inline: 0x98F2D88, // dword[]: inline index array (small-map) index_heap: 0x98F2D90, // qword: &heap-index-array (if grown) hash_size: 0x98F2D98, // dword: size of index array (pow2) }; function dump() { try { var bucketsBase = mod.base.add(OFF.buckets_ptr).readPointer(); var used = mod.base.add(OFF.used_count).readU32(); var empty = mod.base.add(OFF.empty_marker).readU32(); var size = mod.base.add(OFF.hash_size).readU32(); var heap = mod.base.add(OFF.index_heap).readPointer(); var indexArr = heap.isNull() ? mod.base.add(OFF.index_inline) : heap; send({ t: 'dispatch_info', module_base: mod.base.toString(), module_size: mod.size, buckets: bucketsBase.toString(), index_arr: indexArr.toString(), used: used, empty_marker: empty, hash_size: size, inline: heap.isNull(), }); if (size === 0 || size > 0x10000) { send({t:'error', msg:'implausible hash_size=' + size}); return; } var seen = {}; var count = 0; for (var i = 0; i < size; i++) { var idx = indexArr.add(i * 4).readS32(); var hops = 0; while (idx !== -1 && !seen[idx] && hops < 10000) { seen[idx] = true; var entry = bucketsBase.add(idx * 32); var mid = entry.readU32(); var factory = entry.add(8).readPointer(); var nextIdx = entry.add(24).readS32(); // Try dereferencing factory->vtbl to confirm it's a real // pointer. If it faults, we misunderstood the layout. var vtbl = null, firstVtblEntry = null, rawEntry = null; try { vtbl = factory.readPointer(); } catch (e) {} try { firstVtblEntry = vtbl ? vtbl.readPointer() : null; } catch (e) {} try { rawEntry = entry.readByteArray(32); } catch (e) {} send({ t: 'dispatch_entry', msg_id: mid, factory: factory.toString(), factory_rel_base: factory.sub(mod.base).toString(), vtbl: vtbl ? vtbl.toString() : null, first_vtbl_entry: firstVtblEntry ? firstVtblEntry.toString() : null, slot: idx, bucket: i, }, rawEntry); count++; idx = nextIdx; hops++; } } send({t: 'dispatch_done', total: count}); } catch (e) { send({t:'error', msg: 'dispatch walk failed: ' + e.message + '\n' + e.stack}); } } // Table fills during startup; 2s delay covers dynamic registration. setTimeout(dump, 2000); // Also expose a manual re-dump via rpc in case we want to re-check later. rpc.exports = { dump: dump }; })();