sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
374 lines (373 loc) • 12.3 kB
JavaScript
// # lot-file.js
import WriteBuffer from './write-buffer.js';
import { FileType, ZoneType, DemandSourceIndex } from './enums.js';
import { kFileType, kFileTypeArray } from './symbols.js';
// Predefined bit-flags.
const Flags = {
Historical: 0x20,
Watered: 0x08,
Powered: 0x10,
};
// # Lot
// Represents a single lot from the lot file
export default class Lot {
static [kFileType] = FileType.Lot;
static [kFileTypeArray] = true;
crc = 0x00000000;
mem = 0x00000000;
major = 0x0008;
IID = 0x00000000;
flag1 = 0b01000000;
minX = 0x00;
minZ = 0x00;
maxX = 0x00;
maxZ = 0x00;
commuteX = 0x00;
commuteZ = 0x00;
yPos = 0;
ySlope1 = 0;
ySlope2 = 0;
width = 0x00;
depth = 0x00;
orientation = 0x00;
flag2 = 0x03;
flag3 = 0x00;
zoneType = 0x00;
zoneWealth = 0x00;
dateCreated = 0x00000000;
buildingIID = 0x00000000;
unknown5 = 0x00;
linkedIndustrial = 0x00000000;
linkedAgricultural = 0x00000000;
jobCapacities = [];
jobTotalCapacities = [];
$ = 0;
$$ = 0;
$$$ = 0;
unknown6 = 0x0002;
sgprops = [];
commutes = [];
commuteBuffer;
debug = 0x00;
constructor(opts) {
Object.assign(this, opts);
}
// ## get/set historical()
get historical() {
return Boolean(this.flag1 & Flags.Historical);
}
set historical(on) {
this.flag1 = set(Flags.Historical, this.flag1, on);
}
// ## get isPlopped()
// Getter for checking whether a lot is a plopped lot.
get isPlopped() {
return this.zoneType === ZoneType.Plopped;
}
// ## isResidential()
// Getter for checking whether this lot is a residential lot. A lot is
// considered residential if it has a residential demand source index.
get isResidential() {
let jobs = this.jobCapacities;
for (let job of jobs) {
switch (job.demandSourceIndex) {
case DemandSourceIndex.R$:
case DemandSourceIndex.R$$:
case DemandSourceIndex.R$$$:
return true;
}
}
return false;
}
// ## get isPloppedResidential()
// Getter for checking whether this lot is a plopped residential. We do
// this by checking first of all whether the lot is plopped or not. This
// is stored in zoneType, and plopped lots have zoneType 0x0f. Then we
// still have to figure out if this lot is a residential lot, it could be
// a park etc. as well. Therefore we'll ask the jobCapacities. If it has
// R somewhere here, it's a residential plopped building. Great!
get isPloppedResidential() {
return this.isPlopped && this.isResidential;
}
// ## get isCommercial()
get isCommercial() {
let jobs = this.jobCapacities;
for (let job of jobs) {
switch (job.demandSourceIndex) {
case DemandSourceIndex.CS$:
case DemandSourceIndex.CS$$:
case DemandSourceIndex.CS$$$:
case DemandSourceIndex.CO$$:
case DemandSourceIndex.CO$$$:
return true;
}
}
return false;
}
// ## get isPloppedCommercial()
// Just like `isPloppedResidential`, this checks if the lot is to be
// considered a plopped commercial. This is the case if the ZoneTYpe is
// set to plopped *and* it has commercial capacity. Note that this means
// that *functional* landmarks might get growified this way as well!
get isPloppedCommercial() {
return this.isPlopped && this.isCommercial;
}
// ## get isAgricultural()
get isAgricultural() {
let jobs = this.jobCapacities;
for (let job of jobs) {
if (job.demandSourceIndex === DemandSourceIndex.IR)
return true;
}
return false;
}
// ## get isPloppedAgricultural()
get isPloppedAgricultural() {
return this.isPlopped && this.isAgricultural;
}
// ## get isIndustrial()
// Getter for checking whether this lot is industrial, which is the case
// if there are industrial jobs.
// Note: agricultural is **NOT** treated as industrial! Use is
// AgriCultural for this!
get isIndustrial() {
let jobs = this.jobCapacities;
for (let job of jobs) {
switch (job.demandSourceIndex) {
case DemandSourceIndex.ID:
case DemandSourceIndex.IM:
case DemandSourceIndex.IHT:
return true;
}
}
return false;
}
// ## get isPloppedIndustrial()
// Same principle as isPloppedResidential
get isPloppedIndustrial() {
return this.isPlopped && this.isIndustrial;
}
move(dx, dz) {
if (Array.isArray(dx)) {
[dx, dz] = dx;
}
dx = dx || 0;
dz = dz || 0;
this.minX += dx;
this.maxX += dx;
this.commuteX += dx;
this.minZ += dz;
this.maxZ += dz;
this.commuteZ += dz;
return this;
}
// ## parse(rs)
// Parses the load from a buffer wrapped up in a readable stream.
parse(rs) {
rs.size();
this.crc = rs.dword();
this.mem = rs.dword();
this.major = rs.word();
this.IID = rs.dword();
this.flag1 = rs.byte();
this.minX = rs.byte();
this.minZ = rs.byte();
this.maxX = rs.byte();
this.maxZ = rs.byte();
this.commuteX = rs.byte();
this.commuteZ = rs.byte();
this.yPos = rs.float();
this.ySlope1 = rs.float();
this.ySlope2 = rs.float();
this.width = rs.byte();
this.depth = rs.byte();
this.orientation = rs.byte();
this.flag2 = rs.byte();
this.flag3 = rs.byte();
this.zoneType = rs.byte();
this.zoneWealth = rs.byte();
this.dateCreated = rs.dword();
this.buildingIID = rs.dword();
this.unknown5 = rs.byte();
this.linkedIndustrial = rs.dword();
if (this.linkedIndustrial !== 0) {
// 0x4A232DA8
rs.skip(4);
}
this.linkedAgricultural = rs.dword();
if (this.linkedAgricultural !== 0) {
// 0xC9BD5D4A
rs.skip(4);
}
// Read job capacities. Note that the count byte is either 0 or 1, so
// this means there will ever only be 1 sub array. No need to create
// this sub array then, just flatten it out directly.
let count = rs.byte();
this.jobCapacities.length = 0;
for (let i = 0; i < count; i++) {
let typeCount = rs.byte();
for (let i = 0; i < typeCount; i++) {
let demandSourceIndex = rs.uint32();
let capacity = rs.uint16();
this.jobCapacities.push({ demandSourceIndex, capacity });
}
}
// Read total job capacities.
count = rs.byte();
this.jobTotalCapacities.length = count;
for (let i = 0; i < count; i++) {
let demandSourceIndex = rs.uint32();
let capacity = rs.uint16();
this.jobTotalCapacities[i] = { demandSourceIndex, capacity };
}
this.$ = rs.float();
this.$$ = rs.float();
this.$$$ = rs.float();
this.unknown6 = rs.word();
this.sgprops = rs.sgprops();
// Read the amount of commute blocs.
count = rs.dword();
this.commutes.length = count;
// For now we're not parsing the commutes. Structure is still a bit
// unclear, we'll do this later. Simply store the raw buffer.
if (count > 0) {
this.commuteBuffer = rs.read(rs.remaining() - 1);
}
// for (let i = 0; i < count; i++) {
// if (!debug) break;
// let block = this.commutes[i] = new CommuteBlock();
// block.parse(rs, debug);
// break;
// }
// Read the last byte, is unknown apparently.
this.debug = rs.byte();
// Make sure the entry was read correctly.
rs.assert();
// Done!
return this;
}
// # toBuffer()
toBuffer() {
// Some shorthands.
const p1 = this.linkedIndustrial;
const p2 = this.linkedAgricultural;
// Start filling the buffer now in a smart way.
let ws = new WriteBuffer();
ws.dword(this.mem);
ws.word(this.major);
ws.dword(this.IID);
ws.byte(this.flag1);
ws.byte(this.minX);
ws.byte(this.minZ);
ws.byte(this.maxX);
ws.byte(this.maxZ);
ws.byte(this.commuteX);
ws.byte(this.commuteZ);
ws.float(this.yPos);
ws.float(this.ySlope1);
ws.float(this.ySlope2);
ws.byte(this.width);
ws.byte(this.depth);
ws.byte(this.orientation);
ws.byte(this.flag2);
ws.byte(this.flag3);
ws.byte(this.zoneType);
ws.byte(this.zoneWealth);
ws.dword(this.dateCreated);
ws.dword(this.buildingIID);
ws.byte(this.unknown5);
// Write await the pointers to linked lots.
ws.dword(p1);
if (p1 > 0)
ws.dword(0x4A232DA8);
ws.dword(p2);
if (p2 > 0)
ws.dword(0xC9BD5D4A);
// Remember: jobCapacities is a flat array, but it isn't in the
// buffer, so take this into account.
ws.byte(this.jobCapacities.length ? 1 : 0);
if (this.jobCapacities.length) {
ws.byte(this.jobCapacities.length);
for (let entry of this.jobCapacities) {
ws.dword(entry.demandSourceIndex);
ws.word(entry.capacity);
}
}
// Total job capacities is a flat array in the buffer. No special
// treatment here.
ws.byte(this.jobTotalCapacities.length);
for (let entry of this.jobTotalCapacities) {
ws.dword(entry.demandSourceIndex);
ws.word(entry.capacity);
}
// Go on.
ws.float(this.$);
ws.float(this.$$);
ws.float(this.$$$);
ws.word(this.unknown6);
ws.array(this.sgprops);
// Create a new buffer for the amount of commute blocks.
ws.dword(this.commutes.length);
// Check if there's a commute buffer. Include it as is. We don't allow
// modifying the commutes for now.
let commuteBuffer = this.commuteBuffer || new Uint8Array(0);
ws.write(commuteBuffer);
// A last buffer for the debug symbol.
ws.byte(this.debug);
// Seal the buffer and return.
return ws.seal();
}
}
// # CommuteBlock
// Represents a commmute block that is part of a lot.
// eslint-disable-next-line no-unused-vars
class CommuteBlock {
paths = [];
unknown1 = 0x00;
unknown2 = 0x00;
unknown3 = 0x08;
destinationX = 0x00;
destinationY = 0x00;
tripLength = 0;
unknown4 = 0x00000002;
// ## parse(rs)
parse(rs) {
let pathCount = rs.dword();
this.paths.length = pathCount;
for (let i = 0; i < pathCount; i++) {
let path = this.paths[i] = new CommutePath();
path.parse(rs);
}
// console.log(rs.buffer.slice(rs.i, rs.i+20).toString('hex'));
// this.path = new CommutePath();
// this.path.parse(rs);
}
}
// # CommutePath
class CommutePath {
startType = 0x00;
coords = [0x00, 0x00];
parse(rs) {
// Read the full path buffer.
// let pos = rs.i;
// let size = rs.dword();
// rs.jump(pos);
// rs = new Stream(rs.read(size));
// rs.skip(4);
// // Now repeat until we've read the entire path.
// while (!rs.eof()) {
// let swap = rs.byte();
// let length = rs.byte();
// console.log(length);
// }
// console.log(rs.toString('hex'));
// let size = rs.dword();
// this.startType = rs.byte();
// this.coords = [rs.byte(), rs.byte()];
// console.log(this.coords);
}
}
// Helper function for switching bit flags on & off.
function set(bit, flag, on) {
return on ? bit | flag : bit & 0xff - flag;
}