import chalk from "chalk"; import { Serializable } from "frigid"; import { getTheme } from "@themes"; type AbbreviatedMonthName = string; const daysInMonth = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; const months: AbbreviatedMonthName[] = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] // TODO split ticker util and calendar util... export default class Time extends Serializable { targetTPS: number; paused = true; thing: Tickable; year: number; month: number; day: number; hour: number; minute: number; ticksInSecond: number; lastTPSCheckpoint: number; tps: number; _boundTick: Function; constructor(timestamp: number = 0) { super(); this.minute = timestamp; this.normalize(); } asAge() { if(this.year > 1) { return this.year + ' years old'; } else { if(this.month > 2) { return this.month + ' months old'; } else { if(this.day > 1) { return this.day + ' days old'; } else if(this.day === 1) { return '1 day old'; } else { return 'newborn'; } } } } render() { const sym = (this.hour >= 6 && this.hour < 20) ? chalk.ansi256(226).bgAnsi256(27)(' ☼ ') : chalk.ansi256(254).bgAnsi256(17)(' ☾ ') return `${sym} ${ getTheme().normal(`${ this.toString() }`) }`; } toString() { return `${ this.hour }:${ Math.floor(this.minute).toString().padStart(2, '0') } ${ months[this.month] } ${ this.day + 1 }, ${ this.normalizedYear } [${ this.tps } / ${ this.targetTPS }]`; } ctor() { this.targetTPS = 60; this.minute ??= 0; this.hour ??= 0; this.day ??= 0; this.month ??= 0; this.year ??= 0; this.tps ??= 0; this.lastTPSCheckpoint = ms4(); this.ticksInSecond = 0; this._boundTick = this.doTick.bind(this); } get second() { return Math.floor((this.minute % 1) * 60) } get stamp() { let minute = this.minute; let hour = this.hour; let day = this.day; let month = this.month; let year = this.year; const daysInYear = daysInMonth.reduce((a, b) => a+b, 0); day += daysInYear * year; year = 0; while(month > 0) { day += daysInMonth[month]; month --; } hour += day * 24; day = 0; minute += hour * 60; hour = 0; return minute; } pause() { this.paused = true; } resume() { this.paused = false; setTimeout(this.doTick.bind(this), 0); } start(tickable: Tickable) { this.thing = tickable; this.paused = false; setTimeout(this.doTick.bind(this), 0); } advanceTime(seconds: number) { this.minute += seconds / 60; this.normalize() } get normalizedYear() { if(this.year >= 0) { return (this.year + 1).toString().padStart(4, '0') + ' CE'; } else { return Math.abs(this.year).toString().padStart(4, '0') + ' BCE'; } } normalize() { // while(t) while(this.minute >= 60) { this.minute -= 60; this.hour ++; } while(this.minute < 0) { this.minute += 60; this.hour --; } while(this.hour >= 24) { this.hour -= 24; this.day ++; } while(this.hour < 0) { this.hour += 24; this.day --; } while(this.day < 0) { this.day += daysInMonth[ ((this.month % months.length) + months.length) % months.length ]; this.month --; } while(this.day >= daysInMonth[this.month % months.length]) { this.day -= daysInMonth[this.month % months.length]; this.month ++; } while(this.month >= 12) { this.month -= 12; this.year ++; } while(this.month < 0) { this.month += 12; this.year --; } } async execTick(seconds: number) { const doDynamicTick = true; if(doDynamicTick) { if(this.thing) { await this.thing.tick(seconds); } this.advanceTime(seconds); } else { for(let i = 0; i < seconds; i ++) { if(this.thing) { await this.thing.tick(1); } this.advanceTime(1); } } } async doTick() { // the higher the multitick, the more lopsided // ticks become in realtime, however, they rely // on fewer setTimeouts, which helps tick scheduling const multitick = 1; if(multitick > 1) { const timeout = (1000 / this.targetTPS) * multitick; const start = ms4(); for(let tickNum = 0; tickNum < multitick; tickNum ++ ) { const seconds = 3; this.execTick(seconds); this.ticksInSecond ++; const end = ms4(); if(end > this.lastTPSCheckpoint + 1000) { this.lastTPSCheckpoint = end; this.tps = this.ticksInSecond; this.ticksInSecond = 0; } } const end = ms4() const elapsed = end - start; const wait = timeout - elapsed; const normalizedWait = Math.floor(Math.max(wait, 0)); // process.stdout.write(`tick took ${elapsed} waiting ${normalizedWait}\n`); if(wait < 0) { const ticksOver = (-wait / timeout) + 1; if(ticksOver > 1.5) console.log(chalk.yellow('Can\'t keep up! Tick took ' + ticksOver.toFixed(2) + ' ticks (' + (timeout - wait).toFixed(4) + 'ms)')); } if(this.paused) return; setTimeout(this._boundTick, normalizedWait); } else { // this code is from not needing a multi-tick workaround, due to scheduling. const seconds = 3; const timeout = 1000 / this.targetTPS; // const start = ms4() const start = ms4(); this.execTick(seconds); const end = ms4() const elapsed = end - start; const wait = timeout - elapsed; const normalizedWait = Math.floor(Math.max(wait, 0)); // process.stdout.write(`tick took ${elapsed} waiting ${normalizedWait}\n`); if(wait < 0) { const ticksOver = (-wait / timeout) + 1; if(ticksOver > 1.5) console.log(chalk.yellow('Can\'t keep up! Tick took ' + ticksOver.toFixed(2) + ' ticks (' + (timeout - wait).toFixed(4) + 'ms)')); } this.ticksInSecond ++; if(end > this.lastTPSCheckpoint + 1000) { this.lastTPSCheckpoint = end; this.tps = this.ticksInSecond; this.ticksInSecond = 0; } if(this.paused) return; setTimeout(this._boundTick, normalizedWait); } } } export interface Tickable { tick: (seconds: number) => Promise } function ms4() { const a = process.hrtime() return a[0]*10e2 + a[1]/1000000; }