155 lines
3.9 KiB
JavaScript
155 lines
3.9 KiB
JavaScript
// 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;
|
|
}
|