sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
186 lines (185 loc) • 5.9 kB
JavaScript
import BinaryIndex from './binary-tgi-index.js';
// # TGIIndex
// A data structure that allows for efficiently querying objects by (type,
// group, instance).
export default class TGIIndex extends Array {
index;
dirty = false;
// ## build()
// A TGI collection doesn't create an index by default. We only do this when
// the build() is called. That way it becomes easy to use `.filter()` etc.
// without automatically rebuilding the index.
build() {
let tree = BinaryIndex.fromEntries(this);
this.index = tree;
this.dirty = false;
return this;
}
// ## load(cache)
// Loads an index from a cache instead of building it up ourselves.
load(buffer) {
this.index = BinaryIndex.fromBuffer(buffer);
return this;
}
find(...args) {
let result = this.findAll(...args);
return result.at(-1);
}
findAll(query, group, instance) {
// If the query is specified as a function, use it as such.
if (typeof query === 'function') {
return super.filter(query, group);
}
// If we have no index, then we have no choice but to loop everything
// manually.
let q = normalize(query, group, instance);
if (!q)
return [];
if (!this.index) {
return super.filter(createFilter(q));
}
// If the index is dirty, we first have to rebuild it.
if (this.dirty) {
this.build();
}
// If we have all three the props, we can do an exact lookup.
const { type: t, group: g, instance: i } = q;
if (known(t)) {
if (known(i)) {
if (known(g)) {
return this.expand(this.index.findTGI(t, g, i));
}
else {
return this.expand(this.index.findTI(t, i));
}
}
// If we reach this point, the type is known, but the instance is
// for sure *not known*.
let result = this.expand(this.index.findType(t));
if (known(g)) {
return result.filter(createFilter(q));
}
else {
return result;
}
}
// If we reach this point, we can't use an index. Pity.
return this.filter(createFilter(q));
}
// ## expand(pointers)
// Accepts an array of pointers - i.e. indices - that the index has found,
// and then we fill in - we *expand* - the actual entries from our array.
expand(pointers) {
let output = [];
for (let i = 0; i < pointers.length; i++) {
output[i] = this[pointers[i]];
}
return output;
}
remove(query, g, i) {
let removed;
if (typeof query === 'function') {
removed = filterInPlace(this, invert(query));
}
else {
let normalizedQuery = normalize(query, g, i);
if (!normalizedQuery)
return 0;
removed = filterInPlace(this, invert(createFilter(normalizedQuery)));
}
if (this.index) {
this.dirty = true;
}
return removed.length;
}
// ## push(...tgis)
// Adds a new tgi object to the index.
push(...tgis) {
super.push(...tgis);
if (this.index) {
this.dirty = true;
}
return tgis.length;
}
// ## add(tgi)
// Alias for `.push()`.
add(...args) {
this.push(...args);
return this;
}
// ## [Symbol.for('nodejs.util.inspect.custom')]
[Symbol.for('nodejs.util.inspect.custom')](_opts, _level, inspect) {
let c = { colors: true };
return inspect([...this], c) + ` (indexed: ${inspect(!!this.index, c)})`;
}
}
const known = (x) => x !== undefined;
// # normalize(query, g, i)
function normalize(query, g, i) {
let type;
let group;
let instance;
if (typeof query === 'number') {
type = query;
group = g;
instance = i;
}
else if (Array.isArray(query)) {
[type, group, instance] = query;
}
else {
({ type, group, instance } = query);
}
if (type === undefined &&
group === undefined &&
instance === undefined) {
if (process.env.NODE_ENV !== 'test') {
console.warn('You provided an empty TGI query. Please verify if this is intentional! To avoid performance problems, this returns an empty result instead of all entries.');
}
return null;
}
let result = {};
if (type !== undefined)
result.type = type;
if (group !== undefined)
result.group = group;
if (instance !== undefined)
result.instance = instance;
return result;
}
// # createFilter(query)
// Creates a filter function from a { type, group, instance } object some values
// may be undefiend.
function createFilter({ type, group, instance }) {
return (entry) => {
return ((type > -1 ? entry.type === type : true) &&
(group > -1 ? entry.group === group : true) &&
(instance > -1 ? entry.instance === instance : true));
};
}
// # filterInPlace(array, condition)
// The in-place equivalent of Array.prototype.filter
function filterInPlace(array, condition) {
let out = [];
let i = 0, j = 0;
while (i < array.length) {
const val = array[i];
if (condition(val, i, array)) {
array[j++] = val;
}
else {
out.push(val);
}
i++;
}
array.length = j;
return out;
}
// # invert(fn)
// Curries a function to return the boolean inverse. This is used for removing
// entries again, because in that case we have to invert obviously.
function invert(fn) {
return function (...args) {
return !fn(...args);
};
}