adaptorex
Version:
Connect all your live interactive storytelling devices and software
343 lines (297 loc) • 10.6 kB
JavaScript
/**
* 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