UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

343 lines (297 loc) 10.6 kB
/** * Allows to control adaptor:ex basic functionalities. * * control is a core plugin and will be included with every game * * @requires plugin * @requires medley * * @module control/control * @copyright Lasse Marburg 2022 * @license MIT */ const plugin = require("../plugin.js") const schema = require('./schema.json') const { Condition } = require ('../logic/logic.js') /** @typedef {import('../../types').AdaptorAction} Action */ /** * control this or remote sessions * * * @class SessionControl * @extends {plugin.Plugin} */ class SessionControl extends plugin.Plugin { constructor() { super(schema) this.core = true } /** * @param {Object} config * @returns {Object} - schema */ setup(config, game) { super.setup(config, game) this.addTemplate("split", (payload, action) => { const title = `control ${action.name}` const subtitle = `Split up ${payload.length} new paths` const body = [] for(let path of payload) { body.push({text:`${path.name}`, next:path.next}) } return {title, subtitle, body} }) this.addTemplate("join", (payload, action) => { const title = `control ${action.name}` const subtitle = `Join ${payload.length} paths` const body = [] for(let path of payload) { body.push({text: path}) } return {title, subtitle, body} }) this.addTemplate("launch", (payload, action) => { const title = `control ${action.name}` let subtitle = `Launch ${payload.level}` const body = [] if (payload.content) { subtitle += `(${payload.content})` } if(payload.reference) { subtitle += ` as ${payload.reference}` } if (payload.name) { body.push({text: `name: ${payload.name}`}) } if (payload.arguments) { body.push({text: `Level Arguments:`}) for(let arg of payload.arguments) { body.push({text: `${arg.name} = ${arg.value}`}) } } return {title, subtitle, body} }) this.addConditionTemplate("onError", { value_name: "error message", subtitle_insertion:"`Catch ${payload.type} in this ${payload.from || 'state'}`" }) this.log = function(data, session) { session.log[data.level](data.message) } return this.schema } /** * Cue next state * * @param {string} data - state name to cue next * @param {Object} session - session context variables and functions * @returns state name to cue next */ next(data, session) { if(!data || !data.next) { throw new adaptor.NotFoundError(`Next action is missing 'next' value.`) } return data.next } /** * Empty listener action with the purpose to show at what state a manual operation might be necessary. * * @param {string} data - cue text and state name to cue manually * @param {Object} session - session context variables and functions * @returns {function} - empty 'cancel' function */ onCue(data, session) { session.log(`Wait for manual operation to goto '${data.next}' on cue '${data.cue}'`) return () => {} } /** * quit this session * * The function will also be called on game quit which is smething different and will not ave any effect here. * * @param {boolean|undefined} data - undefined/true * @param {Object} session - session context variables and functions */ async quit(data, session) { if(session) { await this.game.deleteSession({"_id":session._id}) } } /** * call session splitPath function to split a new path. Create uid pathname if name is not provided. * @param {Object} data - path action properties next and name * @param {Object} session - session context variables and functions */ split(data, session) { if(Array.isArray(data)) { for(let split of data) { if(split.hasOwnProperty('name')) { session.splitPath(split.next, split.name) } else { session.splitPath(split.next, adaptor.createId()) } } } else { throw new Error('split path requires an array of paths to be split.') } } /** * cancel a specific path * * This action does not have any functionality. Join adds the specified paths to the path list. * This way they get canceled once the state is cued. * * @param {Object} data - join action properties * @param {Array} data.path - list of origin paths * @param {*} session - session context variables and functions */ async join(data, session) { // await session.joinPath(data) // not required } /** * create a new session * * @param {Object} data - launch action properties * @param {string} data.level - level the session will be based on * @param {Object} [data.arguments] - arguments to forward to new session * @param {string} [data.name] - name for new session document * @param {string} [data.reference] - name for reference of new session inside origin session * @param {Object} session - session context variables and functions */ async launch(data, session) { data.level = await session.variables.review(data.level) data.name = await session.variables.review(data.name) data.content = await session.variables.review(data.content) if(!data.level) { throw new adaptor.InvalidError(`Could not launch session. Level is undefined or not a valid level name.`) } let args = {} let arg_promises = [] if(Array.isArray(data["arguments"])) { arg_promises = data["arguments"].map(async arg => {args[arg.name] = await this.resolveArgument(arg.value, session)}) } else if(typeof data["arguments"] === "object") { arg_promises = Object.entries(data["arguments"]).map(async arg => {args[arg[0]] = await this.resolveArgument(`[[${arg[1]}]]`, session)}) // Make level arguments compatible with before v.2.7 } await Promise.all(arg_promises) var new_session = await this.game.createSession({"level":data.level, "arguments":args, "name":data.name, "content":data.content},{login: session.name + " launch session action"}) await session.variables.store({_id:new_session._id, name:new_session.name}) if(data.hasOwnProperty("reference")) { await session.createReference("sessions", {_id:new_session._id}, data.reference) } } async resolveArgument(arg_value, session) { if(/^\[\[.*\]\]$/.test(arg_value)) { let reference = await session.variables.getVariableReference(arg_value) if (reference.field) { let value = await session.variables.getValue(reference.variable) return {value: value} } else { return reference } } else { let value = await session.variables.review(arg_value) value = session.variables.parseJSON(value) return {value: value} } } /** * Cancel one or more sessions based on session name, reference or query * * @type {Action} */ async cancel(data, session) { if(!data.session) { throw new adaptor.InvalidError(`Can not cancel session. The session value is empty.`) } if(typeof data.session === "object" && data.session.hasOwnProperty("_id")) { session.log(`Cancel session ${data.session.name || ''} ${data.session._id}`) data.session = {_id: data.session._id} } else if(typeof data.session === "string" && !/.*[{}:].*/.test(data.session) && !this.game.findSession("name", data.session)) { let session_ref = await session.variables.getReference(data.session) if(session_ref.topic != "session") { throw new adaptor.InvalidError(`Can not cancel session. ${data.session} is not a session. ${data.session} turns out to be a ${session_ref.topic} item.`) } if(session_ref.field) { throw new adaptor.NotFoundError(`Can not cancel session. ${data.session} doesn't resolve an active session.`) } else { session.log(`Cancel session ${JSON.stringify(session_ref.query)}`) data.session = session_ref.query } } else { data.session = session.variables.parseQuery(data.session) session.log(`Cancel session ${JSON.stringify(data.session)}`) } try { await this.game.deleteSession(data.session) } catch (error) { if(error instanceof adaptor.NotFoundError) { session.log.error(error) } else { throw error } } } /** * Listen for error pipeline and GoTo next if condition matches * * @type {Action} */ async onError(data, session) { let listener = new ErrorListener(data, session, this.game) // await listener.init() return listener.cancel.bind(listener) } } class ErrorListener { constructor(properties, session, game) { Object.assign(this, properties) /** @type {Session} */ this.session = session /** @type {Game} */ this.game = game this.rev = async (error) => { try { await this.review(error) } catch (error) { session.log.error("An Error occurred while trying to catch another error.") session.log.error(error) } } this.session.event.on("error", this.rev) this.session.log.info(`Listening for '${this.type}' errors in '${this.from}'`) } async init() { } async review(error) { if(this.from == "state" && error.state.id != this.session.state.id) { return } if(error.name == this.type || error.type == this.type || this.type == "Error") { await this.session.variables.store(error) if(this.hasOwnProperty("if") && Array.isArray(this['if'])) { for(let if_condition of this["if"]) { if_condition['value'] = error.message let condition = new Condition(if_condition, this.session, this.game) let match = await condition.match() if(match) { this.session.next(match.next) return } } } if (this.hasOwnProperty("else")) { if(this.else.next) { this.session.next(this.else.next) return true } else { this.session.log.warn("'next' missing in else to handle error: " + error.message) } } } } cancel() { this.session.event.removeListener("error", this.rev) this.session.log(`Stop waiting for error events. ${this.session.event.listenerCount("error")-1} error listeners left open in session ${this.session.name}`) } } module.exports.Plugin = SessionControl