adaptorex
Version:
Connect all your live interactive storytelling devices and software
1,243 lines (1,125 loc) • 36.5 kB
JavaScript
/**
* 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