// ==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.4
// @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);
throw 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) {
if (err instanceof Error)
captureStackTrace(err, timeSetter);
throw err;
}
},
enumerable: false,
configurable: true
}, timeGetter = desc.get, timeSetter = desc.set;
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));