sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
273 lines (272 loc) • 9.5 kB
JavaScript
// # 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]);
}
};
}