UNPKG

sc4

Version:

A command line utility for automating SimCity 4 modding tasks & modifying savegames

153 lines (152 loc) 5.55 kB
import Pointer from './pointer.js'; import { hex } from 'sc4/utils'; import { isArrayType, readRecordsAsBuffers } from './helpers.js'; import { SmartBuffer } from 'smart-arraybuffer'; import { cClass } from './enums.js'; // # SavegameContext // Class for providing some context around a savegame file. It is mainly used // for tracking memory addresses used in the savegame. This makes it possibly to // generate new memory addresses that are guaranteed to be unique, as well as to // dereference points that might appear in the savegame. export default class SavegameContext { dbpf; memRefs = new Set(); #mem = 1; // ## constructor(dbpf) constructor(dbpf) { this.dbpf = dbpf; for (let { pointer } of this.getFlatRecordList()) { this.memRefs.add(pointer.address); } } // ## mem() // Returns a memory address (just a number) that is not in use in the // savegame yet. This allows us to insert content in a savegame file while // ensuring that the memory address of it won't conflict with another entry. mem() { let ref = this.#mem++; while (this.memRefs.has(ref)) { ref = this.#mem++; } return ref; } // ## deref(pointer) // Dereferences a pointer, meaning that we'll look up the entry in its // corresponding subfile. Note that we don't use an index here, so for very // large subfiles - such as the prop file - this is O(n) and can take up // quite a bit of time! Consider indexing it first if you have to do this. deref(pointer) { if (!pointer || pointer.type === 0x00000000) { throw new Error(`Trying to dereference a null pointer!`); } let { type, address } = pointer; let entry = this.dbpf.find({ type }); if (!entry) { throw new Error(`Trying to dereference a pointer from subfile ${hex(type)}, which does not exist in the savegame!`); } let file = entry.read(); if (file instanceof Uint8Array) { let buffers = readRecordsAsBuffers(entry.decompress()); let record = buffers.find(buffer => { let reader = SmartBuffer.fromBuffer(buffer); let mem = reader.readUInt32LE(8); return mem === address; }); if (!record) { throw new Error(`Trying to dereference a pointer to a non-existent record!`); } return record; } let result; if (!Array.isArray(file) || isArrayType(file)) { result = find([file], address); } else { result = find(file, address); } if (!result) { throw new Error(`Trying to dereference a pointer to a non-existent record!`); } return result; } // Kept for legacy purposes, but we'd like to get rid of this. findAllMemRefs(dbpf = this.dbpf) { let all = []; let list = this.getRecordList(); for (let { type, records, entry } of list) { for (let record of records) { all.push({ mem: record.pointer.address, type, entry, index: record.offset, }); } } return all; } // ## getRecordList() // Returns a list of all records (could be sub-records) in the dbpf that // use a memory reference (i.e. have general structure SIZE CRC MEM). // We're using the CRC to detect if this kind of entry works this way. getRecordList(dbpf = this.dbpf) { let all = []; for (let entry of dbpf) { // If the entry does not follow the SIZE CRC MEM convention, then we // don't include it. let buffer = entry.decompress(); let buffers = readRecordsAsBuffers(buffer); if (buffers.length === 0) continue; // Create the rows. let row = { entry, type: entry.type, label: cClass[entry.type], records: [], byteLength: buffer.byteLength, }; let offset = 0; for (let i = 0; i < buffers.length; i++) { let buffer = buffers[i]; let address = SmartBuffer.fromBuffer(buffer).readUInt32LE(8); row.records.push({ pointer: new Pointer(entry.type, address), type: entry.type, label: row.label, address, offset, buffer, }); offset += buffer.byteLength; } all.push(row); } return all; } // ## getFlatRecordList() getFlatRecordList() { let list = this.getRecordList(); return list.map(row => row.records).flat(); } // ## getRecordCountTable() getRecordCountTable() { let list = this.getRecordList(); return list.map(row => { return { name: row.label, count: row.records.length, bytes: row.byteLength, }; }); } } // # find() // Helper function for finding an object with the given address. function find(array, address) { return array.find(record => { if (!('mem' in record)) return false; return record.mem === address; }); }