DH2 Fixed

Improve Diamond Hunt 2

目前为 2017-02-27 提交的版本。查看 最新版本

// ==UserScript==
// @name         DH2 Fixed
// @namespace    FileFace
// @description  Improve Diamond Hunt 2
// @version      0.29.1
// @author       Zorbing
// @grant        none
// @run-at       document-start
// @include      http://www.diamondhunt.co/game.php
// ==/UserScript==

(function ()
{
'use strict';



/**
 * observer
 */

let observedKeys = new Map();
/**
 * Observes the given key for change
 * 
 * @param {string} key	The name of the variable
 * @param {Function} fn	The function which is called on change
 */
function observe(key, fn)
{
	if (key instanceof Array)
	{
		for (let k of key)
		{
			observe(k, fn);
		}
	}
	else
	{
		if (!observedKeys.has(key))
		{
			observedKeys.set(key, new Set());
		}
		observedKeys.get(key).add(fn);
	}
	return fn;
}
function unobserve(key, fn)
{
	if (key instanceof Array)
	{
		let ret = [];
		for (let k of key)
		{
			ret.push(unobserve(k, fn));
		}
		return ret;
	}
	if (!observedKeys.has(key))
	{
		return false;
	}
	return observedKeys.get(key).delete(fn);
}
function updateValue(key, newValue)
{
	if (window[key] === newValue)
	{
		return false;
	}

	const oldValue = window[key];
	window[key] = newValue;
	(observedKeys.get(key) || []).forEach(fn => fn(key, oldValue, newValue));
	return true;
}



/**
 * global constants
 */

const tierLevels = ['empty', 'sapphire', 'emerald', 'ruby', 'diamond'];
const furnaceLevels = ['stone', 'bronze', 'iron', 'silver', 'gold'];
const furnaceCapacity = [10, 30, 75, 150, 300];
const ovenLevels = ['bronze', 'iron', 'silver', 'gold'];
const maxOilStorageLevel = 3; // 7
const oilStorageSize = [10e3, 50e3, 100e3];



/**
 * general functions
 */

let styleElement = null;
function addStyle(styleCode)
{
	if (styleElement === null)
	{
		styleElement = document.createElement('style');
		document.head.appendChild(styleElement);
	}
	styleElement.innerHTML += styleCode;
}
function getBoundKey(key)
{
	return 'bound' + key[0].toUpperCase() + key.substr(1);
}
function getTierKey(key, tierLevel)
{
	return tierLevels[tierLevel] + key[0].toUpperCase() + key.substr(1);
}
function formatNumber(num)
{
	return parseFloat(num).toLocaleString('en');
}
function formatNumbersInText(text)
{
	return text.replace(/\d(?:[\d',\.]*\d)?/g, (numStr) =>
	{
		return formatNumber(parseInt(numStr.replace(/\D/g, ''), 10));
	});
}
function now()
{
	return (new Date()).getTime();
}
function padLeft(num, padChar)
{
	return (num < 10 ? padChar : '') + num;
}
function formatTimer(timer)
{
	timer = parseInt(timer, 10);
	const hours = Math.floor(timer / 3600);
	const minutes = Math.floor((timer % 3600) / 60);
	const seconds = timer % 60;
	return padLeft(hours, '0') + ':' + padLeft(minutes, '0') + ':' + padLeft(seconds, '0');
}
const timeSteps = [
	{
		threshold: 1
		, name: 'second'
		, short: 'sec'
		, padp: 0
	}
	, {
		threshold: 60
		, name: 'minute'
		, short: 'min'
		, padp: 0
	}
	, {
		threshold: 3600
		, name: 'hour'
		, short: 'h'
		, padp: 1
	}
	, {
		threshold: 86400
		, name: 'day'
		, short: 'd'
		, padp: 2
	}
];
function formatTime2NearestUnit(time, long = false)
{
	let step = timeSteps[0];
	for (let i = timeSteps.length-1; i > 0; i--)
	{
		if (time >= timeSteps[i].threshold)
		{
			step = timeSteps[i];
			break;
		}
	}
	const factor = Math.pow(10, step.padp);
	const num = Math.round(time / step.threshold * factor) / factor;
	const unit = long ? step.name + (num === 1 ? '' : 's') : step.short;
	return num + ' ' + unit;
}



/**
 * hide crafting recipes of lower tiers or of maxed machines
 */

function hideTierRecipes(max, getKey, init)
{
	const keys2Observe = [];
	let maxLevel = 0;
	for (let i = max-1; i >= 0; i--)
	{
		const level = i+1;
		const key = getKey(i);
		const boundKey = getBoundKey(key);
		keys2Observe.push(key);
		keys2Observe.push(boundKey);
		if (window[key] > 0 || window[boundKey] > 0)
		{
			maxLevel = Math.max(maxLevel, level);
		}

		const recipeRow = document.getElementById('crafting-' + key);
		if (recipeRow)
		{
			const hide = level <= maxLevel;
			recipeRow.style.display = hide ? 'none' : '';
		}
	}

	if (init)
	{
		observe(keys2Observe, () => hideTierRecipes(max, getKey, false));
	}
}
function hideRecipe(key, max, init)
{
	const maxValue = typeof max === 'function' ? max() : max;
	const boundKey = getBoundKey(key);
	const unbound = parseInt(window[key], 10);
	const bound = parseInt(window[boundKey], 10);

	const recipeRow = document.getElementById('crafting-' + key);
	if (recipeRow)
	{
		const hide = (bound + unbound) >= maxValue;
		recipeRow.style.display = hide ? 'none' : '';
	}

	if (init)
	{
		observe([key, boundKey], () => hideRecipe(key, max, false));
	}
}
function hideCraftedRecipes()
{
	function processRecipes(init)
	{
		// furnace
		hideTierRecipes(
			furnaceLevels.length
			, i => furnaceLevels[i] + 'Furnace'
			, init
		);
		// oil storage
		hideTierRecipes(
			7
			, i => 'oilStorage' + (i+1)
			, init
		);
		// oven recipes
		hideTierRecipes(
			ovenLevels.length
			, i => ovenLevels[i] + 'Oven'
			, init
		);
		// drills
		hideRecipe('drills', 10, init);
		// crushers
		hideRecipe('crushers', 10, init);
		// oil pipe
		hideRecipe('oilPipe', 1, init);
		// row boat
		hideRecipe('rowBoat', 1, init);
	}
	processRecipes(true);

	const oldProcessCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		oldProcessCraftingTab();

		if (reinit)
		{
			processRecipes(true);
		}
	};
}



/**
 * improve item boxes
 */

function hideNumberInItemBox(key, setVisibility)
{
	const itemBox = document.getElementById('item-box-' + key);
	const numberElement = itemBox.lastElementChild;
	if (setVisibility)
	{
		numberElement.style.visibility = 'hidden';
	}
	else
	{
		numberElement.style.display = 'none';
	}
}
function addSpan2ItemBox(key)
{
	hideNumberInItemBox(key);

	const itemBox = document.getElementById('item-box-' + key);
	const span = document.createElement('span');
	itemBox.appendChild(span);
	return span;
}
function setOilPerSecond(span, oil)
{
	span.innerHTML = `+ ${formatNumber(oil)} L/s <img src="images/oil.png" class="image-icon-20" style="margin-top: -2px;">`;
}
function improveItemBoxes()
{
	// show capacity of furnace
	for (let i = 0; i < furnaceLevels.length; i++)
	{
		const key = furnaceLevels[i] + 'Furnace';
		const capacitySpan = addSpan2ItemBox(getBoundKey(key));
		capacitySpan.className = 'capacity';
		capacitySpan.textContent = 'Capacity: ' + formatNumber(furnaceCapacity[i]);
	}

	// show oil cap of oil storage
	for (let i = 0; i < maxOilStorageLevel; i++)
	{
		const key = 'oilStorage' + (i+1);
		const capSpan = addSpan2ItemBox(getBoundKey(key));
		capSpan.className = 'oil-cap';
		capSpan.textContent = 'Oil cap: ' + formatNumber(oilStorageSize[i]);
	}

	// show oil per second
	const handheldOilSpan = addSpan2ItemBox('handheldOilPump');
	setOilPerSecond(handheldOilSpan, 1*window.miner);
	observe('miner', () => setOilPerSecond(handheldOilSpan, 1*window.miner));
	const oilPipeSpan = addSpan2ItemBox('boundOilPipe');
	setOilPerSecond(oilPipeSpan, 50);

	// show current tier
	hideNumberInItemBox('emptyAnvil', true);
	hideNumberInItemBox('farmer', true);
	hideNumberInItemBox('planter', true);
	const tierItemList = ['pickaxe', 'shovel', 'hammer', 'axe', 'rake', 'fishingRod'];
	for (let tierItem of tierItemList)
	{
		for (let i = 0; i < tierLevels.length; i++)
		{
			const key = getTierKey(tierItem, i);
			const tierSpan = addSpan2ItemBox(tierItem == 'rake' ? key : getBoundKey(key));
			tierSpan.className = 'tier';
			tierSpan.textContent = 'Tier: ' + i;
		}
	}

	// show boat progress
	function setTransitText(span, isInTransit)
	{
		span.textContent = isInTransit ? 'In transit' : 'Ready';
	}
	const boatSpan = addSpan2ItemBox('boundRowBoat');
	setTransitText(boatSpan, window.rowBoatTimer > 0);
	observe('rowBoatTimer', () => setTransitText(boatSpan, window.rowBoatTimer > 0));
	const canoeSpan = addSpan2ItemBox('boundCanoe');
	setTransitText(canoeSpan, window.canoeTimer > 0);
	observe('canoeTimer', () => setTransitText(canoeSpan, window.canoeTimer > 0));
}



/**
 * fix wood cutting
 */

function fixWoodcutting()
{
	addStyle(`
img.woodcutting-tree-img
{
	border: 1px solid transparent;
}
	`);
}



/**
 * fix chat
 */

const lastMsg = new Map();
function isMuted(user)
{
	// return window.mutedPeople.some((name) => user.indexOf(name) > -1);
	return window.mutedPeople.includes(user);
}
function isSpam(user, msg)
{
	return lastMsg.has(user) &&
		lastMsg.get(user).msg == msg &&
		// last message in the last 30 seconds?
		(now() - lastMsg.get(user).time) < 30e3;
}
function handleSpam(user, msg)
{
	const msgObj = lastMsg.get(user);
	msgObj.time = now();
	msgObj.repeat++;
	// a user is allowed to repeat a message twice (write it 3 times in total)
	if (msgObj.repeat > 1)
	{
		window.mutedPeople.push(user);
	}
}
function fixChat()
{
	const oldAddToChatBox = window.addToChatBox;
	window.addToChatBox = (userChatting, iconSet, tagSet, msg, isPM) =>
	{
		if (isMuted(userChatting))
		{
			return;
		}
		if (isSpam(userChatting, msg))
		{
			return handleSpam(userChatting, msg);
		}
		lastMsg.set(userChatting, {
			time: now()
			, msg: msg
			, repeat: 0
		});

		// add clickable links
		msg = msg.replace(/(https?:\/\/[^\s"<>]+)/g, '<a target="_blank" href="$1">$1</a>');

		oldAddToChatBox(userChatting, iconSet, tagSet, msg, isPM);
	};

	// add checkbox instead of button for toggling auto scrolling
	const btn = document.querySelector('input[value="Toggle Autoscroll"]');
	const checkboxId = 'chat-toggle-autoscroll';
	// create checkbox
	const toggleCheckbox = document.createElement('input');
	toggleCheckbox.type = 'checkbox';
	toggleCheckbox.id = checkboxId;
	toggleCheckbox.checked = true;
	// create label
	const toggleLabel = document.createElement('label');
	toggleLabel.htmlFor = checkboxId;
	toggleLabel.textContent = 'Autoscroll';
	btn.parentNode.insertBefore(toggleCheckbox, btn);
	btn.parentNode.insertBefore(toggleLabel, btn);
	btn.style.display = 'none';

	toggleCheckbox.addEventListener('change', function ()
	{
		window.isAutoScrolling = this.checked;
		window.scrollText('none', this.checked ? 'lime' : 'red', (this.checked ? 'En' : 'Dis') + 'abled');
	});
}



/**
 * hopefully only temporary fixes
 */

function temporaryFixes()
{
	// fix recipe of oil storage 3
	const oldProcessCraftingTab = window.processCraftingTab;
	window.processCraftingTab = () =>
	{
		const reinit = !!window.refreshLoadCraftingTable;
		oldProcessCraftingTab();

		if (reinit)
		{
			// 200 instead of 100 gold bars
			window.craftingRecipes['oilStorage3'].recipeCost[2] = 200;
			document.getElementById('recipe-cost-oilStorage3-2').textContent = 200;
			window.showMateriesNeededAndLevelLabels('oilStorage3');
		}
	};

	// fix burn rate of ovens
	window.getOvenBurnRate = () =>
	{
		if (boundBronzeOven == 1)
		{
			return .5;
		}
		else if (boundIronOven == 1)
		{
			return .4;
		}
		else if (boundSilverOven == 1)
		{
			return .3;
		}
		else if (boundGoldOven == 1)
		{
			return .2;
		}
		return 1;
	};

	// fix grow time of some seeds
	const seeds = {
		'limeLeafSeeds': '1 hour and 30 minutes'
		, 'greenLeafSeeds': '45 minutes'
	};
	for (let seedName in seeds)
	{
		const tooltip = document.getElementById('tooltip-' + seedName);
		tooltip.lastElementChild.lastChild.textContent = seeds[seedName];
	}

	// fix exhaustion timer
	const oldClientGameLoop = window.clientGameLoop;
	window.clientGameLoop = () =>
	{
		oldClientGameLoop();
		if (document.getElementById('tab-container-combat').style.display != 'none')
		{
			combatNotFightingTick();
		}
	};

	// fix scrollText (e.g. when joining the game and receiving xp at that moment)
	window.mouseX = window.innerWidth / 2;
	window.mouseY = window.innerHeight / 2;
}



/**
 * improve timer
 */

function improveTimer()
{
	window.formatTime = (seconds) =>
	{
		return formatTimer(seconds);
	};
	window.formatTimeShort2 = (seconds) =>
	{
		return formatTimer(seconds);
	};

	const barInfo = {
		1: {
			name: 'bronze'
			, timePerBar: 1
		}
		, 2: {
			name: 'iron'
			, timePerBar: 5
		}
		, 3: {
			name: 'silver'
			, timePerBar: 10
		}
		, 4: {
			name: 'gold'
			, timePerBar: 30
		}
		, 5: {
			name: 'glass'
			, timePerBar: 1
		}
	};
	const smeltingPercEl = document.querySelector('span[data-item-display="smeltingPerc"]');
	const smeltingTimerEl = document.createElement('span');
	smeltingPercEl.style.display = 'none';
	smeltingPercEl.parentNode.lastElementChild.style.display = 'none';
	smeltingPercEl.parentNode.appendChild(smeltingTimerEl);
	let interval = null;
	let barName = '';
	let remainingTime = 0;
	function stopTimerTick()
	{
		interval && window.clearInterval(interval);
		barName = '';
		interval = null;
	}
	function setRemainingTime(time)
	{
		remainingTime = time;
		if (remainingTime < 0)
		{
			stopTimerTick();
		}
		smeltingTimerEl.textContent = formatTimer(Math.max(remainingTime, 0));
	}
	function updateSmeltingPerc()
	{
		const info = barInfo[window.smeltingBarType];
		if (info)
		{
			const totalTime = info.timePerBar * window.smeltingTotalAmount;
			const time = Math.round(totalTime * (1 - window.smeltingPerc / 100));
			if (interval == null || barName != info.name)
			{
				stopTimerTick();
				barName = info.name;
				interval = window.setInterval(() => setRemainingTime(remainingTime-1), 1000);
				setRemainingTime(time);
			}
			// tolerate up to 2 seconds deviation (to make it smoother for the user)
			else if (Math.abs(remainingTime - time) > 2)
			{
				setRemainingTime(time);
			}
		}
		else if (interval)
		{
			stopTimerTick();
		}
	}
	observe('smeltingPerc', () => updateSmeltingPerc());
	observe('smeltingTotalAmount', () => updateSmeltingPerc());
	updateSmeltingPerc();

	// add tree grow timer
	const treeInfo = {
		1: {
			name: 'Normal tree'
			// 3h = 10800s
			, growTime: 3 * 60 * 60
		}
		, 2: {
			name: 'Oak tree'
			// 6h = 21600s
			, growTime: 6 * 60 * 60
		}
		, 3: {
			name: 'Willow tree'
			 // 8h = 28800s
			, growTime: 8 * 60 * 60
		}
	};
	function updateTreeInfo(place, nameEl, timerEl, init)
	{
		const idKey = 'treeId' + place;
		const growTimerKey = 'treeGrowTimer' + place;
		const lockedKey = 'treeUnlocked' + place;

		const info = treeInfo[window[idKey]];
		if (!info)
		{
			const isLocked = place > 4 && window[lockedKey] == 0;
			nameEl.textContent = isLocked ? 'Locked' : 'Nothing';
			timerEl.textContent = '';
		}
		else
		{
			nameEl.textContent = info.name;
			const remainingTime = info.growTime - parseInt(window[growTimerKey], 10);
			timerEl.textContent = remainingTime > 0 ? '(' + formatTimer(remainingTime) + ')' : 'Fully grown';
		}

		if (init)
		{
			observe(
				[idKey, growTimerKey, lockedKey]
				, () => updateTreeInfo(place, nameEl, timerEl, false)
			);
		}
	}
	for (let i = 0; i < 6; i++)
	{
		const treePlace = i+1;
		const treeContainer = document.getElementById('wc-div-tree-' + treePlace);
		treeContainer.style.position = 'relative';
		const infoEl = document.createElement('div');
		infoEl.setAttribute('style', 'position: absolute; top: 0; left: 0; right: 0; pointer-events: none; margin-top: 5px; color: white;');
		const treeName = document.createElement('div');
		treeName.style.fontSize = '1.2rem';
		infoEl.appendChild(treeName);
		const treeTimer = document.createElement('div');
		infoEl.appendChild(treeTimer);
		treeContainer.appendChild(infoEl);

		updateTreeInfo(treePlace, treeName, treeTimer, true);
	}
}



/**
 * improve smelting dialog
 */

const smeltingRequirements = {
	'glass': {
		sand: 1
		, oil: 10
	}
	, 'bronzeBar': {
		copper: 1
		, tin: 1
		, oil: 10
	}
	, 'ironBar': {
		iron: 1
		, oil: 100
	}
	, 'silverBar': {
		silver: 1
		, oil: 300
	}
	, 'goldBar': {
		gold: 1
		, oil: 1e3
	}
};
function improveSmelting()
{
	const amountInput = document.getElementById('input-smelt-bars-amount');
	amountInput.type = 'number';
	amountInput.min = 0;
	amountInput.step = 5;
	function onValueChange(event)
	{
		smeltingValue = null;
		window.selectBar('', '', amountInput, document.getElementById('smelting-furnace-capacity').value);
	}
	amountInput.addEventListener('mouseup', onValueChange);
	amountInput.addEventListener('keyup', onValueChange);
	amountInput.setAttribute('onkeyup', '');

	const oldSelectBar = window.selectBar;
	let smeltingValue = null;
	window.selectBar = (bar, inputElement, inputBarsAmountEl, capacity) =>
	{
		const requirements = smeltingRequirements[bar];
		let maxAmount = capacity;
		for (let key in requirements)
		{
			maxAmount = Math.min(Math.floor(window[key] / requirements[key]), maxAmount);
		}
		if (amountInput.value > maxAmount)
		{
			smeltingValue = amountInput.value;
			amountInput.value = maxAmount;
		}
		else if (smeltingValue != null)
		{
			amountInput.value = Math.min(smeltingValue, maxAmount);
			if (smeltingValue <= maxAmount)
			{
				smeltingValue = null;
			}
		}
		return oldSelectBar(bar, inputElement, inputBarsAmountEl, capacity);
	};

	const oldOpenFurnaceDialogue = window.openFurnaceDialogue;
	window.openFurnaceDialogue = (furnace) =>
	{
		if (smeltingBarType == 0)
		{
			amountInput.max = getFurnaceCapacity(furnace);
		}
		return oldOpenFurnaceDialogue(furnace);
	};
}



/**
 * add chance to time calculator
 */

/**
 * calculates the number of seconds until the event with the given chance happened at least once with the given
 * probability p (in percent)
 */
function calcSecondsTillP(chancePerSecond, p)
{
	return Math.round(Math.log(1 - p/100) / Math.log(1 - chancePerSecond));
}
function addChanceTooltip(headline, chancePerSecond, elId, targetEl)
{
	// ensure element existence
	const tooltipElId = 'tooltip-chance-' + elId;
	let tooltipEl = document.getElementById(tooltipElId);
	if (!tooltipEl)
	{
		tooltipEl = document.createElement('div');
		tooltipEl.id = tooltipElId;
		tooltipEl.style.display = 'none';
		document.getElementById('tooltip-list').appendChild(tooltipEl);
	}

	// set elements content
	const percValues = [1, 10, 20, 50, 80, 90, 99];
	let percRows = '';
	for (let p of percValues)
	{
		percRows += `
			<tr>
				<td>${p}%</td>
				<td>${formatTime2NearestUnit(calcSecondsTillP(chancePerSecond, p), true)}</td>
			</tr>`;
	}
	tooltipEl.innerHTML = `<h2>${headline}</h2>
		<table class="chance">
			<tr>
				<th>Probability</th>
				<th>Time</th>
			</tr>
			${percRows}
		</table>
	`;

	// ensure binded events to show the tooltip
	if (targetEl.dataset.tooltipId == null)
	{
		targetEl.setAttribute('data-tooltip-id', tooltipElId);
		window.$(targetEl).bind({
			mousemove: window.changeTooltipPosition
			, mouseenter: window.showTooltip
			, mouseleave: window.hideTooltip
		});
	}
}
function chance2TimeCalculator()
{
	addStyle(`
table.chance
{
	border-spacing: 0;
}
table.chance th
{
	border-bottom: 1px solid gray;
}
table.chance td:first-child
{
	border-right: 1px solid gray;
	text-align: center;
}
table.chance th,
table.chance td
{
	padding: 4px 8px;
}
table.chance tr:nth-child(2n) td
{
	background-color: white;
}
	`);

	const oldClicksShovel = window.clicksShovel;
	window.clicksShovel = () =>
	{
		oldClicksShovel();

		const shovelChance = document.getElementById('dialogue-shovel-chance');
		const titleEl = shovelChance.parentElement;
		const chance = 1/window.getChanceOfDiggingSand();
		addChanceTooltip('One sand every:', chance, 'shovel', titleEl);
	};

	// depends on fishingXp
	const oldClicksFishingRod = window.clicksFishingRod;
	window.clicksFishingRod = () =>
	{
		oldClicksFishingRod();

		const fishList = ['shrimp', 'sardine', 'tuna', 'swordfish', 'shark'];
		for (let fish of fishList)
		{
			const rawFish = 'raw' + fish[0].toUpperCase() + fish.substr(1);
			const row = document.getElementById('dialogue-fishing-rod-tr-' + rawFish);
			const chance = row.cells[4].textContent
				.replace(/[^\d\/]/g, '')
				.split('/')
				.reduce((p, c) => p / parseInt(c, 10), 1)
			;
			addChanceTooltip(`One raw ${fish} every:`, chance, rawFish, row);
		}
	};
}



/**
 * init
 */

function init()
{
	temporaryFixes();

	hideCraftedRecipes();
	improveItemBoxes();
	fixWoodcutting();
	fixChat();
	improveTimer();
	improveSmelting();
	chance2TimeCalculator();
}
document.addEventListener('DOMContentLoaded', () =>
{
	const oldDoCommand = window.doCommand;
	window.doCommand = (data) =>
	{
		if (data.startsWith('REFRESH_ITEMS='))
		{
			const itemDataValues = data.split('=')[1].split(';');
			const itemArray = [];
			for (var i = 0; i < itemDataValues.length; i++)
			{
				const [key, newValue] = itemDataValues[i].split('~');
				if (updateValue(key, newValue))
				{
					itemArray.push(key);
				}
			}

			window.refreshItemValues(itemArray, false);

			if (window.firstLoadGame)
			{
				window.loadInitial();
				window.firstLoadGame = false;
				init();
			}
			else
			{
				window.clientGameLoop();
			}
			return;
		}
		return oldDoCommand(data);
	};
});



/**
 * fix web socket errors
 */

function webSocketLoaded(event)
{
	if (window.webSocket == null)
	{
		console.error('no webSocket instance found!');
		return;
	}

	const messageQueue = [];
	const oldOnMessage = webSocket.onmessage;
	webSocket.onmessage = (event) => messageQueue.push(event);
	document.addEventListener('DOMContentLoaded', () =>
	{
		messageQueue.forEach(event => onMessage(event));
		webSocket.onmessage = oldOnMessage;
	});

	const commandQueue = [];
	const oldSendBytes = window.sendBytes;
	window.sendBytes = (command) => commandQueue.push(command);
	const oldOnOpen = webSocket.onopen;
	webSocket.onopen = (event) =>
	{
		window.sendBytes = oldSendBytes;
		commandQueue.forEach(command => window.sendBytes(command));
		return oldOnOpen(event);
	};
}
function isWebSocketScript(script)
{
	return script.src.includes('socket.js');
}
function fixWebSocketScript()
{
	if (!document.head)
	{
		return;
	}

	const scripts = document.head.querySelectorAll('script');
	let found = false;
	for (let i = 0; i < scripts.length; i++)
	{
		if (isWebSocketScript(scripts[i]))
		{
			// does this work?
			scripts[i].onload = webSocketLoaded;
			return;
		}
	}

	// create an observer instance
	const mutationObserver = new MutationObserver((mutationList) =>
	{
		mutationList.forEach((mutation) =>
		{
			if (mutation.addedNodes.length === 0)
			{
				return;
			}

			for (let i = 0; i < mutation.addedNodes.length; i++)
			{
				const node = mutation.addedNodes[i];
				if (node.tagName == 'SCRIPT' && isWebSocketScript(node))
				{
					mutationObserver.disconnect();
					node.onload = webSocketLoaded;
					return;
				}
			}
		});
	});
	mutationObserver.observe(document.head, {
		childList: true
	});
}
fixWebSocketScript();
})();