UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

501 lines (384 loc) • 12.5 kB
import { assert } from "../../../core/assert.js"; import { noop } from "../../../core/function/noop.js"; import Task from "../../../core/process/task/Task.js"; import { TaskSignal } from "../../../core/process/task/TaskSignal.js"; import { countTask } from "../../../core/process/task/util/countTask.js"; import { DATABASE_SERIALIZATION_IGNORE_PROPERTY } from "./DATABASE_SERIALIZATION_IGNORE_PROPERTY.js"; let id_seed = 0; /** * * @param {{contains:function(string):boolean}} database * @returns {string} */ function generate_id(database) { let retries_left = 10000; let id; do { id = id_seed.toString(16); id_seed++; retries_left--; if (retries_left <= 0) { throw new Error(`Couldn't find a unique ID after maximum number of attempts`); } } while (database.contains(id)) return id; } /** * Base class for managing various kinds of static game data * @template T */ export class StaticKnowledgeDataTable { /** * Elements mapped by their string ID * @type {Object<T>} */ elements = {}; /** * * @type {Map<string, object>} * @protected */ __json = new Map(); /** * Array representation of elements, useful for fast traversal * @type {T[]} * @protected */ __array = []; /** * Allow automatic ID generation when ID is not present * @type {boolean} * @protected */ __automatic_ids = false; /** * * @type {string} * @protected */ __element_type_name = 'element'; reset() { Object.keys(this.elements).forEach(id => { delete this.elements[id]; }) this.__json.clear(); this.__array.splice(0, this.__array.length); } toJSON() { const result = []; this.__json.forEach((value, key) => { result.push(value); }); return result; } /** * Number of elements in the table * @returns {number} */ size() { return Object.keys(this.elements).length; } /** * * @param {string} id * @return {boolean} */ contains(id) { return this.elements[id] !== undefined; } /** * * @param {string} id * @returns {T|null} */ get(id) { assert.isString(id, 'id'); const element = this.elements[id]; if (element === undefined) { return null; } else { return element; } } /** * * @param {String[]} ids * @param {T[]} result * @throws Error if an item can't be found */ getMany(result, ids) { for (let i = 0, l = ids.length; i < l; i++) { const id = ids[i]; const element = this.get(id); if (element === null) { throw new Error(`Failed to get ${this.__element_type_name} '${id}' from the database'`); } result.push(element); } } /** * * @param {function(T)} visitor * @param {*} [thisArg] */ traverse(visitor, thisArg) { for (let id in this.elements) { const element = this.elements[id]; if (element === undefined) { continue; } visitor.call(thisArg, element); } } /** * * @param {function(T):boolean} f * @param {*} [thisArg] * @return {T[]} */ filter(f, thisArg) { const result = []; for (let id in this.elements) { const element = this.elements[id]; if (element === undefined) { continue; } if (f.call(thisArg, element)) { result.push(element); } } return result; } /** * * @return {T[]} */ asArray() { return this.__array.slice(); } /** * * @param {T} item * @returns {boolean} */ add(item) { const id = item.id; if (this.contains(id)) { //already contains such an element return false; } this.elements[id] = item; this.__array.push(item); return true; } /** * * @param {object} json * @returns {T|{id:string}} */ parse(json) { throw new Error(`Abstract method must be overridden`); } /** * * @param {Array} data * @param {ConcurrentExecutor} executor * @returns {Promise} */ load(data, executor) { assert.defined(executor, 'executor'); const n = data.length; const task = countTask(0, n, i => { const datum = data[i]; let id = datum.id; if (typeof id !== "string") { // TODO consider coercing to string for integer numbers if (this.__automatic_ids) { id = generate_id(this); } else { console.error(`datum.id must be a string, instead was '${typeof id}' (= ${id})`); return; } } const flag_ignore = datum[DATABASE_SERIALIZATION_IGNORE_PROPERTY]; if (flag_ignore !== undefined) { if (typeof flag_ignore !== "boolean") { console.warn(`${DATABASE_SERIALIZATION_IGNORE_PROPERTY} flag is present on ${this.__element_type_name} '${id}'. ${DATABASE_SERIALIZATION_IGNORE_PROPERTY} must be a boolean (true/false), instead was '${typeof flag_ignore}'(=${flag_ignore})`); } else if (flag_ignore) { console.warn(`[${this.__element_type_name}:${id}] ${DATABASE_SERIALIZATION_IGNORE_PROPERTY} flag is set to true, skipping`); return; } else { console.warn(`[${this.__element_type_name}:${id}] ${DATABASE_SERIALIZATION_IGNORE_PROPERTY} flag is set to false and has no effect, please remove this field from JSON`); } } let element; try { element = this.parse(datum); } catch (e) { let _id; //attempt to extract item ID try { _id = datum.id; } catch (e) { _id = 'ERROR'; } console.error(`Failed to parse ${this.__element_type_name} (id=${_id})`, e, datum); return; } if (element === undefined) { console.error('.parse produced undefined value for datum:', datum); return; } if (element === null) { console.error('.parse produced null value for datum:', datum); return; } if (this.contains(id)) { console.error(`Duplicate id '${id}'`); return; } this.__json.set(id, datum); if (datum.id !== id) { // ID was generated, write it back into the element element.id = id; } const added = this.add(element); if (!added) { console.error(`Failed to add ${this.__element_type_name} '${id}', most likely this a duplicate key`); } }); const result = Task.promise(task); executor.run(task); return result; } /** * * @param {T} element * @param {object} json * @param {StaticKnowledgeDatabase} database * @param {AssetManager} assetManager * @returns {Promise} */ linkOne(element, json, database, assetManager) { return Promise.resolve(); } /** * * @param {StaticKnowledgeDatabase} database * @param {AssetManager} assetManager * @param {ConcurrentExecutor} executor * @returns {Promise} */ link(database, assetManager, executor) { /** * * @type {T[]} */ const elements = this.asArray(); const errors = []; const promises = []; const task = countTask(0, elements.length, i => { const element = elements[i]; const id = element.id; const json = this.__json.get(id); let promise; try { promise = this.linkOne(element, json, database, assetManager); } catch (e) { const wrap = new Error(`.linkOne(#id=${id}) threw unexpectedly: ${e.message}`); if (e.stack !== undefined) { wrap.stack = e.stack; } //re-throw to fail throw wrap; } if (promise === undefined) { throw new Error('.linkOne expected to produce a Promise, result was undefined instead'); } if (promise === null) { throw new Error('.linkOne expected to produce a Promise, result was null instead'); } if (typeof promise.then !== "function") { throw new Error('.linkOne expected to produce a Promise, but result does not have .then method'); } if (typeof promise.catch !== "function") { throw new Error('.linkOne expected to produce a Promise, but result does not have .catch method'); } promise.catch(reason => { errors.push({ id, cause: reason }); }); promises.push(promise); }); const result = Task.promise(task) .then(() => { return new Promise((resolve, reject) => { Promise.all(promises).then(resolve, () => { reject(errors); }); }); }); executor.run(task); return result; } /** * @protected * @param element * @param {StaticKnowledgeDatabase} database * @param {function(problem:string)} errorConsumer * @returns {boolean} */ validateOne(element, database, errorConsumer) { return true; } /** * @param {StaticKnowledgeDatabase} database * @param {function(problem:string)} errorConsumer * @throws {Error} * @returns {Task} Task succeeds if validation succeeds, and fails if validation fails */ validate(database, errorConsumer = noop) { assert.notNull(database, 'database'); assert.defined(database, 'database'); const elements = this.asArray(); const elementCount = elements.length; let i = 0; let invalidFound = false; const json = this.__json; const cycleFunction = () => { if (i >= elementCount) { if (invalidFound) { return TaskSignal.EndFailure; } else { return TaskSignal.EndSuccess; } } const element = elements[i]; const isValid = this.validateOne(element, database, problem => { const id = element.id; const jsonDatum = json.get(id); errorConsumer(id, problem, jsonDatum); }); if (typeof isValid !== "boolean") { console.error(`validateOne must return a boolean, instead was '${typeof isValid}'`); return TaskSignal.EndFailure; } if (!isValid) { invalidFound = true; } i++; return TaskSignal.Continue; }; return new Task({ name: 'Table Validation', cycleFunction }); } } /** * @readonly * @type {boolean} */ StaticKnowledgeDataTable.prototype.isStaticKnowledgeDataTable = true;