UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

474 lines (397 loc) 12.8 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