@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
501 lines (384 loc) • 12.5 kB
JavaScript
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;