UNPKG

sc4

Version:

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

380 lines (379 loc) 12 kB
// # dependency-types.js import chalk from 'chalk'; import path from 'node:path'; import { util, inspect, hex } from 'sc4/utils'; import { LotObjectType, getTypeLabel } from 'sc4/core'; const DEFAULT_WIDTH = 150; // # Dependency export class Dependency { entry; constructor({ entry }) { this.entry = entry; } toString(opts = {}) { return this.toLines(opts).join('\n'); } // ## get id() // The default dependency id is just the entry id. This is overridden though // for missing dependencies as they have no entries associated with them! // Same for families. get id() { return this.entry.id; } // ## get children() // By default a dependency has no children. If a dependency has cildren, it // needs to implement it itself. get children() { return []; } } // # Lot // Represents a lot in the dependency tree. export class Lot extends Dependency { kind = 'lot'; name = ''; foundation = null; building = null; textures = []; props = []; flora = []; parent = null; // ## constructor(opts) constructor(opts) { super(opts); Object.assign(this, opts); } // ## get children() // Returns all dependencies that are considered children of this dependency. // Note that certain dependencies might actually be *families*, meaning we // have to flatten the array! get children() { return [ this.foundation, this.building, ...this.textures, ...this.props, ...this.flora, this.parent, ].flat(1).filter(dep => !!dep); } // ## toLines() toLines(opts = {}) { let { width = DEFAULT_WIDTH, level = 0, root = true } = opts; let lines = []; let { name, entry } = this; if (root) { lines.push(chalk.magenta('Lot')); } lines.push($(width, `${' '.repeat(2 * level)}${name} ${entryToString(entry)}`, entry.dbpf?.file)); let { foundation, building, textures, props, flora, parent } = this; let s = ' '.repeat(2 * (level + 1)); level += 2; if (foundation) { lines.push(`${s}${chalk.green('Foundation')}`, ...foundation.toLines({ width, level, root: false })); } if (building) { lines.push(`${s}${chalk.green('Building')}`, ...building.toLines({ width, level, root: false })); } if (textures.length > 0) { lines.push(`${s}${chalk.green('Textures')}`); for (let texture of textures) { lines.push(...texture.toLines({ width, level, root: false })); } } if (props.length > 0) { lines.push(`${s}${chalk.green('Props')}`); for (let prop of props) { lines.push(...prop.toLines({ width, level, root: false })); } } if (flora.length > 0) { lines.push(`${s}${chalk.green('Flora')}`); for (let item of flora) { lines.push(...item.toLines({ width, level, root: false })); } } if (parent) { lines.push(`${s}${chalk.green('Parent')}`); lines.push(...parent.toLines({ width, level, root: false })); } if (root) { lines.push(''); } return lines; } [Symbol.for('nodejs.util.inspect.custom')](_level, opts, nodeInspect) { let { entry, ...rest } = this; return `Lot ${nodeInspect({ entry: inspect.tgi(entry, 'Entry'), ...rest, }, opts)}`; } } // # getLotSetter(lot, type) // Returns a setter that allows us to set a specific lot object in a deferred // way - i.e. further up the event loop. export const getLotSetter = (lot, type) => { switch (type) { case LotObjectType.Building: return (object) => lot.building = object; case LotObjectType.Texture: return getArraySetter(lot.textures); case LotObjectType.Prop: return getArraySetter(lot.props); case LotObjectType.Flora: return getArraySetter(lot.flora); default: return () => void null; } }; // # getArraySetter(array) const getArraySetter = (array) => { return (dep) => { let ids = new Set(array.map(dep => dep.id)); if (!ids.has(dep.id)) { array.push(dep); array.sort(); } return dep; }; }; // # Family // Represents a family in the dependency tree. Note that this could be a // building, prop or flora family, but we don't make an explicit distinction // here. export class Family extends Dependency { kind = 'family'; familyId; elements; // ## constructor(id, children) constructor(children, id) { let entry = { instance: id ?? 0 }; super({ entry }); this.elements = children; this.familyId = id; } // ## toLines() toLines({ width, level = 0 }) { let line = `${' '.repeat(2 * level)}${chalk.green('Family')}`; if (this.familyId !== undefined) { line += ` ${chalk.yellow(hex(this.familyId))}`; } let lines = [line]; level += 1; for (let dep of this.elements) { lines.push(...dep.toLines({ width, level, root: false })); } return lines; } // ## get id() get id() { let id = this.familyId ?? [...this.elements] .map(dep => dep.id) .join('/'); return `family/${id}`; } // ## get children() get children() { return this.elements; } } // # Texture // Represents a texture in the dependency tree. export class Texture extends Dependency { kind = 'texture'; toLines({ width = DEFAULT_WIDTH, level = 0, root = true }) { let s = ' '.repeat(2 * level); let l = root ? chalk.magenta('Texture ') : ''; return [$(width, `${s}${l}${chalk.yellow(hex(this.entry.instance))}`, this.entry.dbpf?.file)]; } [Symbol.for('nodejs.util.inspect.custom')]() { return inspect.tgi(this.entry, 'Texture'); } } // # Model // Represents a model in the dependency tree. export class Model extends Dependency { kind = 'model'; // ## toLines() toLines({ width = DEFAULT_WIDTH, level = 0 } = {}) { let { entry } = this; let lines = []; lines.push($(width, `${' '.repeat(2 * level)}Model ${entryToString(entry)}`, entry.dbpf?.file)); return lines; } [Symbol.for('nodejs.util.inspect.custom')]() { return inspect.tgi(this.entry, 'Model'); } } const exemplarTypes = { 0x00: 'Other', 0x01: 'Tuning', 0x02: 'Buildings', 0x03: 'RCI', 0x04: 'Developer', 0x05: 'Simulator', 0x06: 'Road', 0x07: 'Bridge', 0x08: 'MiscNetwork', 0x09: 'NetworkIntersection', 0x0a: 'Rail', 0x0B: 'Highway', 0x0c: 'PowerLine', 0x0d: 'Terrain', 0x0e: 'Ordinances', 0x0f: 'Flora', 0x10: 'Lotconfigurations', 0x11: 'Foundations', 0x12: 'Advice', 0x13: 'Lighting', 0x14: 'Cursor', 0x15: 'LotReainingWalls', 0x16: 'Vehicles', 0x17: 'Pedestrians', 0x18: 'Aircraft', 0x19: 'Watercraft', 0x1e: 'Prop', 0x1f: 'Construction', 0x20: 'Automata Tuning', 0x21: 'Type 21', 0x22: 'Disaster', 0x23: 'Data view', 0x24: 'Crime', 0x25: 'Audio', 0x26: 'My Sim Template', 0x27: 'TerrainBrush', 0x28: 'Misc Catalog', }; export class Exemplar extends Dependency { kind = 'exemplar'; exemplarType = 0x00; name = ''; parent = null; models = []; props = []; // ## constructor(opts) constructor(opts) { super(opts); Object.assign(this, opts); } // ## get children() get children() { return [ ...this.models, this.parent, ...this.props.map(row => row[1]), ].filter(dep => !!dep); } // ## toLines(opts) toLines({ width = DEFAULT_WIDTH, level = 0, root = true } = {}) { let lines = []; let { name, entry } = this; if (root) { let type = exemplarTypes[this.exemplarType]; lines.push(`${chalk.magenta('Exemplar')} ${chalk.gray(`(${type})`)}`); } lines.push($(width, `${' '.repeat(2 * level)}${name} ${entryToString(entry)}`, entry.dbpf?.file)); if (this.models.length > 0) { for (let model of this.models) { lines.push(...model.toLines({ width, level: level + 1 })); } } for (let [name, { entry }] of this.props) { if (!entry) continue; lines.push($(width, `${' '.repeat(2 * (level + 1))}${name} ${entryToString(entry)}`, entry.dbpf?.file)); } if (this.parent) { let s = ' '.repeat(2 * (level + 1)); lines.push(`${s}${chalk.green('Parent')}`, ...this.parent.toLines({ width, level: level + 1, root: false })); } if (root) lines.push(''); return lines; } [Symbol.for('nodejs.util.inspect.custom')](_level, opts, nodeInspect) { let { entry, ...rest } = this; return `Exemplar ${nodeInspect({ entry: inspect.tgi(entry, 'Entry'), ...rest, }, opts)}`; } } // # Raw // Represents a raw resource in the dependency tree. This means that all we do // here is show the tgi. There's no need to parse anything in the resource // itself because it can't reference others. export class Raw extends Dependency { kind = 'raw'; get label() { return getTypeLabel(this.entry.type); } toLines({ width = DEFAULT_WIDTH, root = true } = {}) { let line = chalk.yellow(this.label ?? hex(this.entry.type)); if (root) { line = `${chalk.magenta('Raw')} ${line}`; } return [$(width, line, this.entry.dbpf?.file)]; } [Symbol.for('nodejs.util.inspect.custom')]() { return inspect.tgi(this.entry, this.label); } } // # Missing // Represents a mising dependency export class Missing extends Dependency { kind = 'missing'; resource; parent; constructor(opts) { let { type, group, instance, resource, parent } = opts; super({ entry: { type, group, instance }, }); this.resource = resource; this.parent = parent; } toLines({ width = DEFAULT_WIDTH, level = 0 } = {}) { const { type, group, instance } = this.entry; let prefix = `${' '.repeat(2 * level)}`; if (!type) { return [$(width, `${prefix}${chalk.yellow(hex(instance))}`)]; } else if (instance && group) { let tgi = [type, group, instance].map(x => chalk.yellow(hex(x))); return [$(width, `${prefix}${tgi}`)]; } else { return []; } } get id() { const { type = 0, group = 0, instance = 0 } = this.entry; return `missing/${type}-${group}-${instance}`; } [Symbol.for('nodejs.util.inspect.custom')]() { return inspect.tgi(this.entry, util.styleText('red', 'MISSING')); } } // # entryToString(entry) function entryToString({ type, group, instance }) { if (!group) { return chalk.yellow(hex(instance)); } if (type === undefined) { return chalk.yellow(hex(instance)); } else { return [type, group, instance].map(nr => { return chalk.yellow(hex(nr)); }).join('-'); } } function $(width, line, file) { // eslint-disable-next-line no-control-regex let filtered = line.replaceAll(/\x1B\[\d+m/g, ''); let basename = file ? path.basename(file) : 'Not found'; let spaces = ' '.repeat(Math.max(width - basename.length - filtered.length, 0)); return `${line}${spaces}${chalk[file ? 'cyan' : 'red'](basename)}`; }