sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
219 lines (218 loc) • 7.47 kB
JavaScript
// # stream.js
import { SmartBuffer } from 'smart-arraybuffer';
import Pointer from './pointer.js';
import SGProp from './sgprop.js';
import Color from './color.js';
import Vertex from './vertex.js';
import TractInfo from './tract-info.js';
import { Box3 } from './box-3.js';
import Vector3 from './vector-3.js';
import Matrix3 from './matrix-3.js';
import NetworkCrossing from './network-crossing.js';
import TGI from './tgi.js';
import SimulatorDate from './simulator-date.js';
// # Stream
// Helper class that provides some methods for reading from a buffer
// sequentially, maintaining buffer state.
export default class Stream extends SmartBuffer {
// ## constructor(opts)
constructor(opts) {
if (opts instanceof Uint8Array || opts instanceof ArrayBuffer) {
super({ buff: opts });
}
else if (opts instanceof Stream) {
super({ buff: opts.internalUint8Array });
this.readOffset = opts.readOffset;
}
else {
super(opts);
}
}
// ## skip(n = 1)
// Skips n bytes.
skip(n = 1) {
this.readOffset += n;
}
// ## read()
read(length) {
return this.readUint8Array(length);
}
// ## string(length)
// Reads a string with the given length, as utf8. Note: if no length is
// given, we assume we have to read the length first as a dword.
string(length = this.dword()) {
if (length === Infinity) {
return this.readString();
}
else {
return this.readString(length);
}
}
// ## chunk()
// Reads a chunk where the first 4 bytes are the size of the chunk. This
// is useful when parsing files because a lot of them have the structure
// "SIZE CRC MEM ...". Note that we return a view on top of the underlying
// buffer, we don't copy it!
chunk() {
let size = this.readUInt32LE(this.readOffset);
return this.read(size);
}
// ## rest()
// Helper function for reading the rest of the buffer - as a slice.
rest() {
return this.readUint8Array();
}
int8(offset) { return this.readInt8(offset); }
int16(offset) { return this.readInt16LE(offset); }
int32(offset) { return this.readInt32LE(offset); }
bigint64(offset) { return this.readBigInt64LE(offset); }
float(offset) { return this.readFloatLE(offset); }
double(offset) { return this.readDoubleLE(offset); }
uint8(offset) { return this.readUInt8(offset); }
uint16(offset) { return this.readUInt16LE(offset); }
uint32(offset) { return this.readUInt32LE(offset); }
biguint64(offset) { return this.readBigUInt64LE(offset); }
// Some aliases.
byte(offset) { return this.uint8(offset); }
word(offset) { return this.uint16(offset); }
dword(offset) { return this.uint32(offset); }
qword(offset) { return this.biguint64(offset); }
bool(offset) { return Boolean(this.uint8(offset)); }
// When using TypeScript, it's beneficial to be explicict about when we're
// reading in a file type so that we can properly type it.
type(offset) {
return this.dword(offset);
}
// ## size()
// The size of a record is simply a dword, but it makes it clearer that
// we're reading in a size, so we use an alias.
size(offset) { return this.dword(offset); }
// ## version(n)
// Helper function for reading in a version identifier of a record. The
// default is major.minor, but more are possibl as well.
version(n = 2) {
let parts = [];
for (let i = 0; i < n; i++) {
parts.push(this.word());
}
return parts.join('.');
}
// ## tgi()
// Reads in a TGI.
tgi() {
let type = this.dword();
let group = this.dword();
let instance = this.dword();
return new TGI(type, group, instance);
}
// ## gti()
// Reads in a TGI when it is given as GTI. This often happens in savegames
// where gti is used to reference a model to render.
gti() {
let group = this.dword();
let type = this.dword();
let instance = this.dword();
return new TGI(type, group, instance);
}
// ## date()
// Reads in a date - as Julian date - and returns it as a simulator date
// instance.
date() {
return SimulatorDate.fromJulian(this.dword());
}
// Helper function for reading a pointer. Those are given as [pointer,
// Type ID]. Note that if no address was given, we return "null" (i.e. a
// null pointer).
pointer() {
let address = this.dword();
if (address === 0x00000000)
return null;
let type = this.dword();
return new Pointer(type, address);
}
// ## vector3()
// Helper function for reading in a 3D vector object.
vector3() {
let v = new Vector3();
v.parse(this);
return v;
}
// ## matrix3()
// Helper function for reading in a 3x3 matrix from the stream.
matrix3() {
let matrix = new Matrix3();
matrix.parse(this);
return matrix;
}
// # color()
// Reads in a color from the stream.
color() {
return new Color(this.byte(), this.byte(), this.byte(), this.byte());
}
// ## vertex()
// Reads in a vertex class from the stream.
vertex() {
let vertex = new Vertex();
vertex.parse(this);
return vertex;
}
// ## tract()
// Reads in a TractInfo object from the stream.
tract() {
let tract = new TractInfo();
tract.parse(this);
return tract;
}
// ## bbox()
// Reads in a bounding box from the stream.
bbox(opts) {
let bbox = new Box3();
bbox.parse(this, opts);
return bbox;
}
// Helper function for reading in an array. We first read in the length
// and then fill up the array. It's important that the function passed
// properly consumers the readable stream though!
array(fn, size = this.dword()) {
let arr = new Array(size);
for (let i = 0; i < arr.length; i++) {
arr[i] = fn.call(this, this, i);
}
return arr;
}
// ## struct(Constructor)
// Helper method for reading in a specific data structure. The premisse is
// that the class implements a `parse(rs)` method.
struct(Constructor) {
let struct = new Constructor();
struct.parse(this);
return struct;
}
// ## sgprops()
// Reads in an array of sgprops.
sgprops() {
return this.array(() => new SGProp().parse(this));
}
// ## crossings()
// Reads in an array of network crossings. They often appear in network
// subfiles, so it makes sense to have a specific parser for it.
crossings() {
let n = this.byte() + 1;
let array = [];
for (let i = 0; i < n; i++) {
let crossing = new NetworkCrossing().parse(this);
array.push(crossing);
}
return array;
}
// ## assert()
// Helper method that ensures the stream has been fully consumed. Throws
// an error if it's not the case. Useful for checking if a decoded
// structure is valid for all kinds of cities.
assert() {
let n = this.remaining();
if (n > 0) {
throw new Error(`Stream has not been fully consumed yet! ${n} bytes remaining!`);
}
}
}