Time Control

Script allowing you to control time.

// ==UserScript==
// @name         Time Control
// @description  Script allowing you to control time.
// @icon         https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/ce262758ff44d053136358dcd892979d_low_res_Time_Machine.png
// @namespace    mailto:[email protected]
// @version      1.4.0
// @author       lucaszheng
// @license      MIT
//
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue

// @inject-into  page
// @run-at       document-start
// ==/UserScript==
/*globals unsafeWindow,GM_setValue,GM_getValue,GM_deleteValue*/

(function (window) {
  'use strict';
  let scale = 1, pristine = true;
  /** @type {null | number} */
  let timeJump = null;

  let timeReset = false;
  let debug = false;

  const {
    Reflect: {
      apply, construct,
      setPrototypeOf,
      getPrototypeOf,
      getOwnPropertyDescriptor
    },
    Object: {
      defineProperty,
      freeze
    },
    Event,
    Number: {
      isFinite
    },
    Symbol: {
      toPrimitive,
      toStringTag
    },
    console: {
      trace: log
    }
  } = window;

  function update() {
    for (let idx = 0; idx < updaters.length; idx++) {
      updaters[idx]();
    }
  }

  /**
   * @this { { toString: () => string; now: number }}
   * @param {'string' | 'number' | 'default'} type
   */
  function timeToPrimitive(type) {
    switch (type) {
      case 'string': return this.toString();
      default: return this.now;
    }
  }

  /**
   * @this { { now: number } }
   */
  function timeToString() {
    return apply(date.toString, construct(DateConstructor, [this.now]), []);
  }

  const time = {
    [toStringTag]: 'time',
    [toPrimitive]: timeToPrimitive,
    toString: timeToString,
    /**
     * @param {number} newTime
     */
    jump(newTime) {
      if (newTime == null) return;
      pristine = false;
      timeJump = +newTime;
      update();
      timeJump = null;
    },

    reset(resetTime = true, resetScale = true, resetDebug = true) {
      if (resetDebug) debug = false;
      if (pristine) return;

      if (resetScale) scale = 1;

      if (!resetTime) return;
      timeReset = true;
      update();
      timeReset = false;
      pristine = scale === 1;
    },

    storage: {
      [toStringTag]: 'storage',
      [toPrimitive]: timeToPrimitive,
      toString: timeToString,

      /**
       * @param {number} newTime
       */
      jump(newTime) {
        GM_setValue('baseTime', time.real);
        GM_setValue('contTime', +newTime);
      },

      save(saveTime = true, saveScale = true, saveDebug = true) {
        if (saveDebug) {
          if (debug === false) time.storage.reset(false, false, true);
          else time.storage.debug = debug;
        }
        if (saveTime) {
          if (pristine) time.storage.reset(true, false, false);
          else time.storage.now = time.now;
        }
        if (saveScale) {
          if (scale === 1) time.storage.reset(false, true, false);
          else time.storage.scale = scale;
        }
      },

      load(loadTime = true, loadScale = true, loadDebug = true) {
        if (loadDebug) time.debug = time.storage.debug;
        if (time.storage.pristine) return time.reset(true, true, false);

        if (loadTime) {
          let baseTime = GM_getValue('baseTime', null);
          let contTime = GM_getValue('contTime', null);
          if (baseTime != null && contTime != null)
            time.jump((time.real - baseTime) + contTime);
        }
        if (loadScale) time.scale = time.storage.scale;
      },

      reset(resetTime = true, resetScale = true, resetDebug = true) {
        if (resetTime) {
          GM_deleteValue('baseTime');
          GM_deleteValue('contTime');
        }
        if (resetScale) GM_deleteValue('scale');
        if (resetDebug) GM_deleteValue('debug');
      },

      get debug() { return GM_getValue('debug', false); },
      set debug(value) { GM_setValue('debug', !!value); },

      get now() {
        let baseTime = GM_getValue('baseTime', null);
        let contTime = GM_getValue('contTime', null);
        if (baseTime != null && contTime != null)
          return (time.real - baseTime) + contTime;
        return time.real;
      },
      set now(value) { time.storage.jump(value); },

      get pristine() {
        let baseTime = GM_getValue('baseTime', null);
        let contTime = GM_getValue('contTime', null);
        let scale = GM_getValue('scale', null);
        return (baseTime == null || contTime == null) && scale == null;
      },
      set pristine(value) {
        if (!value) return;
        time.storage.reset(true, true, false);
      },

      get real() { return date.realTime(); },

      get scale() {
        let scale = GM_getValue('scale', null);
        if (scale != null) return scale;
        return 1;
      },
      set scale(value) {
        if (value === time.storage.scale) return;
        GM_setValue('scale', +value);
      }
    },

    get debug() { return debug; },
    set debug(value) { debug = !!value; },

    get now() { return apply(date.now, DateConstructor, []); },
    set now(value) { time.jump(value); },

    get pristine() { return pristine; },
    set pristine(value) { if (value) time.reset(); },

    get real() { return date.realTime(); },

    get scale() { return scale; },
    set scale(value) {
      value = +value;
      if (value === scale) return;
      pristine = false; update(); scale = value;
    }
  };

  freeze(time.storage);
  defineProperty(getPrototypeOf(window), 'time', {
    value: freeze(time),
    writable: true,
    enumerable: false,
    configurable: true
  });

  /** @type {(() => void)[]} */
  const updaters = [];

  /**
   * @template {() => number | null | undefined} T
   * @param {T} func
   * @param {object} self
   * @param {object | null} req_self
   * @param {(func: T) => number} offset
   */
  function wrap_now(func, self, offset = () => 0, req_self = null) {
    let baseTime = 0;
    let contTime = baseTime;

    /** @type {ProxyHandler<typeof func>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        let time = apply(target, self, args);
        // pristine check necessary due to handler.apply(func, self, [])
        if (pristine || !isFinite(time) || (req_self !== null && self !== req_self)) return time;
        return ((time - baseTime) * scale) + contTime;
      }
    };
    setPrototypeOf(handler, null);

    updaters[updaters.length] =
      function update() {
        if (!handler.apply) return;
        contTime = timeJump == null ? handler.apply(func, self, []) : timeJump + offset(func);
        baseTime = apply(func, self, []) ?? baseTime;
        if (timeReset) contTime = baseTime;
      };

    return new Proxy(func, wrapHandler(handler));
  }

  /**
   * @template {object} O
   * @template {keyof O} P
   * @template {(this: O) => Extract<O[P], number | null | undefined>} T
   * @param {O} obj
   * @param {P} prop
   * @param {() => object} getSelf
   * @param {null | ((getter: (...args: unknown[]) => O[P]) => T)} getFunc
   * @param {object | null} req_self
   * @param {(func: T) => number} offset
   */
  function wrap_getter(obj, prop, getSelf, getFunc = null, offset = () => 0, req_self = null) {
    const propDesc = getOwnPropertyDescriptor(obj, prop);
    if (propDesc?.get) {
      const func = getFunc?.(propDesc.get) ?? propDesc.get, real_func = propDesc.get;
      let baseTime = 0;
      let contTime = baseTime;

      /** @type {ProxyHandler<typeof real_func>} */
      const handler = {
        apply(_target, self, args) {
          // cannot show `self`, it results in infinite loop from Chrome Devtools automatically expanding document.timeline
          if (debug) log('apply(%o, self, %o)', func, args);
          let time = apply(func, self, args);
          // pristine check necessary due to handler.apply(func, self, [])
          if (pristine || !isFinite(time) || (req_self !== null && self !== req_self)) return time;
          return ((time - baseTime) * scale) + contTime;
        }
      };
      setPrototypeOf(handler, null);

      updaters[updaters.length] =
        function update() {
          if (!handler.apply) return;
          contTime = timeJump == null ? handler.apply(real_func, getSelf(), []) : timeJump + offset(/** @type {T} */(func));
          baseTime = apply(func, getSelf(), []) ?? baseTime;
          if (timeReset) contTime = baseTime;
        };

      const wrappedGetter = new Proxy(real_func, wrapHandler(handler));

      defineProperty(obj, prop, {
        configurable: propDesc.configurable,
        enumerable: propDesc.enumerable,
        get: wrappedGetter,
        set: propDesc.set
      });
      return /** @type {T} */(wrappedGetter);
    }
    return null;
  }

  const DateConstructor = window.Date;
  /** @type {{ realTime: typeof Date.now, now: typeof Date.now, real_perfNow: typeof performance.now, toString: typeof Date.prototype.toString, handler: ProxyHandler<DateConstructor> }} */
  const date = {
    realTime: window.Date.now,
    now: wrap_now(window.Date.now, window.Date),
    real_perfNow: window.performance.now.bind(performance),
    toString: DateConstructor.prototype.toString,
    handler: {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        return time.toString();
      },
      construct(target, args, newTarget) {
        if (debug) log('construct(%o, %o, %o)', target, args, newTarget);
        if (args.length < 1) {
          args[0] = time.now;
        }
        return construct(DateConstructor, args, newTarget);
      }
    }
  };
  setPrototypeOf(date, null);
  setPrototypeOf(date.handler, null);
  DateConstructor.now = date.now;

  window.Date = new Proxy(DateConstructor, wrapHandler(date.handler));
  window.Date.prototype.constructor = window.Date;

  window.Performance.prototype.now = wrap_now(
    window.Performance.prototype.now,
    window.performance,
    () => date.real_perfNow() - date.realTime(),
    window.performance
  );

  function noop() { }

  /**
   * @param {(handler: TimerHandler, timeout?: number | undefined, ...args: any[]) => number} func
   */
  function wrap_timer(func) {
    /** @type {ProxyHandler<typeof func>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        if (args.length > 1) {
          args[1] = +args[1];
          if (args[1] && scale === 0)
            args[0] = noop;
          else if (args[1] && isFinite(args[1]))
            args[1] /= scale;
        }
        return apply(target, self, args);
      }
    };
    setPrototypeOf(handler, null);
    return new Proxy(func, wrapHandler(handler));
  }

  window.setTimeout = wrap_timer(window.setTimeout);
  window.setInterval = wrap_timer(window.setInterval);

  const docTimeline = window.document.timeline;
  const wrappedGetAnimTime = wrap_getter(
    window.AnimationTimeline.prototype, 'currentTime', () => docTimeline,
    (func) => function () {
      const time = apply(func, this, arguments);
      if (this !== docTimeline) return time;
      return typeof time === 'number' ? time : null;
    },
    getAnimTime =>
      (apply(getAnimTime, docTimeline, []) ?? date.real_perfNow()) - date.realTime(),
    docTimeline
  );
  if (wrappedGetAnimTime) {
    /** @type {ProxyHandler<typeof requestAnimationFrame>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        if (typeof args[0] === 'function') {
          const cb = args[0];
          args[0] = function () {
            if (!pristine)
              arguments[0] = apply(wrappedGetAnimTime, docTimeline, []);
            return apply(cb, this, arguments);
          }
        }
        return apply(target, self, args);
      }
    };
    setPrototypeOf(handler, null);
    window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, wrapHandler(handler));
  }
  wrap_getter(
    window.Event.prototype, 'timeStamp', () => new Event(''), null,
    getTimeStamp =>
      (apply(getTimeStamp, new Event(''), []) ?? date.real_perfNow()) - date.realTime()
  );

  /**
   * @param {ProxyHandler<any>} handler
   */
  function wrapHandler(handler) {
    /** @type {ProxyHandler<ProxyHandler<any>>} */
    const internalHandler = {
      get(target, prop) {
        if (pristine) return undefined;
        return Reflect.get(target, prop);
      }
    };
    setPrototypeOf(internalHandler, null);
    return new Proxy(handler, internalHandler);
  }

  time.storage.load();
})(/** @type {typeof window} */(unsafeWindow));