Time Control

Script allowing you to control time.

As of 2025-10-14. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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.5.3
// @author       lucaszheng
// @license      MIT
//
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues

// @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,
      defineProperty,
      get
    },
    Object,
    Object: {
      freeze,
      hasOwn,
      create
    },
    Event,
    Number: {
      isFinite
    },
    Symbol: {
      toPrimitive,
      toStringTag
    },
    console: {
      trace: log
    },
    Error,
    ReferenceError,
    String: {
      raw
    },
    RegExp
  } = 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]), []);
  }

  let profile_id = '';
  /** @param {string} name */
  function get_var_name(name) {
    if (profile_id != '') name = name + '_profile_' + profile_id;
    return name;
  }
  /** @type {typeof GM_getValue} */
  function getValue(name, defaultValue) {
    return GM_getValue(get_var_name(name), defaultValue);
  }
  /** @type {typeof GM_setValue} */
  function setValue(name, value) {
    return GM_setValue(get_var_name(name), value);
  }
  /** @type {typeof GM_deleteValue} */
  function deleteValue(name) {
    return GM_deleteValue(get_var_name(name));
  }

  const substring = String.prototype.substring;
  function getProfiles() {
    const keys = GM_listValues();
    const profiles = [];
    /** @type {{[key: string]: boolean}} */
    const seen = {};
    const match = '_profile_';
    setPrototypeOf(seen, null);

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      for (let j = 1; j < 20 && j < (key.length - match.length); j++) {
        if (apply(substring, key, [j, j + match.length]) === match) {
          const profile = apply(substring, key, [j + match.length, key.length]);
          if (seen[profile]) break;
          profiles[profiles.length] = profile;
          seen[profile] = true;
          break;
        }
      }
    }

    return profiles;
  }

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

    /**
     * @param {number | null} [shiftTime]
     */
    shift(shiftTime) {
      if (!shiftTime) return;
      shiftTime = +shiftTime;
      if (!shiftTime) return;
      time.jump(time.now + shiftTime);
    },

    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,

      get profile() {
        return profile_id || null;
      },
      set profile(val) {
        profile_id = (val ?? '') + '';
      },

      get profiles() {
        return getProfiles();
      },

      /**
       * @param {string | null} [profile]
       */
      erase(profile) {
        const prev_profile_id = profile_id;
        profile_id = (profile ?? '') + '';
        try {
          time.storage.reset();
        } finally {
          profile_id = prev_profile_id;
        }
      },

      /**
       * @param {number} newTime
       */
      jump(newTime) {
        setValue('baseTime', time.real);
        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 = getValue('baseTime', null);
          let contTime = getValue('contTime', null);
          if (baseTime != null && contTime != null)
            time.jump((time.real - baseTime) * time.storage.scale + contTime);
        }
        if (loadScale) time.scale = time.storage.scale;
      },

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

      get debug() { return getValue('debug', false); },
      set debug(value) { setValue('debug', !!value); },

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

      get pristine() {
        let baseTime = getValue('baseTime', null);
        let contTime = getValue('contTime', null);
        let scale = 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 = getValue('scale', null);
        if (scale != null) return scale;
        return 1;
      },
      set scale(value) {
        if (value === time.storage.scale) return;
        setValue('scale', +value);
      }
    },

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

    get now() { return date.now(); },
    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;
    },

    get hidden() {
      return time_global_is_hidden;
    },

    set hidden(val) {
      time_global_is_hidden = !!val;
    }
  };

  let time_global_is_hidden = true;

  const testRegExp = RegExp.prototype.test;
  /** @returns {[stackIntrospection: false, windowProps: null] | [stackIntrospection: true, windowProps: boolean]} */
  function detectDevtools() {
    try {
      throw new Error();
    } catch (thrownError) {
      try {
        if (!(thrownError instanceof Error)) return [false, null];
        const stack = thrownError.stack;
        if (!stack) return [false, null];
        const regex = /(\n|^)\s*(global code@|@debugger eval code:.*|at <anonymous>:.*)\s*$/;
        if (!apply(testRegExp, regex, [stack])) return [false, null];
      } catch { return [false, null];  }
    }
    try {
      const props = ['$', '$$', '$x', 'clear', 'copy', 'inspect', 'keys', 'values'];
      for (let i = 0; i < props.length; i++)
        if (!(props[i] in window)) return [true, false];
    } catch { return [true, false]; }
    return [true, true];
  }

  /** @type {<T extends (...args: any[]) => any>(object: object, constructor: T) => void} */
  let captureStackTrace = () => { };
  if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function')
    captureStackTrace = /** @type {any} */(Error.captureStackTrace);

  /**
   * @param {(...args: any[]) => any} introspectionPoint
   * @param {number} safariTrimLevel
   */
  function detectEval(introspectionPoint = detectEval, safariTrimLevel = 1) {
    try {
      const err = new Error();
      captureStackTrace(err, introspectionPoint);
      const stack = err.stack;
      if (!stack) return false;
      const regexStr = raw`^\s*(Error\s*\n\s*at eval(:| ).*|.*> eval:.*|` +
                       '(?:.*\n)?'.repeat(safariTrimLevel) + raw`\s*eval code@)\s*(\n|$)`;
      if (apply(testRegExp, new RegExp(regexStr), [stack])) return true;
    } catch { return false; }
  }

  freeze(time.storage); freeze(time);
  const windowProto = getPrototypeOf(window);
  if (windowProto) {
    const windowProperties = getPrototypeOf(windowProto) ?? create(null);
    /** @type {Required<Pick<PropertyDescriptor, 'get' | 'set' | 'configurable' | 'enumerable'>> & ThisType<any>} */
    const desc = {
      get() {
        if (!time_global_is_hidden) return time;
        if (this === window) {
          const result = detectDevtools();
          if (result[0] && result[1]) return time;
          if (!('time' in windowProperties) && detectEval(timeGetter, 2)) {
            const refError = new ReferenceError('time is not defined');
            captureStackTrace(refError, timeGetter);
            throw refError;
          }
        }
        try {
          return get(windowProperties, 'time', window);
        } catch (err) {
          if (err instanceof Error)
            captureStackTrace(err, timeGetter);
          return err;
        }
      },
      set(value) {
        try {
          const self = Object(this ?? window);
          const hasProp = hasOwn(self, 'time');
          if (hasProp) {
            self.time = value;
          } else {
            const desc = {
              value, writable: true,
              enumerable: true, configurable: true
            };
            setPrototypeOf(desc, null);
            defineProperty(self, 'time', desc);
          }
        } catch (err) {
          log(err);
        }
      },
      enumerable: false,
      configurable: true
    }, timeGetter = desc.get;
    defineProperty(windowProto, 'time', desc);
  }

  /** @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, {
        get: wrappedGetter
      });
      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));