🏠 

TankTrouble Development Library

Shared library for TankTrouble userscript development

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/482092/1309109/TankTrouble%20Development%20Library.js

// ==UserScript==
// @name        TankTrouble Development Library
// @author      commander
// @namespace   https://github.com/asger-finding/tanktrouble-userscripts
// @version     1.0.0-beta.1
// @license     GPL-3.0
// @description Shared library for TankTrouble userscript development
// @match       *://*.tanktrouble.com/*
// @grant       none
// @run-at      document-start
// @noframes
// ==/UserScript==
// eslint-disable-next-line no-unused-vars
class Loader {
/**
* Pass a function to a hook with the correct context
* @param context Function context (e.g `window`)
* @param funcName Function identifier in the context
* @param handler Hook to call before the original
* @param attributes Optionally additional descriptors
*/
static interceptFunction(context, funcName, handler, attributes) {
const original = Reflect.get(context, funcName);
if (typeof original !== 'function') throw new Error('Item passed is not typeof function');
Reflect.defineProperty(context, funcName, {
/**
* Call the handler with the original function bound to its context
* and supply with the arguments list
* @param args Arguments passed from outside
* @returns Original function return value
*/
value: (...args) => handler(original.bind(context), ...args),
...attributes
});
}
/**
* Fires when the `main()` function is done on TankTrouble.
* @returns Promise that resolves when Content.init() finishes
*/
static whenContentInitialized() {
if (GM.info.script.runAt !== 'document-start') return Loader.#createGameProxy();
return whenContentLoaded().then(() => Loader.#createGameProxy());
}
/**
* Fires when the document is readyState `interactive` or `complete`
* @returns Promise that resolves upon content loaded
*/
static whenContentLoaded() {
return new Promise(resolve => {
if (document.readyState === 'interactive' || document.readyState === 'complete') resolve();
else document.addEventListener('DOMContentLoaded', () => resolve());
});
}
/**
* Apply a hook to the Content.init function which resolves when the promise ends
* @returns Promise when Content.init has finished
* @private
*/
static #createGameProxy() {
const functionString = Function.prototype.toString.call(Content.init);
const isAlreadyHooked = /hooked-by-userscript/u.test(functionString);
return new Promise(resolve => {
if (isAlreadyHooked) {
const eventListener = document.addEventListener('content-initialized', () => {
document.removeEventListener('content-initialized', eventListener);
resolve();
});
} else {
const event = new Event('content-initialized');
const { init } = Content;
Reflect.defineProperty(Content, 'init', {
/**
* Intercept the Content.init function, add a stamp, dispatch the custom event and resolve
* @param args Arguments passed from outside
* @returns Original function return value
*/
value: (...args) => {
// Hack that will add the string to
// the return of toString so we can
// lookup if it's already hooked
// eslint-disable-next-line no-void
void 'hooked-by-userscript';
const result = init(...args);
document.dispatchEvent(event);
resolve();
return result;
},
configurable: true
});
}
});
}
}