adaptorex
Version:
Connect all your live interactive storytelling devices and software
560 lines (486 loc) • 13.7 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