492 lines
19 KiB
TypeScript
492 lines
19 KiB
TypeScript
/**
|
|
* Deterministic 1.13c Binary Table Extractor (`D2Net.dll`, `Fog.dll`, `D2Client.dll`).
|
|
*
|
|
* Reads the authoritative Diablo II: Lord of Destruction v1.13c DLLs from
|
|
* `/usr/local/google/home/taodao/d2-data` (or `D2_DATA_DIR`), verifies their SHA-1
|
|
* digests, and generates:
|
|
* - `src/netproto/d2gs/tables/s2c-sizes.ts`
|
|
* - `src/netproto/d2gs/tables/huffman.ts`
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto'
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const ROOT_DIR = resolve(__dirname, '..')
|
|
const D2_DATA_DIR = process.env.D2_DATA_DIR ?? '/usr/local/google/home/taodao/d2-data'
|
|
|
|
const EXPECTED_D2NET_SHA1 = '4b2579f301bedc03d2510c911949584d372f59ce'
|
|
const EXPECTED_FOG_SHA1 = 'fc9e40e6b81e8c65703afdaaae010aace85d0969'
|
|
const EXPECTED_D2CLIENT_SHA1 = '26c2b536744ec8bba05dc43150b179313404d5ee'
|
|
|
|
// D2Net.dll image base & table offsets (1.13c)
|
|
const D2NET_IMAGE_BASE = 0x6fbf0000
|
|
const D2NET_S2C_TABLE_OFFSET = 0x0000a900
|
|
const D2NET_S2C_COUNT = 181 // 0x00..0xB4
|
|
const D2NET_C2S_TABLE_OFFSET = 0x0000abd8
|
|
const D2NET_C2S_COUNT = 113 // 0x00..0x70
|
|
const D2NET_VARLEN_FN_VA = 0x6fbf6750
|
|
const D2NET_SWITCH_INDEX_OFFSET = 0x00006970
|
|
const D2NET_SWITCH_TABLE_OFFSET = 0x00006934
|
|
|
|
// Fog.dll image base & Huffman table offsets (1.13c)
|
|
const FOG_IMAGE_BASE = 0x6ff50000
|
|
const FOG_HUFFMAN_LENGTHS_OFFSET = 0x0002f210
|
|
const FOG_HUFFMAN_MASKS_OFFSET = 0x0002f310
|
|
const FOG_HUFFMAN_BUILD_FN_VA = 0x6ff6efd0
|
|
const FOG_HUFFMAN_DECOMPRESS_FN_VA = 0x6ff6eda0
|
|
|
|
// D2Client.dll image base & S->C handler table offset (1.13c)
|
|
const D2CLIENT_IMAGE_BASE = 0x6fab0000
|
|
const D2CLIENT_S2C_HANDLERS_OFFSET = 0x000dde60
|
|
const D2CLIENT_S2C_HANDLERS_COUNT = 175 // 0x00..0xAE
|
|
|
|
function sha1Hex(buf: Uint8Array): string {
|
|
return createHash('sha1').update(buf).digest('hex')
|
|
}
|
|
|
|
function readInt32ArrayLE(buf: Buffer, offset: number, count: number): number[] {
|
|
const out: number[] = []
|
|
for (let i = 0; i < count; i++) {
|
|
out.push(buf.readInt32LE(offset + i * 4))
|
|
}
|
|
return out
|
|
}
|
|
|
|
function readUint32ArrayLE(buf: Buffer, offset: number, count: number): number[] {
|
|
const out: number[] = []
|
|
for (let i = 0; i < count; i++) {
|
|
out.push(buf.readUInt32LE(offset + i * 4))
|
|
}
|
|
return out
|
|
}
|
|
|
|
interface CanonicalHuffmanEntry {
|
|
readonly symbol: number
|
|
readonly bitLength: number
|
|
readonly code: number
|
|
}
|
|
|
|
/**
|
|
* Replicates `Fog.10219` (`0x6ff6efd0`) canonical MSB-first Huffman code assignment.
|
|
*/
|
|
function computeCanonicalHuffman(bitLengths: readonly number[]): CanonicalHuffmanEntry[] {
|
|
const sortedSymbols = Array.from({ length: 256 }, (_, i) => i)
|
|
sortedSymbols.sort((a, b) => {
|
|
const lenDiff = bitLengths[b]! - bitLengths[a]!
|
|
if (lenDiff !== 0) return lenDiff
|
|
return a - b
|
|
})
|
|
|
|
const codes = new Array<number>(256).fill(0)
|
|
let code = 0
|
|
codes[sortedSymbols[0]!] = 0
|
|
for (let i = 0; i < 255; i++) {
|
|
const symCur = sortedSymbols[i]!
|
|
const symNext = sortedSymbols[i + 1]!
|
|
const shift = bitLengths[symCur]! - bitLengths[symNext]!
|
|
code = (code + 1) >>> shift
|
|
codes[symNext] = code
|
|
}
|
|
|
|
return Array.from({ length: 256 }, (_, sym) => ({
|
|
symbol: sym,
|
|
bitLength: bitLengths[sym]!,
|
|
code: codes[sym]!,
|
|
}))
|
|
}
|
|
|
|
function formatHex(n: number, width = 2): string {
|
|
return '0x' + (n >>> 0).toString(16).padStart(width, '0')
|
|
}
|
|
|
|
function generateS2cSizesTs(
|
|
d2netSha1: string,
|
|
d2clientSha1: string,
|
|
s2cSizes: readonly number[],
|
|
c2sSizes: readonly number[],
|
|
varLenJumpTargets: ReadonlyMap<number, number>,
|
|
d2clientHandlers: readonly { opcode: number; handlerVa: number; expectedSize: number; unitHandlerVa: number }[],
|
|
): string {
|
|
const s2cRows: string[] = []
|
|
for (let i = 0; i < s2cSizes.length; i += 16) {
|
|
const slice = s2cSizes.slice(i, Math.min(i + 16, s2cSizes.length))
|
|
const comment = `/* ${formatHex(i)}..${formatHex(Math.min(i + 15, s2cSizes.length - 1))} */`
|
|
s2cRows.push(` ${comment} ${slice.map(v => String(v).padStart(3, ' ')).join(', ')},`)
|
|
}
|
|
|
|
const c2sRows: string[] = []
|
|
for (let i = 0; i < c2sSizes.length; i += 16) {
|
|
const slice = c2sSizes.slice(i, Math.min(i + 16, c2sSizes.length))
|
|
const comment = `/* ${formatHex(i)}..${formatHex(Math.min(i + 15, c2sSizes.length - 1))} */`
|
|
c2sRows.push(` ${comment} ${slice.map(v => String(v).padStart(3, ' ')).join(', ')},`)
|
|
}
|
|
|
|
const varLenEntries: string[] = []
|
|
for (const [opcode, targetVa] of varLenJumpTargets.entries()) {
|
|
varLenEntries.push(` ${formatHex(opcode)}: ${formatHex(targetVa, 8)},`)
|
|
}
|
|
|
|
const handlerEntries: string[] = []
|
|
for (const h of d2clientHandlers) {
|
|
handlerEntries.push(
|
|
` { opcode: ${formatHex(h.opcode)}, handlerVa: ${formatHex(h.handlerVa, 8)}, expectedSize: ${String(h.expectedSize).padStart(3, ' ')}, unitHandlerVa: ${formatHex(h.unitHandlerVa, 8)} },`,
|
|
)
|
|
}
|
|
|
|
return `/**
|
|
* AUTO-GENERATED by \`scripts/extract-d2net-tables.ts\` — DO NOT EDIT MANUALLY.
|
|
*
|
|
* Ground-Truth 1.13c Binary Sources:
|
|
* - \`D2Net.dll\` SHA-1: \`${d2netSha1}\` (ImageBase \`${formatHex(D2NET_IMAGE_BASE, 8)}\`)
|
|
* - S->C Size Table: VA \`${formatHex(D2NET_IMAGE_BASE + D2NET_S2C_TABLE_OFFSET, 8)}\`, FileOffset \`${formatHex(D2NET_S2C_TABLE_OFFSET, 6)}\` (${D2NET_S2C_COUNT} entries, opcodes 0x00..0xB4)
|
|
* - S->C Variable-Length Resolver \`D2NET_10033\`: VA \`${formatHex(D2NET_VARLEN_FN_VA, 8)}\`, FileOffset \`0x006750\`
|
|
* - C->S Size Table: VA \`${formatHex(D2NET_IMAGE_BASE + D2NET_C2S_TABLE_OFFSET, 8)}\`, FileOffset \`${formatHex(D2NET_C2S_TABLE_OFFSET, 6)}\` (${D2NET_C2S_COUNT} entries, opcodes 0x00..0x70)
|
|
* - \`D2Client.dll\` SHA-1: \`${d2clientSha1}\` (ImageBase \`${formatHex(D2CLIENT_IMAGE_BASE, 8)}\`)
|
|
* - S->C Handler Table: VA \`${formatHex(D2CLIENT_IMAGE_BASE + D2CLIENT_S2C_HANDLERS_OFFSET, 8)}\`, FileOffset \`${formatHex(D2CLIENT_S2C_HANDLERS_OFFSET, 6)}\` (${D2CLIENT_S2C_HANDLERS_COUNT} entries, opcodes 0x00..0xAE)
|
|
*/
|
|
|
|
export const D2NET_DLL_SHA1 = '${d2netSha1}' as const
|
|
export const D2CLIENT_DLL_SHA1 = '${d2clientSha1}' as const
|
|
|
|
export const D2NET_S2C_TABLE_VA = ${formatHex(D2NET_IMAGE_BASE + D2NET_S2C_TABLE_OFFSET, 8)} as const
|
|
export const D2NET_S2C_TABLE_FILE_OFFSET = ${formatHex(D2NET_S2C_TABLE_OFFSET, 6)} as const
|
|
export const D2NET_VARLEN_RESOLVER_VA = ${formatHex(D2NET_VARLEN_FN_VA, 8)} as const
|
|
|
|
export const D2NET_C2S_TABLE_VA = ${formatHex(D2NET_IMAGE_BASE + D2NET_C2S_TABLE_OFFSET, 8)} as const
|
|
export const D2NET_C2S_TABLE_FILE_OFFSET = ${formatHex(D2NET_C2S_TABLE_OFFSET, 6)} as const
|
|
|
|
export const D2CLIENT_S2C_HANDLERS_VA = ${formatHex(D2CLIENT_IMAGE_BASE + D2CLIENT_S2C_HANDLERS_OFFSET, 8)} as const
|
|
export const D2CLIENT_S2C_HANDLERS_FILE_OFFSET = ${formatHex(D2CLIENT_S2C_HANDLERS_OFFSET, 6)} as const
|
|
|
|
/**
|
|
* 181-entry S->C packet size table (\`0x00..0xB4\`) from \`D2Net.dll\` @ \`0x6fbfa900\`.
|
|
* - \`> 0\`: fixed byte length (including the 1-byte opcode)
|
|
* - \`-1\`: variable length resolved by \`D2NET_10033\` (\`0x6fbf6750\`)
|
|
* - \`0\`: invalid / unused opcode
|
|
*/
|
|
export const S2C_PACKET_SIZES: readonly number[] = [
|
|
${s2cRows.join('\n')}
|
|
] as const
|
|
|
|
/**
|
|
* 113-entry C->S packet size table (\`0x00..0x70\`) from \`D2Net.dll\` @ \`0x6fbfabd8\`.
|
|
* - \`> 0\`: fixed byte length (including the 1-byte opcode)
|
|
* - \`-1\`: variable length resolved by \`D2NET_10006\` (\`0x6fbf65d0\`)
|
|
* - \`0\`: invalid / unused opcode
|
|
*/
|
|
export const C2S_PACKET_SIZES: readonly number[] = [
|
|
${c2sRows.join('\n')}
|
|
] as const
|
|
|
|
/**
|
|
* Switch jump targets in \`D2Net.dll\` \`D2NET_10033\` (\`0x6fbf6750\`) for the 14 variable-length S->C opcodes.
|
|
*/
|
|
export const S2C_VARLEN_SWITCH_VAS: Readonly<Record<number, number>> = {
|
|
${varLenEntries.join('\n')}
|
|
} as const
|
|
|
|
export interface D2ClientHandlerEntry {
|
|
readonly opcode: number
|
|
readonly handlerVa: number
|
|
readonly expectedSize: number
|
|
readonly unitHandlerVa: number
|
|
}
|
|
|
|
/**
|
|
* 175-entry S->C handler table (\`0x00..0xAE\`) from \`D2Client.dll\` @ \`0x6fb8de60\`.
|
|
*/
|
|
export const D2CLIENT_S2C_HANDLERS: readonly D2ClientHandlerEntry[] = [
|
|
${handlerEntries.join('\n')}
|
|
] as const
|
|
|
|
export type S2cSizeResolution =
|
|
| { readonly status: 'ok'; readonly size: number }
|
|
| { readonly status: 'incomplete' }
|
|
| { readonly status: 'invalid'; readonly opcode: number; readonly reason: string }
|
|
|
|
/**
|
|
* Exact implementation of \`D2NET_10033_GetServerPacketSize\` (\`0x6fbf6750\`) and caller
|
|
* buffer check (\`0x6fbf6db6\`).
|
|
*
|
|
* @param buf - Buffer containing decompressed S->C packet stream.
|
|
* @param offset - Start index of the packet in \`buf\`.
|
|
* @param availableLen - Number of readable bytes starting at \`offset\`.
|
|
*/
|
|
export function resolveS2cPacketSize(
|
|
buf: Uint8Array,
|
|
offset = 0,
|
|
availableLen = buf.length - offset,
|
|
): S2cSizeResolution {
|
|
if (availableLen <= 0) {
|
|
return { status: 'incomplete' }
|
|
}
|
|
const opcode = buf[offset]!
|
|
if (opcode >= S2C_PACKET_SIZES.length) {
|
|
return { status: 'invalid', opcode, reason: \`opcode \${opcode} >= 0xB5 out of range\` }
|
|
}
|
|
const tableEntry = S2C_PACKET_SIZES[opcode]!
|
|
if (tableEntry === 0) {
|
|
return { status: 'invalid', opcode, reason: \`opcode 0x\${opcode.toString(16).padStart(2, '0')} has size 0 in D2Net.dll table\` }
|
|
}
|
|
if (tableEntry > 0) {
|
|
if (availableLen < tableEntry) {
|
|
return { status: 'incomplete' }
|
|
}
|
|
return { status: 'ok', size: tableEntry }
|
|
}
|
|
|
|
// Variable-length S->C packets (tableEntry === -1), matching D2NET_10033 switch @ 0x6fbf6934
|
|
let size = 0
|
|
switch (opcode) {
|
|
case 0x16: {
|
|
// VA 0x6fbf67a4: u16 LE at +1 (min 3 bytes for header; D2Net checks >= 13)
|
|
if (availableLen < 3) return { status: 'incomplete' }
|
|
size = buf[offset + 1]! | (buf[offset + 2]! << 8)
|
|
break
|
|
}
|
|
case 0x26: {
|
|
// VA 0x6fbf67b8: 10-byte fixed prefix + null-terminated charName + null-terminated text
|
|
if (availableLen < 10) return { status: 'incomplete' }
|
|
let p = offset + 10
|
|
const end = offset + availableLen
|
|
while (p < end && buf[p] !== 0) p++
|
|
if (p >= end) return { status: 'incomplete' }
|
|
p++ // skip first NUL
|
|
while (p < end && buf[p] !== 0) p++
|
|
if (p >= end) return { status: 'incomplete' }
|
|
p++ // skip second NUL
|
|
size = p - offset
|
|
break
|
|
}
|
|
case 0x3e: {
|
|
// VA 0x6fbf6834: u8 size at +1
|
|
if (availableLen < 2) return { status: 'incomplete' }
|
|
size = buf[offset + 1]!
|
|
break
|
|
}
|
|
case 0x5b: {
|
|
// VA 0x6fbf67f4: u16 LE size at +1 (PlayerInGame, min 34 bytes in D2Net)
|
|
if (availableLen < 3) return { status: 'incomplete' }
|
|
size = buf[offset + 1]! | (buf[offset + 2]! << 8)
|
|
break
|
|
}
|
|
case 0x94: {
|
|
// VA 0x6fbf6808: BaseSkillLevels, (buf[1] + 2) * 3 = 6 + 3 * count
|
|
if (availableLen < 2) return { status: 'incomplete' }
|
|
size = (buf[offset + 1]! + 2) * 3
|
|
break
|
|
}
|
|
case 0x9c: {
|
|
// VA 0x6fbf6820: ItemActionWorld, u8 size at +2
|
|
if (availableLen < 3) return { status: 'incomplete' }
|
|
size = buf[offset + 2]!
|
|
break
|
|
}
|
|
case 0x9d: {
|
|
// VA 0x6fbf6848: ItemActionOwned, u8 size at +2
|
|
if (availableLen < 3) return { status: 'incomplete' }
|
|
size = buf[offset + 2]!
|
|
break
|
|
}
|
|
case 0xa6: {
|
|
// VA 0x6fbf685a: u16 LE size at +2
|
|
if (availableLen < 4) return { status: 'incomplete' }
|
|
size = buf[offset + 2]! | (buf[offset + 3]! << 8)
|
|
break
|
|
}
|
|
case 0xa8: {
|
|
// VA 0x6fbf686e: SetState, u8 size at +6
|
|
if (availableLen < 7) return { status: 'incomplete' }
|
|
size = buf[offset + 6]!
|
|
break
|
|
}
|
|
case 0xaa: {
|
|
// VA 0x6fbf6886: AddUnit, u8 size at +6
|
|
if (availableLen < 7) return { status: 'incomplete' }
|
|
size = buf[offset + 6]!
|
|
break
|
|
}
|
|
case 0xac: {
|
|
// VA 0x6fbf689c: AssignNPC, u8 size at +12
|
|
if (availableLen < 13) return { status: 'incomplete' }
|
|
size = buf[offset + 12]!
|
|
break
|
|
}
|
|
case 0xae: {
|
|
// VA 0x6fbf68b4: WardenRequest, u16 LE payloadLen at +1 (if <= 0x1fd, payloadLen + 3, else 3)
|
|
if (availableLen < 3) return { status: 'incomplete' }
|
|
const payloadLen = buf[offset + 1]! | (buf[offset + 2]! << 8)
|
|
size = (payloadLen <= 0x1fd ? payloadLen : 0) + 3
|
|
break
|
|
}
|
|
case 0xaf: {
|
|
// VA 0x6fbf68db: CompressionInfo, buf[1] == 0 ? 2 : buf[1] + 1
|
|
if (availableLen < 2) return { status: 'incomplete' }
|
|
size = buf[offset + 1] === 0 ? 2 : buf[offset + 1]! + 1
|
|
break
|
|
}
|
|
case 0xb3: {
|
|
// VA 0x6fbf6907: SaveDataChunk, buf[1] + 7
|
|
if (availableLen < 2) return { status: 'incomplete' }
|
|
size = buf[offset + 1]! + 7
|
|
break
|
|
}
|
|
default:
|
|
return { status: 'invalid', opcode, reason: \`unhandled variable-length opcode 0x\${opcode.toString(16)}\` }
|
|
}
|
|
|
|
if (size <= 0) {
|
|
return { status: 'invalid', opcode, reason: \`variable-length opcode 0x\${opcode.toString(16)} computed non-positive size \${size}\` }
|
|
}
|
|
if (availableLen < size) {
|
|
return { status: 'incomplete' }
|
|
}
|
|
return { status: 'ok', size }
|
|
}
|
|
`
|
|
}
|
|
|
|
function generateHuffmanTs(
|
|
fogSha1: string,
|
|
bitLengths: readonly number[],
|
|
bitMasks: readonly number[],
|
|
canonical: readonly CanonicalHuffmanEntry[],
|
|
): string {
|
|
const lenRows: string[] = []
|
|
for (let i = 0; i < 256; i += 16) {
|
|
const slice = bitLengths.slice(i, i + 16)
|
|
const comment = `/* ${formatHex(i)}..${formatHex(i + 15)} */`
|
|
lenRows.push(` ${comment} ${slice.map(v => formatHex(v)).join(', ')},`)
|
|
}
|
|
|
|
const codeRows: string[] = []
|
|
for (let i = 0; i < 256; i += 8) {
|
|
const slice = canonical.slice(i, i + 8)
|
|
const comment = `/* ${formatHex(i)}..${formatHex(i + 7)} */`
|
|
codeRows.push(` ${comment} ${slice.map(e => formatHex(e.code)).join(', ')},`)
|
|
}
|
|
|
|
return `/**
|
|
* AUTO-GENERATED by \`scripts/extract-d2net-tables.ts\` — DO NOT EDIT MANUALLY.
|
|
*
|
|
* Ground-Truth 1.13c Binary Source:
|
|
* - \`Fog.dll\` SHA-1: \`${fogSha1}\` (ImageBase \`${formatHex(FOG_IMAGE_BASE, 8)}\`)
|
|
* - Static Huffman Bit-Length Table (\`g_HuffmanBitLengths[256]\`): VA \`${formatHex(FOG_IMAGE_BASE + FOG_HUFFMAN_LENGTHS_OFFSET, 8)}\`, FileOffset \`${formatHex(FOG_HUFFMAN_LENGTHS_OFFSET, 6)}\`
|
|
* - Bit-Mask Table (\`g_BitMasks[16]\`): VA \`${formatHex(FOG_IMAGE_BASE + FOG_HUFFMAN_MASKS_OFFSET, 8)}\`, FileOffset \`${formatHex(FOG_HUFFMAN_MASKS_OFFSET, 6)}\`
|
|
* - Canonical Huffman Builder (\`Fog.10219\`): VA \`${formatHex(FOG_HUFFMAN_BUILD_FN_VA, 8)}\`, FileOffset \`0x01efd0\`
|
|
* - MSB-First Huffman Decompressor (\`Fog.10224\`): VA \`${formatHex(FOG_HUFFMAN_DECOMPRESS_FN_VA, 8)}\`, FileOffset \`0x01eda0\`
|
|
*/
|
|
|
|
export const FOG_DLL_SHA1 = '${fogSha1}' as const
|
|
export const FOG_HUFFMAN_LENGTHS_VA = ${formatHex(FOG_IMAGE_BASE + FOG_HUFFMAN_LENGTHS_OFFSET, 8)} as const
|
|
export const FOG_HUFFMAN_LENGTHS_FILE_OFFSET = ${formatHex(FOG_HUFFMAN_LENGTHS_OFFSET, 6)} as const
|
|
export const FOG_HUFFMAN_MASKS_VA = ${formatHex(FOG_IMAGE_BASE + FOG_HUFFMAN_MASKS_OFFSET, 8)} as const
|
|
export const FOG_HUFFMAN_BUILD_VA = ${formatHex(FOG_HUFFMAN_BUILD_FN_VA, 8)} as const
|
|
export const FOG_HUFFMAN_DECOMPRESS_VA = ${formatHex(FOG_HUFFMAN_DECOMPRESS_FN_VA, 8)} as const
|
|
|
|
/**
|
|
* 256-entry static Huffman symbol bit-length table (\`0x00..0xFF\`) from \`Fog.dll\` @ \`0x6ff7f210\`.
|
|
* Bit lengths range from 1 (\`0x00\`) to 11 bits.
|
|
*/
|
|
export const HUFFMAN_BIT_LENGTHS: readonly number[] = [
|
|
${lenRows.join('\n')}
|
|
] as const
|
|
|
|
/**
|
|
* 16-entry bit-mask table from \`Fog.dll\` @ \`0x6ff7f310\`.
|
|
*/
|
|
export const HUFFMAN_BIT_MASKS: readonly number[] = [
|
|
${bitMasks.map(m => formatHex(m, 4)).join(', ')}
|
|
] as const
|
|
|
|
/**
|
|
* 256-entry canonical MSB-first Huffman codewords computed by \`Fog.10219\` (\`0x6ff6efd0\`)
|
|
* for the default \`HUFFMAN_BIT_LENGTHS\` table.
|
|
*/
|
|
export const HUFFMAN_CODES: readonly number[] = [
|
|
${codeRows.join('\n')}
|
|
] as const
|
|
`
|
|
}
|
|
|
|
function main(): void {
|
|
const d2netPath = resolve(D2_DATA_DIR, 'D2Net.dll')
|
|
const fogPath = resolve(D2_DATA_DIR, 'Fog.dll')
|
|
const d2clientPath = resolve(D2_DATA_DIR, 'D2Client.dll')
|
|
|
|
const d2netBuf = readFileSync(d2netPath)
|
|
const fogBuf = readFileSync(fogPath)
|
|
const d2clientBuf = readFileSync(d2clientPath)
|
|
|
|
const d2netSha1 = sha1Hex(d2netBuf)
|
|
const fogSha1 = sha1Hex(fogBuf)
|
|
const d2clientSha1 = sha1Hex(d2clientBuf)
|
|
|
|
if (d2netSha1 !== EXPECTED_D2NET_SHA1) {
|
|
throw new Error(`D2Net.dll SHA-1 mismatch: expected ${EXPECTED_D2NET_SHA1}, got ${d2netSha1}`)
|
|
}
|
|
if (fogSha1 !== EXPECTED_FOG_SHA1) {
|
|
throw new Error(`Fog.dll SHA-1 mismatch: expected ${EXPECTED_FOG_SHA1}, got ${fogSha1}`)
|
|
}
|
|
if (d2clientSha1 !== EXPECTED_D2CLIENT_SHA1) {
|
|
throw new Error(`D2Client.dll SHA-1 mismatch: expected ${EXPECTED_D2CLIENT_SHA1}, got ${d2clientSha1}`)
|
|
}
|
|
|
|
const s2cSizes = readInt32ArrayLE(d2netBuf, D2NET_S2C_TABLE_OFFSET, D2NET_S2C_COUNT)
|
|
const c2sSizes = readInt32ArrayLE(d2netBuf, D2NET_C2S_TABLE_OFFSET, D2NET_C2S_COUNT)
|
|
|
|
// Extract switch jump table in D2NET_10033 (opcodes 0x16..0xB3)
|
|
const jumpTableVas = readUint32ArrayLE(d2netBuf, D2NET_SWITCH_TABLE_OFFSET, 15)
|
|
const varLenJumpTargets = new Map<number, number>()
|
|
for (let opcode = 0; opcode < s2cSizes.length; opcode++) {
|
|
if (s2cSizes[opcode] === -1) {
|
|
const idx = d2netBuf.readUInt8(D2NET_SWITCH_INDEX_OFFSET + (opcode - 0x16))
|
|
const va = jumpTableVas[idx]
|
|
if (va === undefined || idx >= 14) {
|
|
throw new Error(`Unexpected switch index ${idx} for variable-length opcode 0x${opcode.toString(16)}`)
|
|
}
|
|
varLenJumpTargets.set(opcode, va)
|
|
}
|
|
}
|
|
|
|
// Extract D2Client.dll S->C handler table (175 entries of 12 bytes)
|
|
const d2clientHandlers: { opcode: number; handlerVa: number; expectedSize: number; unitHandlerVa: number }[] = []
|
|
for (let opcode = 0; opcode < D2CLIENT_S2C_HANDLERS_COUNT; opcode++) {
|
|
const off = D2CLIENT_S2C_HANDLERS_OFFSET + opcode * 12
|
|
const handlerVa = d2clientBuf.readUInt32LE(off)
|
|
const expectedSize = d2clientBuf.readInt32LE(off + 4)
|
|
const unitHandlerVa = d2clientBuf.readUInt32LE(off + 8)
|
|
d2clientHandlers.push({ opcode, handlerVa, expectedSize, unitHandlerVa })
|
|
}
|
|
|
|
// Extract Fog.dll Huffman tables
|
|
const bitLengths: number[] = []
|
|
for (let i = 0; i < 256; i++) {
|
|
bitLengths.push(fogBuf.readUInt8(FOG_HUFFMAN_LENGTHS_OFFSET + i))
|
|
}
|
|
const bitMasks = readUint32ArrayLE(fogBuf, FOG_HUFFMAN_MASKS_OFFSET, 16)
|
|
const canonical = computeCanonicalHuffman(bitLengths)
|
|
|
|
const outDir = resolve(ROOT_DIR, 'src/netproto/d2gs/tables')
|
|
mkdirSync(outDir, { recursive: true })
|
|
|
|
const s2cSizesPath = resolve(outDir, 's2c-sizes.ts')
|
|
const huffmanPath = resolve(outDir, 'huffman.ts')
|
|
|
|
writeFileSync(
|
|
s2cSizesPath,
|
|
generateS2cSizesTs(d2netSha1, d2clientSha1, s2cSizes, c2sSizes, varLenJumpTargets, d2clientHandlers),
|
|
'utf8',
|
|
)
|
|
writeFileSync(huffmanPath, generateHuffmanTs(fogSha1, bitLengths, bitMasks, canonical), 'utf8')
|
|
|
|
console.log(`[extract-d2net-tables] Verified D2Net.dll (${d2netSha1}), Fog.dll (${fogSha1}), D2Client.dll (${d2clientSha1})`)
|
|
console.log(`[extract-d2net-tables] Wrote ${s2cSizesPath} (${s2cSizes.length} S->C, ${c2sSizes.length} C->S, ${varLenJumpTargets.size} var-len)`)
|
|
console.log(`[extract-d2net-tables] Wrote ${huffmanPath} (256 Huffman symbols)`)
|
|
}
|
|
|
|
main()
|