UNPKG

sc4

Version:

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

273 lines (272 loc) 9.5 kB
// # lot-index.ts import bsearch from 'binary-search-bounds'; import { Cohort, Exemplar, ExemplarProperty as Property, FileType, LotObject, } from 'sc4/core'; import { hex } from 'sc4/utils'; // # LotIndex // A helper class that we use to index lots by a few important properties. // They're sorted by height and such they will also remain so. This means that // when filtering, you can rest assured that they remain sorted by height as // well! export default class LotIndex { fileIndex; lots = []; height; // ## constructor(index) // Creates the lot index from the given file index. constructor(index) { // Store the file index, we'll still need it. this.fileIndex = index; // Loop every exemplar. If it's a lot configurations exemplar, then // read it so that we can find the building that appears on the lot. for (let entry of index.findAll({ type: FileType.Exemplar })) { let file = entry.read(); if (this.getPropertyValue(file, 0x10) !== 0x10) { continue; } // Cool, add the lot. this.add(entry); } // Now it's time to set up all our indices. For now we'll only index // by height though. this.height = new IndexedArray({ compare: (a, b) => a.height - b.height, entries: this.lots, }); } // ## add(entry) // Adds the given lot exemplar to the index. Note that we create a // LotIndexEntry for *every* building the lot cna be constructed with! add(entry) { // Find all buildings that can appear on this lot, which might happen // because they're part of a building family. let lot = entry.read(); let { lotObjects } = lot; let { IID } = lotObjects.find(({ type }) => type === LotObject.Building); let buildings = this.getBuildings(IID); // Then loop all those buildings and create LotIndexEntries for it. for (let building of buildings) { let lot = new LotIndexEntry(this.fileIndex, entry, building); this.lots.push(lot); } return this; } // ## getBuildings(IID) // Returns an array of all buildings exemplars idenfitied by the given // IID. If it's a single building, we'll return an array containing 1 // building, if it's a family, we return all buildings from the family. getBuildings(IID) { let buildings = this.fileIndex .findAll({ type: FileType.Exemplar, instance: IID }) .filter(entry => { let file = entry.read(); let type = this.getPropertyValue(file, 0x10); return type === 0x02; }); if (buildings.length > 0) { return [buildings.at(-1)]; } // No buildings found? Don't worry, check the families. let family = this.fileIndex.family(IID); if (!family) { throw new Error(`No building found with IID ${hex(IID)}!`); } return family; } // ## getBuilding(IID) getBuilding(IID) { let [building] = this.getBuildings(IID); return building; } // ## getPropertyValue(file, prop) // Helper function for quickly reading property values. getPropertyValue(file, key) { return this.fileIndex.getPropertyValue(file, key); } } // # LotIndexEntry // A class for representing a lot entry on the index. Note that we can't // simply use the lot exemplar because a lot might contain a building // *family*, and hence the characteristics of the lot may vary depending on // the building! Hence we'll create an entry for each (lot, building) // combination! class LotIndexEntry { #fileIndex; lot; building; // ## constructor(fileIndex, lot, building) constructor(fileIndex, lot, building) { // We have to keep a reference to the file index - though we'll "hide" // it on the IndexEntry - so that we're able to properly use // inheritance when reading stuff from the exemplars. this.#fileIndex = fileIndex; // Store the lot and building exemplars. this.lot = lot; this.building = building; } // ## get size() get size() { let [x, z] = this.getLotPropertyValue(Property.LotConfigPropertySize); return Object.assign([x, z], { x, z }); } // ## get buildingSize() get buildingSize() { let [x, y, z] = this.getBuildingPropertyValue(Property.OccupantSize); return Object.assign([x, y, z], { x, y, z }); } // ## get height() get height() { return this.buildingSize.z; } // ## get growthStage() get growthStage() { return this.getLotPropertyValue(Property.GrowthStage); } // ## get zoneTypes() get zoneTypes() { return this.getLotPropertyValue(Property.LotConfigPropertyZoneTypes); } // ## get occupantGroups() get occupantGroups() { return this.getBuildingPropertyValue(Property.OccupantGroups); } // ## getLotPropertyValue(prop) getLotPropertyValue(key) { return this.#fileIndex.getPropertyValue(this.lot.read(), key); } // ## getBuildingPropertyValue(prop) getBuildingPropertyValue(key) { return this.#fileIndex.getPropertyValue(this.building.read(), key); } } class IndexedArray { entries; compare; // ## constructor(opts) constructor(opts) { let { entries, compare, sorted = false } = opts; this.entries = entries; this.compare = compare; if (!sorted) this.entries.sort(compare); } // ## get length() get length() { return this.entries.length; } // ## at(index) at(index) { return this.entries.at(index); } // ## clone(entries) // Helper function for creating a clone of this indexed array, but with // possibly narrowed entries. This is why extending from a true array is // actually usefull because then we derive all the filter methods // automatically, but we have to limit subclassing arrays. clone(entries) { return new IndexedArray({ entries, compare: this.compare, sorted: true, }); } // ## getRangeIndices(min, max) getRangeIndices(min, max) { const { compare } = this; let first = bsearch.le(this.entries, min, compare) + 1; let last = bsearch.ge(this.entries, max, compare); return [first, last]; } // ## range(min, max) // Filters down the subselection to only include the given height range. // Note: perhaps that we should find a way to change the index criterion // easily, that's for later on though. range(min, max) { let [first, last] = this.getRangeIndices(min, max); return new IndexedArray({ entries: this.entries.slice(first, last), compare: this.compare, sorted: true, }); } // ## *it(min, max) // Helper function which allows a range to be used as an iterator. *it(min, max) { let [first, last] = this.getRangeIndices(min, max); for (let i = first; i < last; i++) { yield this.entries[i]; } } // ## query(query) // Helper function for carrying out a query using the normal array filter // method. Only exact queries are possible for the moment, no range // queries though that should be possible as well - see MongoDB for // example. query(query) { // First of all we'll build the query. Building the query means that // we're creating an array of functions which *all* need to pass in // order to evaluate to true. This means an "and" condition. let filters = []; for (let key in query) { let def = query[key]; // If the definition is an array, we'll check whether the value is // within the array. if (Array.isArray(def)) { filters.push($oneOf(key, def)); } else { filters.push($equals(key, def)); } } let entries = this.entries.filter((entry) => { for (let fn of filters) { if (!fn(entry)) { return false; } } return true; }); return this.clone(entries); } // ## filter() filter(fn) { return new IndexedArray({ entries: this.entries.filter(fn), compare: this.compare, sorted: true, }); } // *[Symbol.iterator]() // Allows iterating over the index as we would with normal arrays. *[Symbol.iterator]() { yield* this.entries; } } // ## $equals(key, value) function $equals(key, value) { return function (entry) { let x = entry[key]; if (Array.isArray(x)) { return x.includes(value); } else { return x === value; } }; } // ## $oneOf(key, arr) function $oneOf(key, arr) { return function (entry) { let x = entry[key]; if (Array.isArray(x)) { for (let el of x) { if (arr.includes(el)) { return true; } } return false; } else { return arr.includes(entry[key]); } }; }