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