sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
211 lines (210 loc) • 7.2 kB
JavaScript
// # sim-grid.ts
import Stream from './stream.js';
import WriteBuffer from './write-buffer.js';
import { FileType } from './enums.js';
import { kFileType, kFileTypeArray } from './symbols.js';
const TypedArrays = {
[FileType.SimGridUint8]: Uint8Array,
[FileType.SimGridSint8]: Int8Array,
[FileType.SimGridUint16]: Uint16Array,
[FileType.SimGridSint16]: Int16Array,
[FileType.SimGridUint32]: Uint32Array,
[FileType.SimGridFloat32]: Float32Array,
};
const Readers = {
[FileType.SimGridUint8]: Stream.prototype.uint8,
[FileType.SimGridSint8]: Stream.prototype.int8,
[FileType.SimGridUint16]: Stream.prototype.uint16,
[FileType.SimGridSint16]: Stream.prototype.int16,
[FileType.SimGridUint32]: Stream.prototype.uint32,
[FileType.SimGridFloat32]: Stream.prototype.float,
};
const Writers = {
[FileType.SimGridUint8]: WriteBuffer.prototype.uint8,
[FileType.SimGridSint8]: WriteBuffer.prototype.int8,
[FileType.SimGridUint16]: WriteBuffer.prototype.uint16,
[FileType.SimGridSint16]: WriteBuffer.prototype.int16,
[FileType.SimGridUint32]: WriteBuffer.prototype.uint32,
[FileType.SimGridFloat32]: WriteBuffer.prototype.float,
};
// # getTypedArray(type)
// Returns the typed array constructor to use for a specific type of SimGrid,
// independent.
function getTypedArray(type) {
return TypedArrays[type];
}
// # SimGrid
// SimCity4 has different classes for each SimGrid, so we'll reflect this as
// well. We want them to have similar behavior though, so we'll extend the class
// from the base class automatically.
class SimGrid {
// Note: I think some of the unknowns identifies the data type, where
// 0x01 is UInt8 etc. Not sure though, we should investigate this
// deeper.
crc = 0x00000000;
mem = 0x00000000;
major = 0x0001;
u1 = 0x01;
type;
data;
dataId = 0x00000000;
resolution = 0x00000001;
resolutionExponent = 0x00000000;
xSize = 0x00000040;
zSize = 0x00000040;
u6 = 0x00000000;
u7 = 0x00000000;
u8 = 0x00000000;
u9 = 0x00000000;
// ## constructor(opts)
// The SimGrid constructor allows us to construct a SimGrid easily by
// automically figuring out a bunch of properties. For example, if a single
// "size" field is specified, it assumes that we're dealing with a square -
// which is simply always the case.
constructor(opts = {}) {
let { data, size = 0, xSize = size, zSize = size, resolution = 1, resolutionExponent = Math.log2(resolution), ...rest } = opts;
if (data) {
if (Array.isArray(data)) {
const TypedArray = getTypedArray(this.type);
this.data = new TypedArray(data);
}
else {
this.data = data;
}
}
else if (this.type) {
const TypedArray = getTypedArray(this.type);
this.data = new TypedArray(size);
}
Object.assign(this, {
xSize,
zSize,
resolution,
resolutionExponent,
...rest,
});
}
// ## parse(rs)
parse(rs) {
rs.size();
this.crc = rs.dword();
this.mem = rs.dword();
this.major = rs.word();
this.u1 = rs.byte();
this.type = rs.type();
this.dataId = rs.dword();
this.resolution = rs.dword();
this.resolutionExponent = rs.dword();
this.xSize = rs.dword();
this.zSize = rs.dword();
this.u6 = rs.dword();
this.u7 = rs.dword();
this.u8 = rs.dword();
this.u9 = rs.dword();
// Don't know if multiple values are possible here, the SInt8 does
// some pretty weird stuff... Anyway, for now we'll just read in the
// rest into the appropriate underlying array type.
// Note: we could directly copy the arraybuffer, but it's pretty error
// prone apparently, especially with the offsets and stuff. Hence
// we'll write in manually.
const TypedArray = getTypedArray(this.type);
const reader = Readers[this.type];
const count = this.xSize * this.zSize;
let data = this.data = new TypedArray(count);
for (let i = 0; i < count; i++) {
data[i] = reader.call(rs);
}
// Ensure that we've read everything correctly.
rs.assert();
// Done! Easy data access is available by calling createProxy(). Using
// this it's possible to access the data as if it were a
// multidimensional array.
return this;
}
// ## toBuffer()
toBuffer() {
// Pre-allocate the header.
let ws = new WriteBuffer();
ws.dword(this.mem);
ws.word(this.major);
ws.byte(this.u1);
ws.dword(this.type);
ws.dword(this.dataId);
ws.dword(this.resolution);
ws.dword(this.resolutionExponent);
ws.dword(this.xSize);
ws.dword(this.zSize);
ws.dword(this.u6);
ws.dword(this.u7);
ws.dword(this.u8);
ws.dword(this.u9);
// Use the underlying buffer of our data view. At least on LE systems
// this should be good to be used directly.
const writer = Writers[this.type];
for (let value of this.data) {
writer.call(ws, value);
}
return ws.seal();
}
// ## get(x, z)
// Returns the value stored in cell (x, z)
get(x, z) {
let { zSize } = this;
return this.data[x * zSize + z];
}
// ## set(x, z)
// Sets the value stored in cell (x, z)
set(x, z, value) {
this.data[x * this.zSize + z] = value;
return this;
}
// ## clear(value)
// Clears the entire simgrid again. Note that some simgrids might be cleared
// with different values - like -128 for exammple - so this need sto be
// customizable.
clear(value = 0) {
this.data.fill(value);
}
// ## createProxy()
// Creates a data proxy so that we can access the data in an array-like
// way.
createProxy() {
return new Proxy(this, {
get(target, prop) {
let x = +prop;
return new Proxy(target, {
get(target, prop) {
let z = +prop;
let { zSize, data } = target;
return data[x * zSize + z];
},
});
},
});
}
}
// Helper function for creating the actual class.
function createClass(type) {
return class extends SimGrid {
type = type;
static [kFileType] = type;
static [kFileTypeArray] = type;
};
}
export class SimGridUint8 extends createClass(FileType.SimGridUint8) {
}
;
export class SimGridSint8 extends createClass(FileType.SimGridSint8) {
}
;
export class SimGridUint16 extends createClass(FileType.SimGridUint16) {
}
;
export class SimGridSint16 extends createClass(FileType.SimGridSint16) {
}
;
export class SimGridUint32 extends createClass(FileType.SimGridUint32) {
}
;
export class SimGridFloat32 extends createClass(FileType.SimGridFloat32) {
}