UNPKG

sc4

Version:

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

186 lines (185 loc) 5.9 kB
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); }; }