// Local port of client-table-extract's native codec, with GridFight schema modes. import * as e from './extract.mjs'; const { fs, path, fail, cat, matches, escapeRE, zigzag, small } =e; export class NativeCodec { constructor(dump='', assembly='', evidence='', mode='stages'){ this.assembly=assembly; this.evidence=evidence; this.mode=mode; this.blocks=Object.fromEntries(matches(dump, /\/\/ Full Name: ([\w.]+)\n(.*?)(?=\n\/\/ TypeDefIndex:|$)/gs).map(m=>[m[1], m[2]])); this.rvas=[...new Set(matches(dump, /RVA: (0x[\da-fA-F]+)/g).map(m=>Number(m[1])))].sort((a, b)=>a-b); this.schemas={ }; this.methods={ }; this.subtypes={ }; this.readers=new Map(); for(const name of Object.keys(this.blocks))for(const [, rva]of this.binary_methods(name)){ if(!this.readers.has(rva))this.readers.set(rva, new Set()); this.readers.get(rva).add(name); } } binary_methods(name){ if(!(name in this.blocks))fail('Missing native metadata class '+name); return matches(this.blocks[name], new RegExp('^ public static void (\\w+)\\(FCOEMCEDLNL, '+escapeRE(name)+'\\) .*?RVA: (0x[\\da-fA-F]+)', 'gm')).map(m=>[m[1], Number(m[2])]); } disassemble(rva, size=null){ const end=this.rvas.find(v=>v>rva)??rva+8192; const [address, dis]=e.disassemble(this.assembly, rva, size??Math.min(end-rva, 8192)); return [address, dis.slice(dis.indexOf('Disassembly'))]; } proof(name, dis){ fs.mkdirSync(this.evidence, { recursive:true }); fs.writeFileSync(path.join(this.evidence, name+'.asm'), dis); } fields(name){ const body=this.blocks[name]??fail('Missing native metadata class '+name), base=body.match(/public class \w+ : (RPG\.GameCore\.\w+)/), result=base&&base[1]in this.blocks?this.fields(base[1]):new Map(); for(const m of matches(body, /^ public (.+?) (\w+) \/\/ Offset: (0x[\da-fA-F]+)/gm))result.set(Number(m[3]), [m[2], m[1]]); return result; } schema(name){ if(name in this.schemas)return this.schemas[name]; let known=this.fields(name); if(this.mode==='rounds')known=new Map([...known].filter(([, v])=>!v[1].startsWith('static '))); for(const [method, rva]of this.binary_methods(name)){ const [, dis]=this.disassemble(rva), tests=matches(dis, this.mode==='stages'?/(?:test\s+(?:bl|ebx|rbx|al|eax|rax),0x([\da-f]+)|bt\s+rbx,0x([\da-f]+))/g:/test\s+(?:bl|ebx|rbx|al|eax|rax),0x([\da-f]+)/g), ordered=new Map(), used=new Set(); for(let i=0; iparseInt(m[1], 16)).filter(v=>known.has(v)&&!used.has(v))); if(found.size!==1)break; const offset=[...found][0]; used.add(offset); const [key, type]=known.get(offset); ordered.set(bit.toString(2).length-1, { name:key, type }); if(this.mode==='stages'&&ordered.size===known.size)break; } if(ordered.size&&Array.from({ length:ordered.size }, (_, i)=>ordered.has(i)).every(Boolean)){ if(this.mode==='rounds'&&ordered.sizeordered.get(i)); this.methods[name]={ method, rva }; this.proof(name.split('.').at(-1), dis); return this.schemas[name]; } } fail('Unproved native field order: '+name); } polymorphic(name){ if(name in this.subtypes)return this.subtypes[name]; for(const [, rva]of this.binary_methods(name)){ const [address, dis]=this.disassemble(rva), limit=dis.match(/cmp\s+eax,0x([\da-f]+)/), table=dis.match(/lea\s+rcx,\[rip\+[^\n]+# (0x[\da-f]+)\s*\n[^\n]*movsxd rax,DWORD PTR \[rcx\+rax\*4\]/); if(!limit||!table||!dis.includes('jmp rax'))continue; const count=parseInt(limit[1], 16)+1; if(count>64)fail('Unsupported subtype dispatch size'); const base=address-BigInt(rva), ta=BigInt(table[1]), [, raw]=e.pe_method(this.assembly, Number(ta-base), count*4), targets=Array.from({ length:count }, (_, i)=>ta+BigInt(raw.readInt32LE(i*4))), result={ }; for(let tag=0; tagv>target).sort((a, b)=>ae.pe_method(this.assembly, rva+Number(table-address), size)[1]); this.proof('DynamicValue_FromByteBinary', dis); e.atomic_json(path.join(this.evidence, 'DynamicValue_reader.json'), {rva, ...result}); return {rva, ...result}; } dynamic(cursor){ const tag=zigzag(cursor.varint()); let v; if(tag===0)v=zigzag(cursor.varint()); else if(tag===1){ if(cursor.at+8>cursor.data.length)fail('Truncated DynamicValue double'); v={ double_wire:cursor.data.subarray(cursor.at, cursor.at+8).toString('hex') }; cursor.at+=8; } else if(tag===2)v=e.field(cursor, 'bool'); else if(tag===3)v=Array.from({ length:cursor.length() }, ()=>this.dynamic(cursor)); else if(tag===4)v=Array.from({ length:cursor.length() }, ()=>[this.dynamic(cursor), this.dynamic(cursor)]); else if(tag===5)v=e.field(cursor, 'string'); else if(tag===6)v=null; else fail('Unsupported DynamicValue tag'); return { [this.mode==='rounds'?'tag':'wire_tag']:tag, value:v }; } encode_dynamic(v){ const tag=v[this.mode==='rounds'?'tag':'wire_tag'], x=v.value, b=e.encode_varint(tag*2); if(tag===0){ const a=BigInt(x); return cat(b, e.encode_varint((a<<1n)^(a>>63n))); } if(tag===1)return cat(b, Buffer.from(x.double_wire, 'hex')); if(tag===2)return cat(b, e.encode_field(x, 'bool')); if(tag===3)return cat(b, e.encode_varint(x.length*2), x.map(a=>this.encode_dynamic(a))); if(tag===4)return cat(b, e.encode_varint(x.length*2), x.flatMap(([a, z])=>[this.encode_dynamic(a), this.encode_dynamic(z)])); if(tag===5)return cat(b, e.encode_field(x, 'string')); if(tag===6)return b; fail('Unsupported DynamicValue tag'); } field(cursor, typ){ if(typ==='RPG.GameCore.DynamicValue')return this.dynamic(cursor); if(['RPG.Client.TextID', 'RPG.GameCore.FixPoint'].includes(typ))return e.field(cursor, typ); if(typ.endsWith('[]'))return Array.from({ length:cursor.length() }, ()=>this.field(cursor, typ.slice(0, -2))); if(['uint', 'ushort'].includes(typ)){ const v=cursor.varint(); if(v>=2**(typ==='ushort'?16:32))fail('Unsigned integer overflow'); return v; } if(['short', 'int'].includes(typ)||(typ in this.blocks&&this.blocks[typ].includes('public enum '))){ const bits=typ==='short'?16:32; if(typ in this.blocks&&!/public int value__/.test(this.blocks[typ]))fail('Unsupported enum storage'); const v=cursor.varint(); if(v>=2**bits)fail('Signed integer overflow'); return zigzag(v); } if(typ==='double'){ if(cursor.at+8>cursor.data.length)fail('Truncated double'); const v={double_wire:cursor.data.subarray(cursor.at,cursor.at+8).toString('hex')}; cursor.at+=8;return v; } if(typ==='float'){ if(cursor.at+4>cursor.data.length)fail('Truncated float'); const v={ float_wire:cursor.data.subarray(cursor.at, cursor.at+4).toString('hex') }; cursor.at+=4; return v; } if(['string', 'bool'].includes(typ))return e.field(cursor, typ); if(typ.startsWith('System.Collections.Generic.Dictionary<')){ const m=typ.match(/^System.Collections.Generic.Dictionary<(.+?), (.+)>$/); if(!m)fail('Unsupported dictionary'); return Array.from({ length:cursor.length() }, ()=>[this.field(cursor, m[1]), this.field(cursor, m[2])]); } if(typ==='RPG.GameCore.FloorCustomValueConfig'){ const tag=cursor.varint(), sub=this.polymorphic(typ); if(!(tag in sub))fail('Unknown native subtype: '+tag); return { tag, value:this.record(cursor, sub[tag]) }; } return this.record(cursor, typ); } record(cursor, typ, through=null){ const cols=this.schema(typ), mask=BigInt(cursor.varint()); if(mask>>BigInt(cols.length))fail('Unsupported presence mask'); const row={ }; for(let i=0; ithis.encode_field(x, t.slice(0, -2)))); if(t.startsWith('System.Collections.Generic.Dictionary<')){ const m=t.match(/^System.Collections.Generic.Dictionary<(.+?), (.+)>$/); return cat(e.encode_varint(v.length*2), v.flatMap(([k, x])=>[this.encode_field(k, m[1]), this.encode_field(x, m[2])])); } if(t==='RPG.GameCore.FloorCustomValueConfig')return cat(e.encode_varint(v.tag), this.encode_record(v.value, this.polymorphic(t)[v.tag])); if(t==='double')return Buffer.from(v.double_wire, 'hex'); if(t==='float')return Buffer.from(v.float_wire, 'hex'); if(['short', 'int'].includes(t)||(t in this.blocks&&this.blocks[t].includes('public enum '))){ const x=BigInt(v); return e.encode_varint((x<<1n)^(x>>BigInt(t==='short'?15:31))); } if(['uint', 'ushort', 'string', 'bool'].includes(t))return e.encode_field(v, t); return this.encode_record(v, t); } encode_record(row, typ){ const cols=this.schema(typ); if(Object.keys(row).some(k=>!cols.some(col=>col.name===k)))fail('Unknown field during round trip'); return cat(e.encode_varint(cols.reduce((m, col, i)=>col.name in row?m|(1n<col.name in row).map(col=>this.encode_field(row[col.name], col.type))); } } export function indexed_entries(data, id, size){ const marker=Buffer.from(id, 'hex'), at0=data.indexOf(marker); if(at0<0||data.indexOf(marker, at0+16)>=0)fail('Missing or ambiguous DesignV blob: '+id); const at=at0+16; if(at+12>data.length)fail('Truncated DesignV header'); const expected=data.readBigUInt64BE(at), count=data.readUInt32BE(at+8); if(expected!==BigInt(size)||!count||at+12+count*16>data.length)fail('DesignV index does not match installed blob'); const entries=[], hashes=new Set(); for(let i=0; isize||hashes.has(key))fail('Invalid or overlapping DesignV asset ranges'); entries.push({ hash:key, offset, length }); hashes.add(key); } const sorted=entries.toSorted((a, b)=>a.offset-b.offset); if(sorted.some((a, i)=>i+1sorted[i+1].offset))fail('Overlapping DesignV asset ranges'); return entries; } // XXH64, seed zero, with explicit 64-bit arithmetic rather than a native FFI. const MASK=(1n<<64n)-1n, P1=11400714785074694791n, P2=14029467366897019727n, P3=1609587929392839161n, P4=9650029242287828579n, P5=2870177450012600261n; const rot=(x, n)=>((x<>BigInt(64-n)))&MASK; const round=(a, x)=>rot((a+x*P2)&MASK, 31)*P1&MASK; export function asset_hash(text){ const b=Buffer.from(text); let p=0, h; if(b.length>=32){ let v=[(P1+P2)&MASK, P2, 0n, (-P1)&MASK]; while(p<=b.length-32){ for(let i=0; i<4; i++)v[i]=round(v[i], b.readBigUInt64LE(p+i*8)); p+=32; } h=(rot(v[0], 1)+rot(v[1], 7)+rot(v[2], 12)+rot(v[3], 18))&MASK; for(const x of v)h=((h^round(0n, x))*P1+P4)&MASK; } else h=P5; h=(h+BigInt(b.length))&MASK; while(p+8<=b.length){ h=(rot(h^round(0n, b.readBigUInt64LE(p)), 27)*P1+P4)&MASK; p+=8; } if(p+4<=b.length){ h=(rot(h^(BigInt(b.readUInt32LE(p))*P1&MASK), 23)*P2+P3)&MASK; p+=4; } while(p>33n; h=h*P2&MASK; h^=h>>29n; h=h*P3&MASK; return h^(h>>32n); } // Both beta readers have this dispatch layout; resolve the RVA from matching metadata. export function verify_dynamic_dispatch(dis, address, read){ const table=dis.match(/lea\s+rcx,\[rip\+[^\n]+# (0x[\da-f]+)\s*\n[^\n]*movsxd rax,DWORD PTR \[rcx\+rax\*4\]/); if(!table||![/cmp\s+rax,0x5/, /shr\s+edx,1/, /neg\s+eax/, /xor\s+eax,edx/, /jmp\s+rax/].every(p=>p.test(dis)))fail('DynamicValue native reader changed'); const ta=BigInt(table[1]), raw=read(ta,24), offsets=[0x85,0xc5,0x102,0x14f,0x18a,0x1cf], targets=offsets.map((_,i)=>ta+BigInt(raw.readInt32LE(i*4))); if(ta-address!==0x2b0n||targets.some((v,i)=>v-address!==BigInt(offsets[i])))fail('DynamicValue native tag binding changed'); const fallback=dis.match(/cmp\s+rax,0x5\s*\n[^\n]*ja\s+(0x[\da-f]+)/), defaultTarget=fallback?BigInt(fallback[1]):0n, tail=dis.slice(dis.indexOf(defaultTarget.toString(16)+':'),dis.indexOf(targets[5].toString(16)+':')); if(defaultTarget-address!==0x1c6n||!/nop/.test(tail)||!/add\s+rsp,0x30/.test(tail)||!/ret/.test(tail)||/\b(?:call|jmp)\s/.test(tail))fail('DynamicValue native null/default case changed'); const checks=[[/shr\s+rdx,1/, /neg\s+rax/, /xor\s+rax,rdx/, /mov\s+QWORD PTR \[rcx\+0x28\],rax/], [/movsd\s+xmm0,QWORD PTR \[rsp\+0x28\]/, /movsd\s+QWORD PTR \[rax\+0x28\],xmm0/], [/mov\s+edx,0x1/, /movzx\s+ecx,bl/, /mov\s+QWORD PTR \[rax\+0x28\],rcx/], [/add\s+rbx,0x20/, /jmp\s+0x/], [/add\s+rbx,0x18/, /call\s+0x/], [/add\s+rbx,0x10/, /jmp\s+0x/]]; for(let i=0;ip.test(part)))fail('DynamicValue native case changed: '+i); } return {dispatch_offset:0x2b0, default_offset:0x1c6, case_offsets:offsets, tags:['int64-zigzag','double-le','bool-byte','array','map','string','null'], wire_layout:'beta-4.6.51/4.6.53'}; }