UNPKG

sc4

Version:

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

241 lines (240 loc) 7.71 kB
// # util.ts import { util } from './node-builtins.js'; import { indexOf } from 'uint8array-extras'; // Julian day offset between unix epoch and Julian Date 0. const JULIAN_OFFSET = 2440587.5; const MS_DAY = 60 * 60 * 24 * 1000; // Export the node-builtins, taking into account that they might not be // available if we're running in the browser. export * from './node-builtins.js'; // # getUnixFromJulian(d) export function getUnixFromJulian(d) { return (d - JULIAN_OFFSET) * MS_DAY; } // # getJulianFromUnix(ms) export function getJulianFromUnix(ms) { return +ms / MS_DAY + JULIAN_OFFSET; } // # invertMap(map) // Inverts a map. export function invertMap(map) { return new Map([...map].map(([K, V]) => [V, K])); } // # invert(obj) // Inverts an object in-place. Beware of keys that are present as values as // well! export function invert(obj) { let out = Object.create(null); let keys = [ ...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj), ]; for (let key of keys) { out[String(obj[key])] = key; } return out; } // # hex(nr, pad) export function hex(nr, pad = 8) { return '0x' + (Number(nr).toString(16).padStart(pad, '0')); } hex.register = function () { if (!('hex' in Number.prototype)) { // eslint-disable-next-line no-extend-native Object.defineProperty(Number.prototype, 'hex', { value(pad) { return util.styleText('yellow', hex(this, pad)); }, }); } }; // # inspect // An object that contains some helper functions for nicely showing things when // using console.log in Node. const kInspect = Symbol.for('nodejs.util.inspect.custom'); export const inspect = { symbol: kInspect, type(value) { if (!value) return value; return { [Symbol.for('nodejs.util.inspect.custom')]() { return util.styleText('cyan', value); }, }; }, constructor(value) { return { [Symbol.for('nodejs.util.inspect.custom')]() { return util.styleText('cyan', typeof value === 'string' ? value : value.name); }, }; }, hex(value, pad) { return { [Symbol.for('nodejs.util.inspect.custom')](_depth, opts) { return opts.stylize(hex(value, pad), 'number'); }, }; }, tgi(object, label) { return { [Symbol.for('nodejs.util.inspect.custom')](_depth, opts, nodeInspect) { if (!object) return object; let prefix = label ? `${label} ` : ''; return `${prefix}${nodeInspect({ type: object.type && inspect.hex(object.type), group: object.group && inspect.hex(object.group), instance: object.instance && inspect.hex(object.instance), }, opts)}`; }, }; }, }; // # randomId(opts) // Returns a random id (for us in TGI), optionally accepting a list of ids that // we should not use. export function randomId(opts = {}) { let { except = [] } = opts; let set = new Set(except); let id; do { id = Math.floor(Math.random() * 0xffffffff) + 1; } while (set.has(id)); return id; } // # split(buffer) // Splits the given buffer that contains multiple "SIZE CRC MEM" records into // an array that contains the individual records. Note that we don't copy // buffers here, we're simply returnning buffer views on top of it. export function split(buffer) { let offset = 0; let slices = []; let view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); while (offset < buffer.length) { let size = view.getUint32(offset, true); let slice = buffer.subarray(offset, offset + size); offset += size; slices.push(slice); } return slices; } // # chunk(format, str) // Chunks the given string according to the given format. Useful when we need // to debug hex dumps. export function chunk(format, str) { let out = []; for (let i = 0; i < format.length; i++) { let length = format[i]; out.push(str.slice(0, length)); str = str.slice(length); } out.push(str); return out.join(' ').trim(); } // Utilty fundction for updating the string prototype. chunk.register = function () { if (!('chunk' in String.prototype)) { // eslint-disable-next-line no-extend-native Object.defineProperty(String.prototype, 'chunk', { value(format = []) { return chunk([...format, ...Array(100).fill(8)], this); }, }); } }; // # bin(nr) export function bin(nr) { return '0b' + Number(nr).toString(2).padStart(8, '0'); } // # tgi(type, group, id) // Returns a tgi id for the given type, group & id. Used for uniquely // identifying files. export function tgi(type, group, id) { return [type, group, id].map(x => hex(x)).join('-'); } // # getCityPath(city, region) // Helper function that easily gets the path of the given city in the given // region. Note that we automatically prefix with "City - " and postfix with // ".sc4". export function getCityPath(city, region = 'Experiments') { const path = process.getBuiltinModule('path'); let file = `City - ${city}.sc4`; return path.resolve(process.env.SC4_REGIONS ?? process.cwd(), region, file); } // # duplicateAsync(generator) // Allows re-using the same code for both a synchronous and asynchronous api. export function duplicateAsync(generator) { return { sync(...args) { let it = generator.call(this, ...args); let { done, value } = it.next(); while (!done) { ({ done, value } = it.next(value)); } return value; }, async async(...args) { let it = generator.call(this, ...args); let { done, value } = it.next(); while (!done) { ({ done, value } = it.next(await value)); } return value; }, }; } // # isLittleEndian() let isLE; export function isLittleEndian() { if (isLE === undefined) { let arr = new Uint32Array([0x11223344]); let view = new Uint8Array(arr.buffer); isLE = view[0] === 0x44; } return isLE; } // # isBigEndian() export function isBigEndian() { return !isLittleEndian(); } // # findPatternOffsets(buffer, pattern) // Finds all offsets of the given Uint8Array pattern. export function findPatternOffsets(buffer, pattern) { let index = 0; let pivot = buffer; let offsets = []; while (index > -1) { index = indexOf(pivot, pattern); if (index > -1) { offsets.push(index); pivot = pivot.subarray(index + pattern.length); } } return offsets; } // Helper function that mimicks the "safe assignment operator", asynchronously. export async function attempt(fn) { try { return [null, await fn()]; } catch (e) { return [e, null]; } } // # getCompressionInfo(buffer) // Returns some information about whether the buffer is QFS compressed or not. // It will also read in the uncompressed size if compressed. export function getCompressionInfo(buffer) { if (buffer.byteLength > 9 && buffer.byteLength <= 0xffffff && buffer[4] === 0x10 && buffer[5] === 0xfb) { let size = (buffer[6] << 16) + (buffer[7] << 8) + buffer[8]; return { compressed: true, size }; } else { return { compressed: false, size: buffer.byteLength }; } }