UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,083 lines (977 loc) 36.2 kB
/** * Enable logics in adaptor:ex Editor. Listen for events or data changes, switch based on conditions etc. * * logic is a core plugin and will be included with every game * * @requires plugin * @requires events * @requires functions * * @module logic/logic * @copyright Lasse Marburg 2021 * @license MIT */ const plugin = require('../plugin.js') var schema = require('./schema.json') const events = require('events') const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor /** @typedef {import('../../game.js').Game} Game */ /** @typedef {import('../../session.js').Session} Session */ /** @typedef {import('../../types').AdaptorAction} Action */ /** * extend states with switch, listener and event options * * @class Logic * @extends {plugin.Plugin} */ class Logic extends plugin.Plugin { constructor() { super(schema) this.name = "logic" this.core = true this.listener = [] this.event = new events.EventEmitter() this.event.setMaxListeners(50) } /** * install change stream listeners for all custom collections and the session collection * * @param {*} config - game variables, functions, eventemitter etc. * @param {Database} config.game.db - game database connector instance * @returns {Object} current logic plugin schema */ setup(config, game) { super.setup(config, game) this.schema["game"] = this.game.name this.addActionTemplates() this.runtime_collections = [] this.runtime_collections = this.game.setup.collections.slice() this.runtime_collections.push("sessions") for(let collection of this.runtime_collections) { if(!this.game.db.hasOwnProperty(collection)) { log.error(this.name, collection + " not found") break } let stream = this.game.db[collection].changes({ fullDocument: 'updateLookup' }) let change_listener = (change) => { if(change.operationType != 'delete' && change.operationType != 'invalidate') { if(change.operationType == 'update') { this.event.emit("change", {document:change.fullDocument, updated:change.updateDescription.updatedFields, removed:change.updateDescription.removedFields, collection:this.game.db[collection]}) //log.debug(this.name, change.updateDescription.updatedFields, change.updateDescription.removedFields) } else { this.event.emit("change", {document:change.fullDocument, collection:this.game.db[collection]}) } } } stream.on("change", change_listener) } this.schema.definitions.function.enum = Object.keys(this.game.getFunctions()) this.game.status.on("functions_update", async functions => { this.schema.definitions.function.enum = Object.keys(functions) await this.schemaUpdate(this.schema) }) return this.schema } addActionTemplates() { this.addConditionTemplate("onChange") this.addConditionTemplate("switch") this.addTemplate("iterate", (payload, action) => { let body = [] if(payload.repeat) { body.push({text: "Iterate elements repeatedly", next: payload.next}) } else { body.push({text: "Iterate once with each element", next: payload.next}) } if(payload.reverse) { body[0].text += " in reverse order" } return { title:`logic ${action.name}`, subtitle: `For '${payload.variable}' of ${payload.with}`, body:body} }) this.addConditionTemplate("onEvent", { value_name: "message", subtitle_insertion:"`Wait for '${payload.event}' from ${payload.from || 'current session'}`" }) this.addTemplate("function", (payload, action) => { let body = [] let args = "" if(payload.arguments && payload.arguments.length) { args = payload.arguments.toString() } /* if(payload.arguments) { body.push({ text: `Args: ${payload.arguments.toString()}` }) }; */ if(Array.isArray(payload.next)) { for(let i = 0; i < payload.next.length; i++) { body.push({text:`next: ${i} then ${payload.next[i]}`, next:payload.next[i]}) } } return { title:`logic ${action.name}`, subtitle:`Call ${payload.function}(${args})`, body:body} }) } /** * generator function that collects all property names the schema defines in sorted strings. * nested properties are appended to each other using dot notation. * e.g.: "first.second.third" * * @param {Object} obj- * @param {string} [previous=""] * * @returns {Array} - list of property path strings */ * schemaToPath(obj, previous = "") { for (const [key, value] of Object.entries(obj)) { if (value.hasOwnProperty("properties")) { if (previous) yield* this.schemaToPath(value, previous + "." + key) else yield* this.schemaToPath(value, key) } else if (key == "properties") { yield* this.schemaToPath(value, previous); } else if (typeof value == "object" && !Array.isArray(value)){ if (previous) yield previous + "." + key else yield key } } } /** * @type {Action} * * dispatches next based on condition. * * If there is more then one condition matching the upmost condition matches. * * If no condition matches else is dispatched if it was defined * * @returns {Promise<string|undefined>} next state or undefined if none of the conditions matched and no else was defined */ async switch(data, session) { if(data.hasOwnProperty("if")) { for(let if_condition of data["if"]) { let condition = new Condition(if_condition, session, this.game) let match = await condition.match() if(match) { return match.next } } if (data.hasOwnProperty("else")) { if(data.else.hasOwnProperty("next")) { return data.else.next } else { session.log.warn("else is missing a 'next' property") } } else { session.log.debug("no match and no else was defined") } } } /** * @type {Action} * * Get next element of array, collection reference or object (key / value) and store it to new variable */ async iterate(data, session) { let reference = {} let value let with_collection = false if(/^\[\[.*\]\]$/.test(data.with)) { reference = await session.variables.getVariableReference(data.with) if (reference.field) { value = await session.variables.getValue(reference.variable) } else { with_collection = true } } else { value = await session.variables.review(data.with) value = session.variables.parseJSON(value) } if(!with_collection) { let element if(!isNaN(parseInt(value))) { let index = await this.getIndex(data.reverse, data.repeat, session, value) if(index < 0) { return } element = index+1 } else if(Array.isArray(value) || typeof value === "string") { let index = await this.getIndex(data.reverse, data.repeat, session, value.length) if(index < 0) { return } element = value[index] } else if(typeof value === "object") { let entries = Object.entries(value) let index = await this.getIndex(data.reverse, data.repeat, session, entries.length) if(index < 0) { return } element = {key: entries[index][0], value:entries[index][1]} } else { throw new adaptor.InvalidError(`Can not iterate with value ${value} of type ${typeof value}`) } await session.variables.set(data.variable, element) session.log(`Use next element ${JSON.stringify(element)} as "${data.variable}"`) } else { let items = await this.game.db[reference.collection].find(reference.query) items = session.variables.sortReferencedDocuments(items, reference.query) let index = await this.getIndex(data.reverse, data.repeat, session, items.length) if(index < 0) { return } let next_item = items[index] await session.createReference(reference["collection"], {_id:next_item._id}, data["variable"]) session.log(`Use next ${reference.collection} item ${next_item.name || next_item._id} as "${data.variable}"`) } return data.next } /** * Manages an index counter for iterating over a collection of elements. * If the index is higher then the number of elements or lower then 0 then it will return -1 * If repeat is enabled restart with first element if last element was reached in previous iteration * If reverse is enabled iterate in reverse order * If action_data contains ended = true index will be reset. * * @param {boolean} reverse - wether to iterate in reverse order and to step index from highest (max) to lowest (0) * @param {Session} session - session API * @param {boolean} repeat - if enabled restart with first element if last element was reached in previous iteration * @param {number} max_elements - total number of items or elements (e.g. length of array) * @returns {Promise<number>} - current index value. -1 if index is higher then number of elements or lower then 0 */ async getIndex(reverse, repeat, session, max_elements) { let action_data = await session.variables.load() let result = -1 if(!max_elements) { action_data = { total: 0, index: 0, loop: 0 } } else if(!action_data || action_data.ended) { action_data = { total: max_elements, loop: 1 } if(reverse) { action_data["index"] = max_elements -1 } else { action_data["index"] = 0 } result = action_data.index } else if(reverse) { action_data.total = max_elements action_data.index -- if(action_data.index >= 0) { result = action_data.index } else if (repeat) { action_data.loop ++ action_data.index = max_elements -1 result = max_elements -1 } else { action_data.index = 0 } } else { action_data.total = max_elements action_data.index ++ if(action_data.index < max_elements) { result = action_data.index } else if (repeat) { action_data.loop ++ action_data.index = 0 result = 0 } else { action_data.index = max_elements - 1 } } if(result < 0) { action_data.ended = true } else { action_data.ended = false } await session.variables.store(action_data, true) return result } /** * @type {Action} * * Listen for changes of a database value/variable * * returns next state if there is an instant match. * Otherwise database change listener will be installed and cancel function is returned. * */ async onChange(data, session) { let listener = new ChangeListener(data, session, this.event, this.game) let match = await listener.review() if(match) { listener.cancel() return match.next } return listener.cancel.bind(listener) } /** * @type {Action} */ async onEvent(data, session) { let listener = new EventListener(data, session, this.game) await listener.init() return listener.cancel.bind(listener) } /** * @type {Action} */ async dispatchEvent(data, session) { let event_source = session if(data.hasOwnProperty("source")) { event_source = await session.variables.getItem(data.source) } if(!event_source) { throw new adaptor.InvalidError(`Can not dispatch event ${data.name}. ${data.source} is not a valid item`) } if(!event_source.hasOwnProperty("event")) { event_source.event = new events.EventEmitter() } session.log(`dispatch event '${data.name}' from ${event_source.name || data.source} `) data.message = session.variables.parseJSON(data.message) this.dispatch(data["name"], data["message"], event_source.event) } /** * emit game or session event. * * @param {string} event * @param {*} payload * @param {EventEmitter} eventemitter */ dispatch(event, payload, eventemitter) { if(event) { eventemitter.emit(event, payload) } } /** * Call a default or a custom function from the games functions * * @param {*} data * @param {*} session */ async function(data, session) { let functions = this.game.getFunctions() if(typeof functions[data.function] === "function") { session.log("call function " + data.function + " with args " + data['arguments']) if(Array.isArray(data.arguments)) { data.arguments.forEach((arg, i) => { data.arguments[i] = session.variables.parseJSON(arg) }) } else { data.arguments = session.variables.parseJSON(data.arguments) } let result = await functions[data.function](data['arguments'], {session:session, game:this.game}) await session.variables.store(result) if(data.hasOwnProperty("next")) { if(result.hasOwnProperty("next")) { if(data.next[result.next]) { return data.next[result.next] } else { throw new adaptor.InvalidError(`Can not dispatch next from function ${data.function}. No next defined at position ${result.next}`) } } } } else { throw new adaptor.InvalidError(data.function + " is not a valid function") } } } class Condition { /** * check different types of conditions and dispatch next on a match * * @param {Object} properties - Condition properties * @param {*} properties.value - The value to review condition against * @param {string} properties.next - Next state value that will be returned on a matching condition * @param {string} [properties.respond] - Immediate response on match * @param {boolean} properties.case_sensitive - Wether or not to only allow case sensitive matching * @param {import("../../session").Session} session - Session context object * @param {import("../../game").Game} game - Game context object */ constructor(properties, session, game) { this.session = session this.game = game this.value = properties["value"] this.next = properties["next"] this.respond = properties["respond"] this.case_sensitive = properties["case_sensitive"] || false if(properties.hasOwnProperty("equals")) { this.condition = properties.equals this.survey = this.equals this.name = "equals" } else if(properties.hasOwnProperty("lessThan")) { this.condition = properties.lessThan this.survey = this.lessThan this.name = "less than" } else if(properties.hasOwnProperty("greaterThan")) { this.condition = properties.greaterThan this.survey = this.greaterThan this.name = "greater than" } else if(properties.hasOwnProperty("contains")) { this.condition = properties.contains this.survey = this.contains this.name = "contains" } else if(properties.hasOwnProperty("regex")) { this.condition = properties.regex this.survey = this.regex this.name = "regex" } else if(properties.hasOwnProperty("javascript")) { this.condition = properties.javascript this.survey = this.javascript this.name = "javascript" } else if (properties.hasOwnProperty("find")) { this.condition = properties.find this.survey = this.query this.name = "query" } } /** * trigger next state based on match in one of: * - equals * - contains * - regex * - query * * @param {Object} data - properties that resolve that would return match under certain conditions * @param {Object} variables - state context instance of session Variables Object * * @returns {Promise} returns a string defining the next state that condition wants to dispatch or undefined it there is no match */ match() { return new Promise((resolve, reject) => { this.evaluate() .then(result => { if(Array.isArray(result.condition)) { if(result.condition) { log.trace(this.name, "compare condition '" + result.condition.join(',').substring(0, 30) + "'... with value '" + result.value + "'. case sensitive: " + this.case_sensitive) } } else if (typeof result.condition === "string") { log.trace(this.name, "compare condition '" + result.condition.substring(0, 30) + "' with value '" + result.value + "'. case sensitive: " + this.case_sensitive) } else { log.trace(this.name, "compare condition '" + JSON.stringify(result.condition) + "' with value '" + result.value + "'. case sensitive: " + this.case_sensitive) } return this.survey(result.value, result.condition) }) .then(result => { return resolve(this.feedback(result)) }) .catch(error => { return reject(error) }) }) } /** * Get survey result. Maybe next state and or respond or undefined * * Is separated from `match()` because messenger `DialogueCondition` requires a more sophisticated handling. * * @param {Object|string|undefined} result - matching value returned by survey function * @returns {{next:string, respond:string, match:string|Object}|undefined} - next state to dispatch or undefined */ feedback(result) { if(typeof result !== "undefined") { if(this.respond) { log.debug(this.name, `condition matches '${result}'. Next: ${this.next}, Respond: ${this.respond}`) this.session.variables.store({match:result}) return {next: this.next, respond: this.respond, match: result} } if(this.next) { log.debug(this.name, `condition matches '${result}'. Next: ${this.next}`) this.session.variables.store({match:result}) return {next: this.next, match: result} } log.warn(this.name, "no next and no response defined") } return undefined } /** * review value / condition pair to dereference variables if any * Merge arrays in conditions if its an array of arrays (flat) * convert undefined value to 'undefined' string to allow matching 'undefined' variables except for javascript condition * * @returns {Promise} condition and value property with replaced variables if any */ async evaluate() { if(this.name == "query") { this.condition.query = await this.session.variables.review(this.condition.query) } else if(Array.isArray(this.condition)) { let review_conditions = this.condition.map(condition => { return this.session.variables.review(condition) }) this.condition = await Promise.all(review_conditions) this.condition = this.condition.flat() this.condition = this.condition.map(condition => { return this.cast(condition) }) } else { this.condition = await this.session.variables.review(this.condition) this.condition = this.cast(this.condition) } if(this.hasOwnProperty("value")) { let review_value = await this.session.variables.review(this.value) this.value = this.cast(review_value) return {condition:this.condition, value:this.value} } else { return {condition:this.condition} } } /** * cast to string except for javascript condition * * convert undefined to 'undefined' string to allow matching 'undefined' variables * @param {*} data * @returns {*} converted data */ cast(data) { if(typeof data === "string" || this.name == "javascript"){ return data } else if (typeof data === "undefined") { return 'undefined' } else if (typeof data.toString === "function") { return data.toString() } } /** * Convert to upper case if condition is not set to be case sensitive * * @param {string} val - value to upper case if not case sensitive * @returns {string} value or value to upper case */ caseSensitivity(val) { if(!this.case_sensitive) { return val.toUpperCase() } return val } /** * check value against condition * if value matches condition exactly or * if condition is empty string or * if one item in condition array matches value exactly, return value * * else return undefined * * @returns {Promise} the value if it matched the condition or undefined */ equals(value, condition) { return new Promise((resolve, reject) => { if(typeof condition === "string") { if(this.caseSensitivity(value) == this.caseSensitivity(condition) || condition == "") { return resolve(value) } } else if(Array.isArray(condition)) { let upper = condition.map(cond => {return this.caseSensitivity(cond)}) if(upper.includes(this.caseSensitivity(value))) { return resolve(value) } } else { log.warn(this.name, "can not resolve condition because condition type is neither string nor array") } return resolve(undefined) }) } /** * check value against condition * if value is Number and less then condition return value */ lessThan(value, condition) { return new Promise((resolve, reject) => { if(!isNaN(value)) { if(!isNaN(condition)) { if(condition == "") { log.warn(this.name, "empty condition string") return resolve(undefined) } else if(value == "") { log.warn(this.name, "empty value string") return resolve(undefined) } if(Number(value) < Number(condition)) { return resolve(value) } } else if(Array.isArray(condition)) { for(let cond of condition) { if(!isNaN(cond)) { if(condition == "") { log.warn(this.name, "contains empty condition string") } else if(value == "") { log.warn(this.name, "contains empty value string") } else if(Number(value) < Number(cond)) { return resolve(value) } } else { log.warn(this.name, cond + " is not a Number") } } } else { log.warn(this.name, "can not resolve because condition " + condition +" is neither Number nor Array") return resolve(undefined) } } else { log.warn(this.name, "can not resolve because value " + value + " is neither Number nor Array") return resolve(undefined) } return resolve(undefined) }) } /** * check value against condition * if value is Number and greater then condition return value */ greaterThan(value, condition) { return new Promise((resolve, reject) => { if(!isNaN(value)) { if(!isNaN(condition)) { if(condition == "") { log.warn(this.name, "empty condition string") return resolve(undefined) } else if(value == "") { log.warn(this.name, "empty value string") return resolve(undefined) } if(Number(value) > Number(condition)) { return resolve(value) } } else if(Array.isArray(condition)) { for(let cond of condition) { if(!isNaN(cond)) { if(condition == "") { log.warn(this.name, "contains empty condition string") } else if(value == "") { log.warn(this.name, "contains empty value string") } else if(Number(value) > Number(cond)) { return resolve(value) } } else { log.warn(this.name, cond + " is not a Number") } } } else { log.warn(this.name, "can not resolve because condition " + condition +" is neither Number nor Array") return resolve(undefined) } } else { log.warn(this.name, "can not resolve because value " + value + " is neither Number nor Array") return resolve(undefined) } return resolve(undefined) }) } /** * check value against condition * if condition is contained in value or * if condition is empty string or * if one item in condition array is contained in value return condition that matched the value * * else return undefined * * @returns {Promise} condition that matched the value or undefined */ contains(value, condition) { return new Promise((resolve, reject) => { if(typeof condition === "string") { if(this.caseSensitivity(value).indexOf(this.caseSensitivity(condition)) >= 0) { if(condition == '') { return resolve(value) } return resolve(condition) } } else if(Array.isArray(condition)) { for(let cond of condition) { if(this.caseSensitivity(value).indexOf(this.caseSensitivity(cond)) >= 0) { return resolve(cond) } } } else { log.warn(this.name, "can not resolve condition because condition type is neither string nor array") } return resolve(undefined) }) } /** * check value against regex * if regex matches, resolve with first match * if condition is an array, it iterates over all conditions and resolves if/once any of them matches * if case_sensitive is "false" or not defined the "i" flag will be added * regex returns after first match (not g / global) * * @returns {Promise} part of value that was matching the regex. If none did, undefined */ regex(value, condition) { return new Promise((resolve, reject) => { let flags = "i" if(this.case_sensitive) { flags = "" } if(typeof condition === "string") { let res = value.match(new RegExp(condition, flags)) if(res) { return resolve(res[0]) } } else if(Array.isArray(condition)) { for(let cond of condition) { let res = value.match(new RegExp(cond, flags)) if(res) { return resolve(res[0]) } } } else { log.warn(this.name, "can not resolve condition because condition type is neither string nor array") } return resolve(undefined) }) } /** * Call javascript snippet using AsyncFunction constructor https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction * * @param {*} value - first and only argument for js function * @param {*} condition - javascript snippet that will be executed in function body * @returns {*} the return value of the function script (undefined if nothing is returned) */ async javascript(value, condition) { try { var anonymous_function = new AsyncFunction('value','session','game', condition) } catch (error) { throw new adaptor.InvalidError('Failed to compile javascript condition\n' + error.message) } try { var return_val = await anonymous_function(value, this.session, this.game) } catch (error) { throw new adaptor.InvalidError('Failed to execute javascript condition\n' + error.message) } return return_val } /** * if path and value are defined, true is returned if values are equal. * without path and value, true is returned if query returns any document in collection * * @param {String} condition.collection - what collection to search in * @param {Object} condition.query - mongo find query. * @param {String} condition.path - '.' noted path to address distinct property in document returned by query * @param {String} value - if condition contains path, compare value with what is in document at path */ query(value, condition) { return new Promise((resolve, reject) => { if(!this.game.db.hasOwnProperty(condition.collection)) { return reject("Collection " + condition.collection + " not found.") } if(!condition.hasOwnProperty("query")) { return reject("Query property missing.") } let find_query = this.session.variables.parseQuery(condition.query) this.game.db[condition.collection].find(find_query) .then(result => { if(!result) { return resolve(undefined) } log.trace(this.name, "matches if " + JSON.stringify(find_query) + " in collection " + condition.collection + " returns 1 or more Documents") if(result.length > 0) { return resolve(result[0]) } else { return resolve(undefined) } }) .catch(error => { return reject(error) }) }) } } /** * creates listener for changes to custom database collections. * * A match immediately cancels and deactivates (active=false) the listener. * it is not unlikely that changes to a collection are made at practically the same moment, * so there may be unresolved promises in the pipeline even though the event listener was canceled. * Thats why any output after a first match is blocked so there are no multiple next states. * * @param {Object} data - listen action data * @session */ class ChangeListener { constructor(data, session, event, game) { Object.assign(this, data) this.active = true this.session = session this.game = game this.event = event this.rev = (change) => { this.review(change) .then(result => { if(result) { if(this.active) { this.active = false session.next(result.next) } else { session.log("tried to dispatch next state but was already canceled") } } }) .catch(error => { session.log.error(error) }) } this.event.on("change", this.rev) session.log.info(`${session.action.name} installed.`) } /** * run match functions to check the changed collections for listener conditions * * begins with: ignore changes if change results from storing a listener match in the current state * listener does not cancel quick enough to ignore this change so it would otherwise trigger twice * * @todo make a more sophisticated pre check what documents or changes to take into account * this might also make the above store ignore obsolete */ review(change) { return new Promise((resolve, reject) => { if(change) { if(change.collection.name == "sessions") { if(change.hasOwnProperty("updated")) { if(change.updated.hasOwnProperty(this.session.state.name + '.' + this.session.action.name)) { // this.session.log.info("IGNORE UPDATE", change.updated) return resolve(undefined) } } } } if(this.hasOwnProperty("if")) { let condition_promises = [] for(let if_condition of this["if"]) { let condition = new Condition(if_condition, this.session, this.game) condition_promises.push(condition.match()) } Promise.all(condition_promises) .then(result => { for(let match of result) { if(match) { return resolve(match) } } return resolve(undefined) }) .catch(error => { return reject(error) }) } }) } /** * remove local db change event listener. * reset active flag to suppress multiple triggers of this same event. */ cancel() { this.active = false this.event.removeListener("change", this.rev) this.session.log.debug("closed. " + this.event.listenerCount("change") + " listeners left open") } } /** * Listen for events from data items, plugin items, game or session and evaluate conditions regarding the event and it's data load. */ class EventListener { /** * @param {Object} properties - properties as set by level designer * @param {string} properties.event - event name * @param {array} properties.if - list of conditions to check against (@see {@link Condition} class) * @param {Object} properties.else - if event was dispatched but no condition matched * @param {string} properties.else.next - state to dispatch on else * @param {Session} session - session context instance * @param {Game} game - game context instance */ constructor(properties, session, game) { // Allows for adaptor v1 On Event to work if(properties.hasOwnProperty("then")) { properties["if"] = properties.then["if"] properties["next"] = properties.then["next"] } Object.assign(this, properties) /** @type {Session} */ this.session = session /** @type {Game} */ this.game = game } async init() { if(this.hasOwnProperty("from")) { this.event_source = await this.session.variables.getItem(this.from) } else { this.event_source = this.session } if(!this.event_source) { throw new adaptor.NotFoundError(`${this.from} does not resolve a variable Item`) } if(!this.event_source.hasOwnProperty("event")) { this.event_source.event = new events.EventEmitter() } if(this.hasOwnProperty("event")) { this.event = await this.session.variables.review(this.event) this.listenerCallback = this.session.getCallback(this.review.bind(this)) this.event_source.event.on(this.event, this.listenerCallback) this.session.log.info(`Listening on '${this.event}' event from ${this.event_source.name || this.from}`) } else { this.session.log.error("Can not register Event listener. No Event name provided.") } } /** * Check for condition matches with the event payload * * If event listener condition has field property, match condition against this field in payload. * * dispatch next state without condition check wherever 'if' property is missing * * @param {*} payload - event payload message provided with the event. Might be anything (including undefined/null) * * @returns {Promise<boolean>} - true if next state was triggered by else option */ async review(payload) { await this.session.variables.store({message:payload, name:this.event}) if(this.hasOwnProperty("if") && Array.isArray(this['if'])) { for(let if_condition of this["if"]) { if(if_condition["field"]) { if(typeof payload !== 'object') { this.session.log(`'${this.event}' message is not of type object`) continue } if_condition['value'] = adaptor.getPath(payload, if_condition.field) if(typeof if_condition.value === 'undefined') { this.session.log(`'${this.event}' message has no field ${if_condition.field}`) continue } } else { if_condition['value'] = payload } 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 event: " + this.event) } } if (this.hasOwnProperty("next")) { this.session.next(this.next) return true } this.session.log(`no match on event '${this.event}' and no else or next was defined`) } /** * remove game and session event listener * */ cancel() { this.event_source.event.removeListener(this.event, this.listenerCallback) this.session.log(`Stop waiting for '${this.event}' Event. ${this.event_source.event.listenerCount(this.event)} '${this.event}' listeners left open for event source ${this.event_source.name || this.from}`) } } module.exports.Plugin = Logic module.exports.Condition = Condition