UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,243 lines (1,125 loc) 36.5 kB
/** * allows index.js to create Game Object instances * * @requires events * * @requires file * @requires session * @requires topic * @requires errors * * @module game * @copyright Lasse Marburg 2021 * @license MIT */ const events = require("events") const express = require("express") const file = require("./file.js") const { SessionTopic } = require("./session.js") const topic = require("./topic.js") const level_topic = require("./level.js") const { PluginTopic } = require("./plugins/plugin.js") const adaptor_functions = require("./functions.js") const path = require("path") const { FilesManagement } = require("./files_management.js") /** @typedef {import('./session').Session} Session */ /** @typedef {import('./session').SessionTopic} SessionTopic */ /** @typedef {import('./topic').Topic} Topic */ /** @typedef {import('./plugins/plugin').Plugin} Plugin */ /** @typedef {import('./mongo').Database} Database */ /** objects that define what plugins and collections are part of which game template. All other templates inherit the default plugins and collections */ const templates = { default: { plugins: ["logic", "data", "time", "control"], collections: ["plugins", "levels", "sessions", "listeners", "setup"] }, basic: { plugins: [], collections: ["players"] }, installation: { plugins: [], collections: ["players"] }, multiplayer: { plugins: [], collections: ["players"] }, "open-world": { plugins: [], collections: ["players", "places"] } } /** * @namespace socket.io * @import {Namespace} from "socket.io" */ /** * Main Framework class that inherits sessions and plugins and serves editor webpage * * host global (inter session game wide) events * * @class */ class Game { /** * @param {Object} preferences - config the game is based on * @param {Database} preferences.db - database connection to game database * @param {express.Router} app - Express app router. Routes to /{game} * @param {express.Router} api - Express app router for API requests. Routes to /api/game/{game} * @param {Object} client - io socket Namespace Connections */ constructor(preferences, app, api, client) { Object.assign(this, preferences) /** @type {express.Router} */ this.app = app /** @type {express.Router} @private */ this.api = api /** dictionary of game topics * @type {Object<string, Topic>} */ this.topics = {} /** counts how many sessions were created since game started to allow individual session names * @type {number} @private */ this.session_count = 0 /** dictionary of enabled plugins * @type {PluginTopic} */ this.topics.plugin /** dictionary of available custom and adaptor functions * @type {Object<string, Function} */ this.functions = {} /** event emitter for relevant game incidents or changes * @type {events.EventEmitter} */ this.status = new events.EventEmitter() /** dictionary of DB change stream listeners. The stream listener for each collection * is stored under its respective name. * @type {Object} */ this.changes = {} /** Indicates if game is done loading * @type boolean */ this.ready = false /** setup information about this game collections and plugins * @property {Array<string>} collections - List of collection names that where added to the game * @property {Array<string>} plugins - List of plugin names that where added to the game */ this.setup = { collections: [], plugins: [] } /** eventemitter instance is forwarded to plugins on creation to interact with other sessions * @type {events.EventEmitter} */ this.event = new events.EventEmitter() /** * @namespace socket.io * @import {Namespace} from "socket.io" */ /** socket namespace connection to editor * @type {Namespace} * @private */ this.client = client this.client.on("connect", (socket) => { log.trace( this.name, `Editor socket connected. User: ${socket.request.user.login}, id: ${socket.id}` ) socket.on("disconnect", () => { log.trace( this.name, `Editor socket disconnected. User: ${socket.request.user.login}, id: ${socket.id}` ) }) }) /** socket namespace connection to updates on plugin and data/collection items * @type {Namespace} * @private */ this.items_client = adaptor.io.of(`/game/${this.name}/live/items`) this.items_client.use(adaptor.auth.authenticateSocket.bind(adaptor.auth)) file.watchDir(this.files, async (e, file) => { this.client.emit("media", this.getFiles()) this.client.emit("files", await this.files_management.getFiles({})) }) file.watchDir(this.public, async (e, file) => { this.client.emit("files", await this.files_management.getFiles({})) }) this.loadFunctions() } /** * Get game properties and functions may be accessed from outside the game class * * @returns {Game} - public game properties and functions */ getAPI() { return { name: this.name, event: this.event, changes: this.changes, status: this.status, data: this.data, files: this.files, public: this.public, url: this.url, files_url: this.files_url, setup: this.setup, topics: this.topics, db: this.db, app: this.app, ready: () => this.ready, addTopic: (topic, name, route) => this.addTopic(topic, name, route), getFilePath: (file) => this.getFilePath(file), getFileURL: (file) => this.getFileURL(file), getFunctions: () => this.getFunctions(), addDataCollection: (name) => this.addDataCollection(name), addPluginCollection: (name, plugin) => this.addPluginCollection(name, plugin), // Is this deprecated? listPluginCollections: () => this.topics.plugin.listCollections(), cancelSession: (query) => this.topics.session.cancelSession(query), deleteSession: (query) => this.topics.session.deleteSession(query), createSession: (args, user) => this.topics.session.createSession(args, user), getSession: (id) => this.topics.session.getSession(id), findSession: (key, property) => this.topics.session.findSession(key, property), getWebhookURLs: (append_path) => this.getWebhookURLs(append_path), findPlugin: (key, value) => this.findPlugin(key, value), getPlugin: (name) => this.getPlugin(name) } } /** * @returns {Object} game setup Object */ async getSetup() { let setup = await this.db.setup.find({}, { project: { _id: 0 } }) return setup[0] } /** * modify game setup properties * adapts local setup object to changes * * @param {Object} data - setup data to be updated * @returns {Promise<Object>} changed true if game setup has changed */ async editSetup(data) { let updateResponse = await this.setup_doc.set(data) this.setup = await this.setup_doc.get() if (updateResponse.matchedCount == 0) { throw new Error(this.name + " is missing a setup document") } else if (updateResponse.modifiedCount == 0) { return { changed: false } } return { changed: true } } /** * create a new (empty) default Game * - assign database * - create collections based on template. Collections in default template are system collections and do not create a default API topic. * - add plugins based on template * - store setup * * Templates might be a deprecated feature. There is no interesting difference between templates. Current web client constellation will always create new games with "basic" template * * @param {Database} database - refers to database with this.name * @param {string} template - template name, the game build is based on */ build(database, template) { return new Promise((resolve, reject) => { this.db = database let all_collections = [] let all_plugins = [] if (templates.hasOwnProperty(template)) { this.setup = templates[template] this.setup["created_at"] = adaptor.now() this.setup["created_with"] = adaptor.info.version all_collections.push(...templates.default.collections) all_collections.push(...templates[template].collections) all_plugins.push(...templates.default.plugins) all_plugins.push(...templates[template].plugins) } else { throw new Error(template + " is not a valid game template") } let create_collections = all_collections.map((coll, index) => { return this.db.addCollection(coll) }) Promise.all(create_collections) .then((result) => { return this.db.setup.insert(this.setup) }) .then((result) => { return this.db.setup.document({}) }) .then((result) => { this.setup_doc = result return this.createTopics() }) .then((result) => { let add_plugins = all_plugins.map((plugin, index) => { return this.topics.plugin.create( { name: plugin }, { user: { login: adaptor.userInfo().username } } ) }) return Promise.all(add_plugins) }) .then((result) => { this.status.emit("plugins_loaded") file.mkdir(this.files + "/examples") file.cp( path.join(__dirname, "/examples/files"), this.files + "/examples" ) log.info(this.name, "game created") this.ready = true return resolve({ status: "built", issues: [], errors: [] }) }) .catch((error) => { return reject(error) }) }) } /** * resume game, based on existing data on database * - enable plugins stored in plugins collection * - load plugins that were added previously * - add local session objects list based on session Collection entries * - create API topics * * @param {Database} database - refers to database with this.name */ async resume(database) { this.db = database log.info(this.name, "Reload plugins and sessions ...") let issues = [] let errors = [] for (let coll of templates.default.collections) { if (!this.db.hasOwnProperty(coll)) { log.error( "Game data is incomplete: " + coll + " collection missing. Create new." ) this.db.addCollection(coll) } } this.setup_doc = await this.db.setup.document({}) this.setup = await this.setup_doc.get() await this.createTopics() try { let load_plugins = await this.topics.plugin.reload() if (load_plugins.issues && load_plugins.issues.length) { issues = issues.concat(load_plugins.issues) } } catch (error) { log.error(this.name, `Unexpected error while trying to load plugins`) log.error(this.name, error) errors.push( new Error(`Unexpected error while trying to load plugins`, { cause: error }) ) } this.status.emit("plugins_loaded") try { await this.topics.session.reload() } catch (error) { log.error(this.name, `Unexpected error while trying to add session`) log.error(this.name, error) errors.push( new Error(`Unexpected error while trying to add session`, { cause: error }) ) } this.ready = true return { status: "reloaded", issues: issues, errors: errors } } /** * * @param {Topic} topic - The topic instance * @param {*} name - The name the topic will be available via Game Interface * @param {*} route - The API route fro the topic */ addTopic(topic, name, route) { this.api.use(route, topic.router) this.topics[name] = topic } /** * establish API to Game and DB connection for * - level * - plugins * - sessions * - data collections */ async createTopics() { const openapi = adaptor.openapi let topic_setups = [] this.files_management = new FilesManagement(this.getAPI()) let level = new level_topic.Level(openapi, { name: this.name, getCollectionSchema: (collection) => this.getCollectionSchema(collection), getActionVariables: () => this.topics.plugin.getActionVariables(), getFunctions: this.getFunctions.bind(this), getAllPluginSetups: async () => { var plugins = await this.db.plugins.find({}) return await this.topics.plugin.getSetups(plugins) }, cancelSession: (query) => this.topics.session.cancelSession(query), deleteSession: (query) => this.topics.session.deleteSession(query), db: this.db, changes: this.changes, client: this.client }) await level.setup() let state = new level_topic.State(openapi, this.getAPI(), level) topic_setups.push(state.setup()) file.watchDir(this.data + "/functions", (e, file) => { log.debug(this.name, `Change in functions directory: ${e} ${file}`) this.loadFunctions() level.updateVariables() }) let session = new SessionTopic(this.getAPI(), openapi) topic_setups.push(session.setup()) let plugin = new PluginTopic( this.getAPI(), this.client, this.items_client, openapi ) topic_setups.push(plugin.setup()) let collection = new topic.Topic( { name: "collection", coll_name: undefined, schema: { query: {}, path: openapi.components.parameters.collection.schema, body: openapi.components.schemas.collection }, main_properties: [], methods: ["POST", "DELETE"] }, this.getAPI() ) topic_setups.push(collection.setup()) let items = new topic.Topic( { name: "items", coll_name: undefined, schema: { query: {}, path: openapi.components.parameters.collection.schema, body: openapi.components.schemas.item }, main_properties: [], cast_query_types_default: true, methods: ["GET"] }, this.getAPI() ) topic_setups.push(items.setup()) collection.create = async (data) => { return this.addDataCollection(data["name"], true) } collection.delete = async (name) => { return this.removeCollection(name) } this.items_client.on("connect", (socket) => { log.trace(this.name, "items update socket connected " + socket.id) }) /** * Items endpoint will search through all plugin and data collections. * * Adds context information to each item document. * * Sort and limit options are prepended to the database find query. * * @param {Object} query - mongo style find query * @param {*} options - game API GET options like sort, project, limit or skip * @returns {Array<object>} list of documents that match query */ items.getMany = async (query, options) => { let return_items = [] let selection = {} selection.type = query.type selection.collection = query.collection selection.plugin = query.plugin delete query.type delete query.collection if (!selection.type || selection.type == "data") { for (let data_collection of this.setup.collections) { if ( !selection.collection || selection.collection == data_collection ) { let coll_items = await this.topics[data_collection].getMany(query) coll_items.forEach((item) => { item.collection = data_collection item.type = "data" }) return_items = return_items.concat(coll_items) } } } delete query.plugin if (!selection.type || selection.type == "plugin" || selection.plugin) { for (let coll of this.topics.plugin.listCollections()) { let plugin = coll.plugin if ( (!selection.collection && !selection.plugin) || selection.collection == coll.name || selection.plugin == plugin ) { let coll_items = await this.topics[coll.collection].getMany(query) coll_items.forEach((item) => { item.collection = coll.name item.type = "plugin" item.plugin = plugin }) return_items = return_items.concat(coll_items) } } } if (options.sort) { let sortby = Object.keys(options.sort)[0] let direction = Object.values(options.sort)[0] return_items.sort((a, b) => a[sortby] > b[sortby] ? direction : -direction ) } if (options.limit || options.skip) { if (options.limit == 0) { options.limit = undefined } return_items = return_items.slice( options.skip || 0, options.limit + (options.skip || 0) ) } return return_items } this.api.use("/files", this.files_management.router) this.api.use("/level", level.router) this.api.use("/level/:level/state", state.router) this.api.use("/session", session.router) this.api.use("/plugin", plugin.router) this.api.use("/collection", collection.router) this.api.use("/items", items.router) Object.assign(this.topics, { level: level, plugin: plugin, session: session, collection: collection, items: items }) for (let coll of this.setup.collections) { this.addDataTopic(coll) topic_setups.push(this.topics[coll].setup()) } await Promise.all(topic_setups) log.debug( this.name, `${Object.keys(this.topics).length} topics added: ${Object.keys(this.topics)}` ) } /** * close topic and delete topic object from game * * @param {string} topic - topic name */ removeTopic(topic) { this.topics[topic].close() delete this.topics[topic] } /** * Get a summary of the basic game elements in this game. * * @returns {Object} level, plugins, sessions and collections that are part of this game */ async getElements() { let setup = await this.getSetup() let level = await this.topics.level.preview() let plugins = await this.topics.plugin.preview() let data_collections = setup.collections let sessions = await this.topics.session.preview() let actions = await this.topics.plugin.getActionsPreview({}) return { name: this.name, setup: setup, level: level, plugins: plugins, actions: actions, collections: data_collections, sessions: sessions } } /** * Get a list off all known URLs through which you can access a webhook that is part of this game. * * @param {string} append_path - path to be appended to each known webhook URL (please prepend '/') * @returns {array<{name:string,url:string}>} - List of known webhook URLS for the appended path to the game url */ getWebhookURLs(append_path) { return adaptor.webhooks.map((webhook) => { let extended_webhook = Object.assign({}, webhook) extended_webhook.url = `${webhook.url}/${this.name}${append_path}` return extended_webhook }) } async getActions(query, keep_references) { return this.topics.plugin.getActions(query, keep_references) } /** * Get a complete level schema for levels in this game. * Each of this games level must validate against the resulting schema. * Action schemas from active plugins are merged into level actions schema. * * @returns level schema as of this moment. Depends on plugins and their settings */ async getLevelSchema() { let level_schema = adaptor.deepClone( adaptor.openapi.components.schemas.level ) level_schema.properties.actions.additionalProperties = { oneOf: [] } let actions = await this.getActions({}) for (let action of actions) { level_schema.properties.actions.additionalProperties.oneOf.push( action.schema ) } return level_schema } /** * writes current game data to file * creates folder for game and for each collection. * * @param {string} [path='./export'] - folder to export game files into. Will be created if it doesnt exist. * @param {array} [exp_coll] - what collections to export. ALl collections are exported if not provided */ export(path, exp_coll) { return new Promise((resolve, reject) => { if (!path) { path = "./export" } if (!exp_coll) { this.db.listCollections().then((result) => { this.writeCollections(path, result) }) } else { this.writeCollections(path, exp_coll) } }) } /** * follows export function * * @param {string} path - folder to export game to * @param {array} collections - list of collection names */ writeCollections(path, collections) { let get_collections = collections.map((coll, index) => { if (this.db.hasOwnProperty(coll)) { return this.db[coll].find({}) } else { throw new Error(this.name + " has no collection with name " + coll) } }) Promise.all(get_collections, path) .then((res) => { file.mkdir(path) file.mkdir(path + "/" + this.name) for (let i = 0; i < res.length; i++) { file.mkdir(path + "/" + this.name + "/" + collections[i]) for (let doc of res[i]) { file.saveJSON( doc, path + "/" + this.name + "/" + collections[i] + "/" + doc.name + ".json" ) } } }) .catch((err) => { log.error(this.name, err) }) } /** * get the correct file path for a file or path that is inside the game files * Will return unchanged if file is an absolute path or if it is a web url (has leading 'http://' or 'https://') * * @param {string} file - the file name or path relative to game files directory * @returns {string} file path relative to game files path */ getFilePath(file) { if (file.startsWith("http://") || file.startsWith("https://")) { return file } if (path.isAbsolute(file)) { return file } return path.join(this.files, file) } /** * get the correct URL for a file or path that is inside the game files * Will return unchanged if file is a web url (has leading 'http://' or 'https://') * * Will throw InvalidError if file is an absolute path * * @param {string} file - the file name or path relative to game files directory * @returns {string} file URL based on adaptor network settings */ getFileURL(file) { if (file.startsWith("http://") || file.startsWith("https://")) { return file } if (path.isAbsolute(file)) { throw new adaptor.InvalidError( `Can not get file URL with absolute path ${file}. File needs to be in ${this.files} directory.` ) } return new URL(file, this.files_url + "/") } /** * get a list of all files in the game files directory * * @returns {Promise<array>} - list of files with path relative to game directory */ getFiles() { return this.getPlugin("data").collectFiles(this.files) } /** * Get plugin by plugin name * @param {string} name * @returns {Plugin} */ getPlugin(name) { return this.topics.plugin.plugins[name] } findPlugin(key, value) { return this.topics.plugin.find(key, value) } loadFunctions() { let functions_path = path.join(this.data, "functions") let directories = file.ls(functions_path, "directories") let files = file.ls(functions_path) for (let dir of directories) { if (dir != "node_modules") { let nested_files = file.ls(path.join(functions_path, dir)).map((f) => { return path.join(dir, f) }) files = files.concat(nested_files) } } files = files.filter((file) => { if (file.endsWith(".js")) { return true } }) this.functions = Object.assign({}, adaptor_functions) for (let file of files) { let filename = path.resolve(this.data, "functions", file) delete require.cache[require.resolve(filename)] try { const functions_module = require(filename) Object.assign(this.functions, functions_module) } catch (error) { log.error( this.name, "Failed to load user generated functions file " + filename ) log.error(this.name, error) delete require.cache[require.resolve(filename)] } } this.status.emit("functions_update", this.functions) } /** * Get all adaptor functions. * * * @returns {Object<function>} - all the games functions from game data */ getFunctions() { return this.functions } /** * Creates a document schema for a data collection from specifications in plugins * and from existing documents. * * @param {string} collection - name of data collection * @returns {Promise<object>} JSON Schema for documents in the collection */ async getCollectionSchema(collection) { let properties = { name: { type: "string" } } let data_variables = await this.db.plugins.distinct( "schema.data_variables." + collection, {} ) for (let variable of data_variables) { Object.assign(properties, variable) } if (this.db[collection]) { let documents = await this.db[collection].find({}) for (let document of documents) { for (let prop in document) { let type = this.getPropertyType(document[prop]) if ( properties[prop] && typeof properties[prop].type === "string" && type != properties[prop].type ) { properties[prop].type = [properties[prop].type, type] } else if ( properties[prop] && Array.isArray(properties[prop].type) && !properties[prop].type.includes(type) ) { properties[prop].type.push(type) } else if (!properties[prop]) { properties[prop] = { type: type } } if ( [ "_id", "created_at", "created_by", "created_with", "modified_at", "modified_by", "modified_with", "sessions" ].includes(prop) ) { properties[prop].readonly = true properties[prop].options = { hidden: true } } } } } else { log.warn( this.name, "Problem when creating collection schema for '" + collection + "'. No such collection in Database" ) } return { type: "object", title: " ", options: { disable_edit_json: true }, properties: properties } } /** * Get data type differentiating between object and array * * @param {*} value - value of any type * @returns {string} data type of value: object, number, string, boolean, array or undefined */ getPropertyType(value) { if (Array.isArray(value)) { return "array" } else { return typeof value } } /** * add new data collection to database and store it in setup * * add new topic to enable remote access to the collection * * @param {string} coll_name - data collection name * @param {boolean} error_on_duplicate - Throw an error if collection already exists and was already added to game * * @returns {Promise<Object>} db_collection_created true if there wasn't already a collection with coll_name in Database */ async addDataCollection(coll_name, error_on_duplicate) { if (templates.default.collections.includes(coll_name)) { throw new adaptor.ForbiddenError( `name '${coll_name}' is reserved and can not be used for data collections` ) } let db_coll_created = false if (!this.db.hasOwnProperty(coll_name)) { await this.db.addCollection(coll_name) db_coll_created = true } if (!this.topics[coll_name]) { this.addDataTopic(coll_name) await this.topics[coll_name].setup() } else { if (!db_coll_created && error_on_duplicate) { throw new adaptor.DuplicateError( `collection ${coll_name} already exists` ) } } if (!this.setup.collections.includes(coll_name)) { await this.setup_doc.push({ collections: coll_name }) this.setup.collections.push(coll_name) log.info(this.name, "collection added " + coll_name) this.status.emit("collection_added", { name: coll_name, collection: coll_name, db_collection_exists: db_coll_created, type: "data" }) } return { created_id: coll_name, db_collection_created: db_coll_created } } async getDataCollectionSchemas() { let collection_schemas = {} for (let collection of this.setup.collections) { collection_schemas[collection] = await this.getCollectionSchema(collection) } return collection_schemas } /** * Create a new topic to enable access to the database via API * * @param {string} coll_name */ addDataTopic(coll_name) { this.topics[coll_name] = new topic.Topic( { name: coll_name, coll_name: coll_name, data_type: "data", client: this.items_client, changes: true, schema: { query: {}, path: { type: "string" }, body: {} }, main_properties: [], cast_query_types_default: true, methods: ["POST", "GET", "PUT", "EDIT", "DELETE"], has_archive: true }, this.getAPI() ) this.api.use(`/collection/${coll_name}`, this.topics[coll_name].router) } /** * close collection API topic * remove data collection from game database * remove collection from setup * * @param {string} coll_name - data collection name * * @returns {Promise} "collection removed" if removed. If collection didn't exist returns undefined */ removeCollection(coll_name) { return new Promise((resolve, reject) => { if (this.setup.collections.includes(coll_name)) { this.removeTopic(coll_name) this.db .dropCollection(coll_name) .then((result) => { this.setup.collections = this.setup.collections.filter((coll) => { return coll != coll_name }) return this.setup_doc.remove({ collections: coll_name }) }) .then((result) => { log.info(this.name, "Data collection removed: " + coll_name) this.status.emit("collection_removed", coll_name) return resolve("collection removed") }) .catch((error) => { log.error(this.name, error) return reject(error) }) } else { log.warn( this.name, "Can not remove " + coll_name + ". No such custom collection" ) return resolve() } }) } /** * react on std input addressing this game * */ command(input) { switch (input[0]) { case "plugins": case "plugin": if (input[1] == "add") { this.topics.plugin .create( { name: input[2] }, { user: { login: adaptor.userInfo().username } } ) .then((result) => { log.info(this.name, "Plugin added: " + input[2]) }) .catch((err) => { switch (err.name) { case "Error": log.error(this.name, err.message) break default: log.error(this.name, err) } }) } else if (input[1] == "remove") { Input.confirmation( "remove", input[2], this.topics.plugin.delete.bind(this.topics.plugin) ) } else if (input[1] == "settings") { for (let plugin in this.topics.plugin.plugins) { log.info( 0, plugin + ", " + JSON.stringify(this.getPlugin(plugin).settings) ) } } else if (input[1] == "list") { this.topics.plugin.showStatus().catch((err) => { log.error(this.name, err) }) } else { log.info( 0, `Please use one of these commands: |cmd |description |---------------------|-------------- |list |list all plugins |add [plugin] |activate plugin |remove [plugin] |deactivate plugin and delete all items` ) } break case "collection": if (input.length > 1) { switch (input[1]) { case "list": for (let coll of this.setup.collections) { log.info(0, coll) } break case "create": this.addDataCollection(input[2], true) break case "delete": Input.confirmation( "remove", input[2], this.removeCollection.bind(this) ) break default: log.warn(this.name, "unknown collection command " + input[1]) } } else { log.info(this.name, "wrong number of arguments") } break case "import": if (this.db.hasOwnProperty(input[1])) { this.db[input[1]].import(input[2]) } else { log.error(this.name, input[1] + " is not a database collection") } break case "export": if (input.length == 1) { this.export() } else if (input.length == 2) { this.export(input[1]) } else { this.export(input[1], input.slice(2)) } break default: if (this.getPlugin(input[0])) { let plugin_name = input.shift() let plugin = this.getPlugin(plugin_name) if (typeof plugin.command === "function") { plugin.command(input) } else { log.info( this.name, plugin_name + " doesn't take std input commands" ) } } else if (this.topics.hasOwnProperty(input[0])) { this.topics[input.shift()].command(input) } else if (input[0] == "levels") { input.shift() this.topics["level"].command(input) } else if (input[0] == "sessions") { input.shift() this.topics["session"].command(input) } else { log.info( this.name, "No such plugin, topic or command '" + input[0] + "'" ) } break } } /** * cancel all listeners * call quit function on all plugins that have one. * call close function on all topics * * @returns {Promise} - resolves once all plugins and topics quit */ quit() { return new Promise((resolve, reject) => { this.status.removeAllListeners() this.event.removeAllListeners() for (let change_listener in this.changes) { this.changes[change_listener].removeAllListeners() } let promises = [] for (let t in this.topics) { promises.push(this.topics[t].close()) } Promise.all(promises) .then((result) => { return resolve() }) .catch((err) => { return reject(err) }) }) } /** * delete game database * * @todo maybe make backup */ async delete() { await this.db.drop() } /** * quit game * @todo feedback to client that all game windows should be closed or notified */ async unload() { //this.client.emit() //this.editor.disconnect() return this.quit() } } /** Game Object */ module.exports.Game = Game