sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
103 lines (102 loc) • 3.9 kB
JavaScript
// # helpers.ts
import { SmartBuffer } from 'smart-arraybuffer';
import cppClasses from './cpp-classes.js';
import crc32 from './crc.js';
import { kFileType, kFileTypeArray } from './symbols.js';
import { FileType } from './enums.js';
import { findPatternOffsets } from 'sc4/utils';
// # getClassType(object)
// Inspects the object and returns its Type ID. If a class constructor is
// specified, we hence return the type id of this constructor, if it's an
// instance we look it up in the constructor.
export function getClassType(object) {
if (kFileType in object) {
return object[kFileType];
}
else if (kFileType in object.constructor) {
return object.constructor[kFileType];
}
else {
return 0;
}
}
// # isArrayType(object)
// Returns whether the given object is an array type subfile.
export function isArrayType(object) {
if (kFileTypeArray in object) {
return !!object[kFileTypeArray];
}
else if (kFileTypeArray in object.constructor) {
return !!object.constructor[kFileTypeArray];
}
else {
return false;
}
}
// # getTypeLabel(value)
// Looks up the label of the given file type. If it is known, we return a
// string, otherwise we return nothing.
export function getTypeLabel(value) {
const entries = Object.entries(FileType);
return entries.find(([, type]) => type === value)?.[0];
}
// # readRecordsAsBuffers(entry)
// This function can be useful when decoding the savegame files. It accepts an
// entry that consists out of multiple SIZE MEM CRC records, and returns the
// array of raw buffers. Note that we return a shallow copy, so the underlying
// memory is the same! It can be used to modify values of subfiles of which the
// structure is not known yet.
export function readRecordsAsBuffers(buffer) {
// If the buffer can't even hold SIZE CRC MEM, then we skip it.
if (buffer.byteLength < 12)
return [];
// If what we're interpreting as size is larged than the buffer,
// it's impossible that this has the structure "SIZE CRC MEM"!
let reader = SmartBuffer.fromBuffer(buffer);
let size = reader.readUInt32LE(0);
if (size > buffer.byteLength)
return [];
// Note that there may be multiple records in this buffer. We're
// going to parse them one by one and calculate the checksum. If the
// checksum matches, we're considering them to have the structure
// "SIZE CRC MEM".
let slice = buffer.subarray(8, size);
let crc = crc32(slice);
if (crc !== reader.readUInt32LE(4))
return [];
// Allright, first entry is of type "SIZE MEM CRC", we assume that
// all following entries are as well.
let records = [];
records.push(reader.readUint8Array(size));
let index = size;
buffer = buffer.subarray(size);
while (buffer.byteLength >= 12) {
let reader = SmartBuffer.fromBuffer(buffer);
let size = reader.readUInt32LE(0);
records.push(reader.readUint8Array(size));
index += size;
buffer = buffer.subarray(size);
}
return records;
}
// # removePointers(record)
// Nullifies all memory addresses for all pointers in the given record buffer.
// This is useful when comparing unknown savegame files where you want to make
// abstraction of the memory address, which can be different.
let knownTypes;
export function removePointers(record, replace = new Uint8Array(4)) {
if (!knownTypes) {
knownTypes = Object.keys(cppClasses)
.map(type => new Uint32Array([+type]))
.map(arr => new Uint8Array(arr.buffer));
}
for (let ptr of knownTypes) {
let offsets = findPatternOffsets(record, ptr);
for (let index of offsets) {
for (let i = 0; i < 4; i++) {
record[index - 4 + i] = replace[i];
}
}
}
return record;
}