UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

560 lines (486 loc) 13.7 kB
/** * Enable timing in adaptor:ex Editor. Trigger next based on timed events. * * time is a core plugin and will be included with every game * * @copyright Lasse Marburg 2022 * @license MIT * @module time/time */ const plugin = require("../plugin.js") var schema = require("./schema.json") class Time extends plugin.Plugin { constructor() { super(schema) this.core = true this.name = "time" /** List of running, pending, done and canceled Timer Objects */ this.timers = [] this.daily = setInterval(this.dailyReview.bind(this), 3600000) // 86400000 } setup(config, game) { super.setup(config, game) this.addTemplate("schedule", (payload, action) => { const title = `time ${action.name}` const body = [] let txt = "Wait until" if (payload.dayOfWeek || payload.dayOfMonth) { let day = "" if (payload.dayOfWeek) { let days = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ] if ( typeof payload.dayOfWeek === "string" && isNaN(payload.dayOfWeek) ) { day = payload.dayOfWeek } else { day = days[payload.dayOfWeek] } } else { day = "day " + payload.dayOfMonth } txt = `Wait until ${day}` if (payload.occurrence) { txt = `Wait until ${payload.occurrence}. ${day}` } else if (payload.month && payload.dayOfWeek) { txt = `Wait until 1. ${day}` } txt += ` at ${payload.time} ` if (payload.month) { let months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] let month = "" if (typeof payload.month === "string" && isNaN(payload.month)) { month = payload.month } else { month = months[payload.month - 1] } txt += ` in ${month}.` } else if (!payload.occurrence && payload.dayOfWeek) { txt += ` in current or upcoming week.` } else { txt += ` in current or upcoming month.` } } else if (payload.minuteOfHour) { txt = `Wait until minute ${payload.minuteOfHour} of current or next hour.` } else { txt = `Wait until ${payload.time} o'clock the current or next day.` } body.push({ text: txt, next: payload.next }) return { title: title, body: body } }) return this.schema } /** * Check all timers once a day if they are due in less than 24 hours. * * If so start a js timeout for the respective timer. * * Also delete timers that are canceled or done. */ dailyReview() { for (let i = this.timers.length - 1; i >= 0; --i) { if (this.timers[i].status == "pending") { this.timers[i].review() } else if ( this.timers[i].status == "done" || this.timers[i].status == "canceled" ) { this.timers.splice(i, 1) } } } enqueue(timer) { this.timers.push(timer) timer.review() } async timeout(data, session) { if (typeof data.timeout === "undefined") { throw new adaptor.InvalidError( `Can not start timeout. Timeout value is undefined.` ) } let timer = new Timer(data.next, session) timer.setDateFromNow(data.timeout) timer.setTimeout() this.enqueue(timer) return timer.cancel.bind(timer) } async datetime(data, session) { let timer = new Timer(data.next, session) timer.setDateByDateString(data.datetime) timer.setTimeout() this.enqueue(timer) return timer.cancel.bind(timer) } async schedule(data, session) { let timer = new Timer(data.next, session) timer.date = new Date(session.date) if (data.month) { data.month = timer.nameToNumber( data.month, [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ], 1 ) } // Day Of Week if (data.dayOfWeek) { if (!data.time) { throw new adaptor.InvalidError("Day Of Week requires Time value.") } let base_date = new Date(session.date) data.dayOfWeek = timer.nameToNumber( data.dayOfWeek, ["SU", "MO", "TU", "WE", "TH", "FR", "SA"], 0 ) if ( isNaN(data.dayOfWeek) || Number(data.dayOfWeek) > 6 || Number(data.dayOfWeek) < 0 ) { throw new adaptor.InvalidError( "Day of Week must be number between 0 - 6 and can not be " + data.dayOfWeek ) } if (data.month) { timer.changeMonth(data.month, base_date) if (!data.occurrence) { data.occurrence = 1 } } timer.setDateByWeek(data.dayOfWeek, data.occurrence, base_date) timer.changeTime(data.time) if (session.date > timer.date) { if (data.month) { base_date.setYear(base_date.getFullYear() + 1) } else if (data.occurrence) { base_date.setMonth(base_date.getMonth() + 1) } else { base_date.setDate(base_date.getDate() + 7) } timer.setDateByWeek(data.dayOfWeek, data.occurrence, base_date) } // Day Of Month } else if (data.dayOfMonth) { if (!data.time) { throw new adaptor.InvalidError("Day Of Month requires Time value.") } if (data.dayOfMonth <= 0) { data.dayOfMonth = parseInt(timer.getDaysInMonth()) + parseInt(data.dayOfMonth) if (data.dayOfMonth <= 0) { data.dayOfMonth = 1 } } if (timer.exceedsDaysInMonth(data.dayOfMonth)) { timer.date.setDate(timer.getDaysInMonth()) } else { timer.date.setDate(data.dayOfMonth) } if (data.month) { timer.changeMonth(data.month) } timer.changeTime(data.time) if (session.date > timer.date) { if (data.month) { timer.date.setYear(timer.date.getFullYear() + 1) } else { timer.date.setMonth(timer.date.getMonth() + 1) } } // Minute of Hour } else if (data.minuteOfHour) { timer.date.setMinutes(data.minuteOfHour, 0, 0) if (session.date > timer.date) { timer.date.setHours(timer.date.getHours() + 1) } // Clock Time } else { timer.changeTime(data.time) if (session.date > timer.date) { timer.date.setDate(timer.date.getDate() + 1) } } timer.setTimeout() this.enqueue(timer) return timer.cancel.bind(timer) } command(input) { switch (input[0]) { case "list": for (let i = 0; i < this.timers.length; i++) { let t = this.timers[i] if (t.status == "canceled" || t.status == "done") { continue } log.info( this.name, i + " status " + t.status + " " + t.msToTime(t.timeLeft()) + " until: " + t.session.name + "." + t.next ) } break default: super.command(input) break } } } class Timer { day = 86400000 hour = 3600000 minute = 60000 second = 1000 /** the date and time the timer runs out */ date /** milliseconds until next state is dispatched */ timeout constructor(next, session) { this.next = next this.session = session this.status = "pending" } review() { if (this.timeLeft() <= this.day) { this.run() } } run() { this.status = "running" this.function = setTimeout(this.done.bind(this), this.timeLeft()) } done() { this.status = "done" if (this.hasOwnProperty("next")) { this.session.log.info("Done. Next: " + this.next) this.session.next(this.next) } else { this.session.log.error("No next to dispatch") } } cancel() { if (this.status == "pending" || this.status == "running") { this.session.log( "Cancel timer " + this.msToTime(this.timeLeft()) + " before it was done." ) } this.status = "canceled" clearTimeout(this.function) } setTimeout() { this.timeout = new Date(this.date) - Date.now() let direction = "from now" if (this.timeout < 0) { direction = "ago" } this.session.log.info( `Set Timer to ${new Date(this.date).toLocaleString()} dispatch "${this.next}" ${this.msToTime(Math.abs(this.timeout))} ${direction}.` ) } setDateFromNow(hms) { let timeout = this.hmsToSeconds(hms.toString()) * 1000 if (isNaN(timeout)) { throw new adaptor.InvalidError( `Could not set interval. "${hms}" is not a valid time value.` ) } this.date = new Date() this.date = this.date.setTime(this.session.date.getTime() + timeout) } setDateByDateString(date) { let new_date = new Date(date) if (new_date == "Invalid Date" || adaptor.isInt(date)) { let clock_time = date.toString().split(":") clock_time.push(0, 0, 0) new_date = adaptor.now() new_date.setHours(...clock_time) this.date = new_date } else { this.date = new_date } } /** * * @param {*} day - Day of the week (Sunday = 0, Saturday = 6) * @param {*} occurrence - Occurrence of day in month (i.e. 2nd Saturday of the month) * @param {Date} base_date - Date that is used for all unspecified date fields */ setDateByWeek(day, occurrence, base_date) { this.date = base_date if (occurrence) { let week = parseInt(occurrence) day = parseInt(day) if (occurrence > 0) { let firstWeekday = new Date( this.date.getFullYear(), this.date.getMonth(), 1 ).getDay() if (firstWeekday <= day) { week = week - 1 } let dayOfMonth = week * 7 + day - (firstWeekday - 1) while (this.exceedsDaysInMonth(dayOfMonth)) { dayOfMonth -= 7 } this.date.setDate(dayOfMonth) } else { week = week * -1 let lastWeekday = new Date( this.date.getFullYear(), this.date.getMonth() + 1, 0 ).getDay() if (lastWeekday < day) { week = week + 1 } let dayOfMonth = this.getDaysInMonth() - week * 7 + day - lastWeekday while (dayOfMonth < 1) { dayOfMonth += 7 } this.date.setDate(dayOfMonth) } } else { this.date.setDate(this.date.getDate() + (day - this.date.getDay())) } } changeMonth(month, date = this.date) { if (month < 0) { month = 12 + month } month -= 1 date.setMonth(month) } changeTime(time) { let t = time.split(":") if (t.length) { this.date.setHours(t[0], 0, 0, 0) } if (t.length > 1) { this.date.setMinutes(t[1]) } if (t.length > 2) { this.date.setSeconds(t[2]) } if (t.length > 3) { this.date.setMilliseconds(t[3]) } } /** * Find out if the month date exceeds the actual days in a month * * @param {number} dayOfMonth - Day Of Month * @param {Date} date - A js Date Object * @returns true if the date does not exist in the given date */ exceedsDaysInMonth(dayOfMonth) { let daysInMonth = this.getDaysInMonth() if (dayOfMonth > daysInMonth) { return true } return false } /** * Get Days in Month of date variable * * @returns {number} - Amount of days in the month of this.date */ getDaysInMonth() { return new Date( this.date.getFullYear(), this.date.getMonth() + 1, 0 ).getDate() } timeLeft() { return Math.round(this.date - Date.now()) } secondsLeft() { return Math.round((this.date - Date.now()) / this.second) } timeElapsed() { return new Date(Date.now() - this.session.date) } /** * from https://stackoverflow.com/questions/9640266/convert-hhmmss-string-to-seconds-only-in-javascript/9640417#9640417 */ hmsToSeconds(str) { var p = str.split(":"), s = 0, m = 1 s += m * parseFloat(p.pop()) m *= 60 while (p.length > 0) { s += m * parseInt(p.pop(), 10) m *= 60 } return s } msToTime(duration) { var milliseconds = Math.floor((duration % 1000) / 100), seconds = Math.floor((duration / 1000) % 60), minutes = Math.floor((duration / (1000 * 60)) % 60), hours = Math.floor((duration / (1000 * 60 * 60)) % 24), days = Math.floor(duration / (1000 * 60 * 60 * 24)) days = days == 0 ? "" : days + " days " hours = hours == 0 ? "" : hours + " hours " minutes = minutes == 0 ? "" : minutes + " minutes and " seconds = seconds == 0 ? "" : seconds + "." + milliseconds + " seconds" return days + hours + minutes + seconds } nameToNumber(name, options, startIndex) { if (typeof name === "string" && isNaN(name)) { for (let i = 0; i < options.length; i++) { if (name.toUpperCase().startsWith(options[i])) { return i + startIndex } } } return name } } module.exports.Plugin = Time