// RenderWare binary stream: every chunk is a 12 byte header followed by its // body, and container chunks simply hold more chunks. That one rule covers // both DFF and TXD. export const RW = { STRUCT: 0x01, STRING: 0x02, EXTENSION: 0x03, TEXTURE: 0x06, MATERIAL: 0x07, MATLIST: 0x08, FRAMELIST: 0x0e, GEOMETRY: 0x0f, CLUMP: 0x10, ATOMIC: 0x14, TEXNATIVE: 0x15, TEXDICT: 0x16, GEOMETRYLIST: 0x1a, BINMESH: 0x50e, SKIN: 0x116, HANIM: 0x11e, FRAME_NAME: 0x253f2fe, }; // GTA:SA ships RenderWare 3.6.0.3, which packs to 0x1803FFFF. export const SA_VERSION = 0x36003; export function unpackVersion(libraryId) { if (libraryId & 0xffff0000) { return (((libraryId >> 14) & 0x3ff00) + 0x30000) | ((libraryId >> 16) & 0x3f); } return libraryId << 8; } export function versionString(version) { // major.minor.revision.build - the build number lives in the low 6 bits. return [(version >> 16) & 0xf, (version >> 12) & 0xf, (version >> 8) & 0xf, version & 0x3f].join('.'); } export class Reader { constructor(view, start, end) { this.view = view; this.pos = start; this.end = end; } get remaining() { return this.end - this.pos; } u8() { return this.view.getUint8(this.pos++); } u16() { const v = this.view.getUint16(this.pos, true); this.pos += 2; return v; } u32() { const v = this.view.getUint32(this.pos, true); this.pos += 4; return v; } i32() { const v = this.view.getInt32(this.pos, true); this.pos += 4; return v; } f32() { const v = this.view.getFloat32(this.pos, true); this.pos += 4; return v; } skip(n) { this.pos += n; } bytes(n) { const v = new Uint8Array(this.view.buffer, this.view.byteOffset + this.pos, n); this.pos += n; return v; } // Fixed width, null padded name field. fixedString(n) { const raw = this.bytes(n); let len = raw.indexOf(0); if (len < 0) len = n; return String.fromCharCode(...raw.subarray(0, len)); } f32Array(n) { const out = new Float32Array(n); for (let i = 0; i < n; i++) out[i] = this.f32(); return out; } } export class Chunk { constructor(view, offset, limit) { this.view = view; this.type = view.getUint32(offset, true); this.size = view.getUint32(offset + 4, true); this.version = unpackVersion(view.getUint32(offset + 8, true)); this.start = offset + 12; this.end = Math.min(this.start + this.size, limit === undefined ? view.byteLength : limit); } reader() { return new Reader(this.view, this.start, this.end); } *children() { let offset = this.start; // A 12 byte header has to fit before anything can be read. while (offset + 12 <= this.end) { const child = new Chunk(this.view, offset, this.end); yield child; const step = 12 + child.size; if (step <= 0) break; // corrupt size, don't spin offset += step; } } find(type) { for (const child of this.children()) { if (child.type === type) return child; } return null; } findAll(type) { const out = []; for (const child of this.children()) { if (child.type === type) out.push(child); } return out; } // Convenience for the STRUCT chunk that nearly every container starts with. structReader() { const struct = this.find(RW.STRUCT); if (!struct) throw new Error(`chunk 0x${this.type.toString(16)} has no struct`); return struct.reader(); } } export function readRoot(buffer, expectedType) { if (buffer.byteLength < 12) throw new Error('file is too small to be a RenderWare stream'); const view = new DataView(buffer); const root = new Chunk(view, 0); if (expectedType !== undefined && root.type !== expectedType) { throw new Error( `expected chunk 0x${expectedType.toString(16)}, got 0x${root.type.toString(16)} ` + '(wrong file type, or the file is compressed/encrypted)' ); } return root; }