UNPKG

sc4

Version:

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

191 lines (190 loc) 7.75 kB
// # plop-all-lots.js import shuffle from 'knuth-shuffle-seeded'; import { DBPF, Exemplar, ExemplarProperty, Savegame, TGI, Vector3 } from 'sc4/core'; import { PluginIndex, FileScanner } from 'sc4/plugins'; import CityManager from './city-manager.js'; export default async function plopAllLots(opts) { // First thing we'll do is looking up all lot files with a file scanner. let { pattern = '**/*', directory = process.env.SC4_PLUGINS ?? process.cwd(), logger, } = opts; let lots = await new FileScanner(pattern, { cwd: directory }).walk(); if (lots.length === 0) { logger?.warn(`No files found that match the pattern ${pattern} in ${directory}.`); } // Check if we have to plop the lots in random order. lots.sort(); let { random: seed } = opts; if (seed) { if (seed === true) { shuffle(lots); } else { shuffle(lots, seed); } } // First of all we need to index the plugin folder, but only if there are // actually lots to be plopped of course. const { installation, plugins } = opts; const index = new PluginIndex({ installation, plugins }); if (lots.length > 0) { logger?.progress.start('Building plugin index...'); await index.build(); logger?.progress.update('Indexing building & prop families...'); await index.buildFamilies(); logger?.progress.succeed('Plugin index built'); } // Open the savegame where we have to plop everything. const { city: cityId } = opts; const mgr = new CityManager({ index }); const city = mgr.load(cityId); // If we have to clear the city first, do it. if (opts.clear) mgr.clear(); // Now loop all files const { bbox } = opts; if (opts.lots !== false) { logger?.progress.start('Plopped 0 lots'); let i = 0; for (let file of lots) { let dbpf = new DBPF({ file, parse: false }); await dbpf.parseAsync(); for (let entry of dbpf.exemplars) { // If this is not a lot exemplar, no need to continue. let exemplar = entry.read(); if (exemplar.value(0x10) !== 0x10) continue; let pos = findPosition(city, exemplar, bbox); if (!pos) { let name = exemplar.value(0x20); logger?.warn(`Unable to find a suitable position for ${name}`); continue; } let { x, z, orientation } = pos; let result = mgr.grow({ tgi: entry.tgi, x, z, orientation, }); if (result !== false) { i++; logger?.progress.update(`Plopped ${i} lots`); } } } logger?.progress.succeed(); } // For the props, we first have to build a data structure that holds which // 1x1 m² squares are occupied. We do this from the zone developer. if (opts.props) { let { width: cityWidth, depth: cityDepth, zoneDeveloper: zones } = city; let occupiedBuffer = new ArrayBuffer(cityWidth * cityDepth * 16 ** 2); let occupied = new Array(16 * cityDepth).fill(null).map((_, i) => { let width = 16 * cityWidth; return new Uint8Array(occupiedBuffer, i * width, width); }); for (let z = 0; z < cityDepth; z++) { for (let x = 0; x < cityWidth; x++) { if (!zones.isOccupied(x, z)) continue; let dx = 16 * x; let dz = 16 * z; for (let j = 0; j < 16; j++) { for (let i = 0; i < 16; i++) { occupied[dz + j][dx + i] = 1; } } } } // Loop all files again, but now look for the props. logger?.progress.start('Added 0 props'); let i = 0; for (let file of lots) { let dbpf = new DBPF({ file, parse: false }); await dbpf.parseAsync(); for (let entry of dbpf.exemplars) { let exemplar = entry.read(); let type = exemplar.get('ExemplarType'); if (type !== ExemplarProperty.ExemplarType.Prop) continue; let position = findPropPosition(city, occupied, exemplar, bbox); if (!position) { let name = exemplar.get('ExemplarName'); logger?.warn(`Unable to find a suitable positoin for prop ${name}`); continue; } let prop = mgr.createProp({ exemplar, position, tgi: new TGI(entry.tgi), }); if (prop) { i++; logger?.progress.update(`Added ${i} props`); } } } logger?.progress.succeed(); } // Save at last. If a backup function was specified, we'll first call it. if (opts.save) { if (opts.backup && city.file) { await opts.backup(city.file, opts); } const { output = city.file } = opts; await city.save({ file: output }); } return city; } // # findPosition(city, exemplar, bbox) // Finds a suitable position for the given lot exemplar to plop. function findPosition(city, exemplar, bbox = []) { const [width, depth] = exemplar.get('LotConfigPropertySize') ?? [0, 0]; const { zoneDeveloper: zones, width: cityWidth, depth: cityDepth } = city; const [minX = 0, minZ = 0, maxX = cityWidth, maxZ = cityDepth] = bbox; for (let z = minZ; z <= maxZ - depth; z++) { outer: for (let x = minX; x <= maxX - width; x++) { for (let i = 0; i < width; i++) { for (let j = 0; j < depth; j++) { if (zones.isOccupied(x + i, z + j)) continue outer; } } // If we reach this point, it means there is sufficient space to // grow the lot. Do it. return { x, z, orientation: 2 }; } } // If we reach this point, it means no suitable position has been found. // Pity. return null; } // # findPropPosition(city, exemplar, bbox) function findPropPosition(city, occupied, exemplar, bbox = []) { const s = (x) => 16 * x; const r = (x) => Math.ceil(x); let { width: cityWidth, depth: cityDepth } = city; let [width, , depth] = (exemplar.get('OccupantSize') ?? [0, 0, 0]).map(r); let [minX = 0, minZ = 0, maxX = 16 * cityWidth, maxZ = 16 * cityDepth,] = bbox.map(s); // For the props, we use cells of 1x1 *meters*, but the bbox and city size // are obviously given in tiles. for (let z = minZ; z <= maxZ - depth; z++) { outer: for (let x = minX; x <= maxX - width; x++) { for (let i = 0; i < width; i++) { for (let j = 0; j < depth; j++) { if (occupied[z + j][x + i]) continue outer; } } // If we reach this point, it means there is sufficient space to put // the prop on. We'll return the position, but we'll also fill up // the occupied buffer. for (let i = 0; i < width; i++) { for (let j = 0; j < depth; j++) { occupied[z + j][x + i] = 1; } } return new Vector3(x + width / 2, 0, z + depth / 2); } } return null; }