SA Model Viewer base
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user