sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
238 lines (237 loc) • 8.29 kB
JavaScript
// # item-index-file.js
import WriteBuffer from './write-buffer.js';
import Pointer from './pointer.js';
import { FileType } from './enums.js';
import { getClassType } from './helpers.js';
import { kFileType } from './symbols.js';
const SIZE = 192;
// # ItemIndex
// The item index is a crucial data structure in determining what the game will
// render on screen. If an item is not present in the item index, it won't be
// rendered, even if it is present in its respective subfile.
export default class ItemIndex {
static [kFileType] = FileType.ItemIndex;
crc = 0x00000000;
mem = 0x00000000;
major = 0x0001;
// Below is stored the size of the item index, in various dimensions.
// (width, depth) is the size in *meters* - 1024, 2048 and 4096 meters for
// respectively small, medium and large tiles. (tractWidth, tractDepth)
// contains the dimensions in amount of tracts - i.e. 4x4 squares. 16, 32
// and 64 tracts for small, medium and large tiles. (tileWidth, tileDepth)
// contains the dimensions in tiles: 64, 128 and 256 for small, medium and
// large cities.
width = 1024;
depth = 1024;
tractWidth = 16;
tractDepth = 16;
tileWidth = 64;
tileDepth = 64;
elements = [];
// Array-like functionality.
get length() { return this.elements.length; }
set length(length) { this.elements.length = length; }
*[Symbol.iterator]() { yield* this.elements; }
// ## constructor(x, z)
// Creates the item index. You can specify a size *in tracts* to set up
// the item index more easily. 16 tracts is a small city, 32 medium and 64
// is large.
constructor(x = 0x10, z = x) {
this.width = 64 * x;
this.depth = 64 * z;
this.tractWidth = x;
this.tractDepth = z;
this.tileWidth = 4 * x;
this.tileDepth = 4 * z;
}
// ## get(x: number, z: number)
get(x, z) {
this.ensure();
let column = this.elements[x];
if (!column)
return undefined;
return column.at(z);
}
// ## fill()
// Fills up the item index with cells. This is useful when not parsing an
// item index, but generating a city from scratch where you need an empty
// item index.
fill() {
let { elements } = this;
elements.length = SIZE;
for (let x = 0; x < elements.length; x++) {
let column = elements[x] = new Array(SIZE);
for (let z = 0; z < column.length; z++) {
column[z] = new Cell(x, z);
}
}
return this;
}
// ## rebuild(type, file)
// Rebuilds the index so that it puts all entries of the given file in
// their correct tracts.
rebuild(type, file) {
// From now on we need a specific file type because certain arrays might
// be empty, in which case we don't know what type of values the array
// holds. That's because we now use bare arrays instead of extensions of
// native arrays!
if (!type) {
throw new Error(`Unknown file type! ${type}`);
}
// First of all we'll remove all references to the give file type.
// Note that it would be useful if we could use cell.filter somehow,
// but it's not possible for now unfortunately so we need to use
// slice...
this.filter(pointer => pointer.type !== type);
// Now loop all records from the file and insert into the correct
// cells.
for (let record of file) {
let { mem, tract } = record;
for (let x = tract.minX; x <= tract.maxX; x++) {
for (let z = tract.minZ; z <= tract.maxZ; z++) {
this.elements[x][z].push(new Pointer(type, mem));
}
}
}
return this;
}
// ## filter(fn)
// Helper method for filtering all cells in the item index.
filter(...params) {
for (let row of this) {
for (let cell of row) {
cell.filter(...params);
}
}
return this;
}
// ## ensure()
ensure() {
if (this.elements.length === 0)
this.fill();
return this;
}
// ## clear()
clear() {
this.fill();
}
// ## add(item, type)
// Adds the given item to the item index. We'll try to figure out the type
// automatically, but you can specify it yourself as well. Note that the
// item needs to expose min and max tract coordinates, but they do so
// quite often!
add(item, type = getClassType(item)) {
this.ensure();
let { tract } = item;
for (let x = tract.minX; x <= tract.maxX; x++) {
for (let z = tract.minZ; z <= tract.maxZ; z++) {
this.elements[x][z].push(new Pointer(type, item.mem));
}
}
}
// ## parse(rs)
parse(rs) {
rs.size();
this.crc = rs.dword();
this.mem = rs.dword();
this.major = rs.word();
this.width = rs.float();
this.depth = rs.float();
this.tractWidth = rs.dword();
this.tractDepth = rs.dword();
this.tileWidth = rs.dword();
this.tileDepth = rs.dword();
let columns = rs.dword();
this.length = columns;
for (let x = 0; x < columns; x++) {
let rows = rs.dword();
let column = new Array(rows);
this.elements[x] = column;
for (let z = 0; z < rows; z++) {
let count = rs.dword();
let cell = new Cell(x, z);
column[z] = cell;
for (let i = 0; i < count; i++) {
let ptr = rs.pointer();
if (ptr === null)
continue;
cell.push(ptr);
}
}
}
// Check if we've read everything correctly.
rs.assert();
return this;
}
// ## toBuffer()
// Serializes the item index into a binary buffer.
// Generator function that will yield buffer chunks. Note that we can only
// ever yield 1 buffer chunk because we need to calculate its checksum and
// we need the entire buffer for this!
toBuffer() {
let ws = new WriteBuffer();
ws.dword(this.mem);
ws.word(this.major);
ws.float(this.width);
ws.float(this.depth);
ws.dword(this.tractWidth);
ws.dword(this.tractDepth);
ws.dword(this.tileWidth);
ws.dword(this.tileDepth);
ws.dword(this.length);
// Write all cells.
for (let column of this) {
ws.dword(column.length);
for (let cell of column) {
ws.dword(cell.length);
for (let ptr of cell) {
ws.pointer(ptr);
}
}
}
return ws.seal();
}
// ## *flat()
// Returns an iterator that iterates over every cell, instead of row by row.
// Note that we might want to do make this the default iterator by the way!
*flat() {
for (let column of this) {
for (let cell of column) {
yield cell;
}
}
}
}
// # Cell
// Tiny class for representing a cell within the item index.
class Cell extends Array {
x = 0;
z = 0;
constructor(x = 0, z = 0) {
super();
this.x = x;
this.z = z;
}
// ## filter(fn)
// Overrides the native array filter function to filter the cell *in
// place*, which is useful because we don't replace cells in the item
// index!
filter(fn) {
// We'll first collect all the indices that need to be removed.
const { length } = this;
let indices = [];
for (let i = 0; i < length; i++) {
if (!fn(this[i], i, this)) {
indices.push(i);
}
}
// Next we'll actually remove the indices, but we take into account
// that the array's length is modified in the meantime!
for (let i = 0; i < indices.length; i++) {
let index = indices[i] - i;
this.splice(index, 1);
}
// Done!
return this;
}
}