Surviv.io input replay recorder

Records lightweight game recordings, which can be reviewed with any custom mods applied

// ==UserScript==
// @name         Surviv.io input replay recorder
// @namespace    https://github.com/notKaiAnderson/
// @version      1.0.6
// @description  Records lightweight game recordings, which can be reviewed with any custom mods applied
// @author       garlic
// @match        *://*.resurviv.io/
// @match        *://snake.io/
// @match        *://*.surviv.io/
// @match        *://surviv.io/
// @match        *://surviv2.io/
// @match        *://2dbattleroyale.com/
// @match        *://2dbattleroyale.org/
// @match        *://piearesquared.info/
// @match        *://thecircleisclosing.com/
// @match        *://archimedesofsyracuse.info/
// @match        *://secantsecant.com/
// @match        *://parmainitiative.com/
// @match        *://nevelskoygroup.com/
// @match        *://kugahi.com/
// @match        *://chandlertallowmd.com/
// @match        *://ot38.club/
// @match        *://kugaheavyindustry.com/
// @match        *://drchandlertallow.com/
// @match        *://rarepotato.com/
// @match        *://resurviv.fly.dev/
// @grant        none
// @icon         https://i.imgur.com/jgHdTYA.png
// @license      MIT
// ==/UserScript==

/*** contributors
 * garlic
 * samer kizi
 * preacher
 ***/

var survivharplayerconfig = { silly1: false };
var ifsurviv = false;
if (window.zzpseudoalert && window.log) {
	window.zzpseudoalert("already active");
	return;
}
try {
	document.getElementsByClassName("ftue-header")[0].style.backgroundColor =
		"violet";
	document.getElementsByClassName("ftue-body")[0].style.backgroundColor = "red";
	ifsurviv = true;
} catch (e) {}
window.zzpseudoalert = function (e) {
	var x = document.createElement("div");
	x.style =
		"zIndex:255;position:fixed;left:50%;width:40%;top:20%;height:fit-content;background-color:rgba(0,0,0,.5);color:#e4d338;text-align:center;border-radius:10px;padding:10px 20px;-webkit-transform: translate(-50%, -50%);transform: translate(-50%, -50%);";
	x.innerHTML = e;
	document.body.append(x);
	x.style.fontSize = "20px";
	x.animate([{ opacity: "1" }, { opacity: "0" }], { duration: 3000 });
	setTimeout(() => document.body.removeChild(x), 3000);
};
window.zzpseudoalert.small = function (e) {
	var x = document.createElement("div");
	x.style =
		"zIndex:255;position:fixed;left:50%;width:40%;top:20%;height:fit-content;background-color:rgba(0,0,0,.5);color:#e4d338;text-align:center;border-radius:10px;padding:10px 20px;-webkit-transform: translate(-50%, -50%);transform: translate(-50%, -50%);";
	x.innerHTML = e;
	document.body.append(x);
	x.style.fontSize = "20px";
	x.animate([{ opacity: "1" }, { opacity: "0" }], { duration: 3000 });
	setTimeout(() => document.body.removeChild(x), 3000);
};
zzpseudoalert("wsrapper init");

function arrayBufferToBase64(buffer) {
	let binary = "";
	let bytes = new Uint8Array(buffer);
	let len = bytes.byteLength;
	for (let i = 0; i < len; i++) {
		binary += String.fromCharCode(bytes[i]);
	}
	return window.btoa(binary);
}
window.logentries = [];
window.log = {
	log: {
		version: "1.2",
		creator: {
			name: "wswrapperallium",
			version: "0.0031",
		},
		entries: window.logentries,
	},
};

var lastgameforunlink;

class RecordingWebSocket extends WebSocket {
	/*constructor(e) {
        e=e.replace('wss://','ws://');
        e=e.replace(':449/','/');
		super(e);
	}*/

	unlinkfromclient() {
		if (this.readyState != this.OPEN) return false;

		let seq = 0;
		let wso = this.__lientry._webSocketMessages;

		for (let i = wso.length - 1; i >= 0; i--) {
			if (wso[i].type == "send") {
				let msgab = from_base64s_ab(wso[i].data);
				let msgbytes = new Uint8Array(msgab);
				if (msgbytes[0] == 3) {
					seq = msgbytes[1];
					break;
				}
			}
		}

		var oldsend = this.send;
		Object.defineProperty(this, "send", {
			configurable: true,
			get: function () {
				return () => {};
			},
			set: function () {},
		});
		Object.defineProperty(this, "close", {
			configurable: true,
			get: function () {
				return () => {};
			},
			set: function () {},
		});

		var thisArg = this;
		function keepAlive() {
			seq = (seq + 1) & 255;
			let a;

			if (window.spectatepinginsteadofinput) {
				a = new Uint8Array(4);
				("C90bAA==");
				a[0] = 11;
				a[1] = 0;
				a[2] = 0;
				a[3] = 2;
			} else {
				a = new Uint8Array(9);

				a[0] = 3;
				a[1] = seq;
				a[3] = 255;
				a[4] = 3;
				a[5] = 8;
			}
			oldsend.apply(thisArg, [a]);
			console.info("tick", seq);
		}

		var keepAliveIt = setInterval(() => keepAlive(), 1017);
		console.info("unlink toggled", seq);
		this.onerror = () => {};
		this.onclose();
		this.onclose = () => {
			clearInterval(keepAliveIt);
			console.info("unlinked conn closed");
		};
		this.__spy_onmessage = () => {};
		return true;
	}

	send(...args) {
		let thisArg = this;
		let e = args[0];
		if (thisArg.__lientry == undefined) {
			thisArg.__lientry = {
				startedTime: new Date().toISOString(),
				time: 0,
				request: {
					method: "GET",
					url: thisArg.url,
				},
				_webSocketMessages: [],
			};
			if (ifsurviv) {
				thisArg.__lientry.__survivio = {};
				thisArg.onEach = function () {};
				if (thisArg.url.includes("/play?game")) lastgameforunlink = thisArg;
			}

			window.logentries.push(thisArg.__lientry);
			let wsa = thisArg.__lientry._webSocketMessages;
			let tos = /*thisArg.__lientry.time.toString=  */ () => {
				let t1 = 0;
				if (wsa.length > 1) {
					t1 = wsa.slice(-1)[0].time - wsa[0].time;
				}
				return t1 + "";
			};
			thisArg.__lientry.time = {
				toString: tos,
			};
			zzpseudoalert(thisArg.url.replace("wss://", "").replace("/", "<p>"));
			thisArg.__spy_onmessage = thisArg.onmessage;
			let recvHandler = function (...args) {
				let e = args[0];
				{
					let temp = {
						type: "receive",
						time: new Date().getTime() / 1e3,
						opcode: 2,
					};
					this.__lientry._webSocketMessages.push(temp);
					if (typeof e.data !== "string") {
						temp.opcode = 2;
						new Response(e.data)
							.arrayBuffer()
							.then((ifAblob) => (temp.data = arrayBufferToBase64(ifAblob)));
					} else {
						temp.opcode = 1;
						temp.message = e.data;
					}
					return this.__spy_onmessage.apply(this, args);
				}
			};
			thisArg.onmessage = recvHandler;
		}
		let temp = {
			type: "send",
			time: new Date().getTime() / 1e3,
			opcode: 2,
		};
		thisArg.__lientry._webSocketMessages.push(temp);
		if (typeof e !== "string") {
			temp.opcode = 2;
			temp.data = arrayBufferToBase64(e);
		} else {
			temp.opcode = 1;
			temp.message = e;
		}
		return super.send(...args);
	}
}
RecordingWebSocket.unlink = function () {
	let msg = "";
	if (lastgameforunlink) {
		if (lastgameforunlink.unlinkfromclient()) msg = "unlink function activated";
		else msg = "unlink function not activated";
	} else msg = "nothing";
	window.zzpseudoalert(msg);
};

window.WebSocket = RecordingWebSocket;

void "surviv .har repeat script. new103. open up at surviv.io, load .har file. f1f2f3f6 -- change replay speed";
{
	let cconsole = console.log;
	Object.defineProperty(console, "log", {
		configurable: true,
		get: function () {
			return cconsole;
		},
		set: function (e) {},
	});
}
(function () {
	"use strict";
	let cycle = (x) => (x == false ? undefined : x == undefined);
	let cycle2 = (x, y, z) => (x == y ? z : y);
	const keys = ["F1", "F2", "F3", "F4", "F6", "F7", "F8", "F9", "F10"];
	const disableKey = (key) => keys.push(key);
	["keypress", "keydown", "keyup"].forEach((type) => {
		document.addEventListener(type, (e) => {
			if (keys.indexOf(e.key) !== -1) {
				if (e.type == "keydown") {
					if (e.key == "F1") window.slowmo = 0.33;
					if (e.key == "F2") window.slowmo = 1;
					if (e.key == "F3") window.slowmo = 2;
					if (e.key == "F4") window.slowmo = 2.5;
					if (e.key == "F6") window.slowmo = 16;
					if (e.key == "F6") window.slowmo = 16;
            	    if (e.key == 'F7') window.ovrCeil = !(window.ovrCeil);
            	    if (e.key == 'F8') window.ovrLayer = cycle(window.ovrLayer);
					window.slowmor = window.slowmo;
				}
				return e.preventDefault();
			}
		});
	});
})();

window.slowmo = 1;
window.harRead = {
	files: [],
	games: {},
	alert: window.alert.bind(window),
	ui: { window: window },
};
window.foo = function () {
	function unlinkfromUI() {
		RecordingWebSocket.unlink();
	}

	function tabuntab() {
		if (!xpassblock) return;
		if (document.getElementById("missions-name").style.display === "none") {
			document.getElementById("missions-name").style = "display:block";
			document.getElementById("pass-quest-wrapper").style = "display:block";
		} else {
			document.getElementById("missions-name").style = "display:none";
			document.getElementById("pass-quest-wrapper").style = "display:none";
		}
		if (x.parentElement.id == "pass-block" || x.parentElement.id == "ad-block-left") {
			window.zzpseudoalert("FUG");
			x.otherwindow = window.open("", "", "height=250,width=300");
			x.otherwindow.document.body.append(x);
			window.harRead.ui.window = x.otherwindow;
		} else {
			xpassblock.insertBefore(x, xpassblock.firstElementChild);
			window.harRead.ui.window = window;
			x.otherwindow.close();
		}
	}

	function saveharlog() {
		0,
			(function (filename, data) {
				try {
					var blob = new Blob([data], { type: "text/plain" });
				} catch (e) {
					alert("whoops, doesnt work");
				}

				if (window.navigator.msSaveOrOpenBlob) {
					window.navigator.msSaveBlob(blob, filename);
				} else {
					var elem = window.document.createElement("a");
					elem.href = window.URL.createObjectURL(blob);
					elem.download = filename;
					document.body.appendChild(elem);
					elem.click();
					document.body.removeChild(elem);
				}
			})(
				document.location.host +
					new Date().toISOString().slice(0, 10) +
					Date.now() +
					".har",
				JSON.stringify(log)
			);
	}

    function disableAutoFill() {
        if(survivharplayerconfig.disableAutoFill) {
            if(survivharplayerconfig.xmlovr) return;
            survivharplayerconfig.xmlovr=true;
            let oldsend=XMLHttpRequest.prototype.send;
            XMLHttpRequest.prototype.send = function (...args){
                if(survivharplayerconfig.disableAutoFill
                   && typeof args[0]=="string"
                   && args[0].startsWith('{"version":') ) {
                    /*args[0]=args[0].replace('"autoFill":true','"autoFill":false');*/
                    args[0]=args[0].replace('"region":"eu"','"region":"dev"');
                }
                return oldsend.apply(this,args);
            };
        }
    }

	function selmodech(tt, event, ename) {
		if (ename == "change") {
			if (tt.value == "Tab/untab") {
				('prompt("wha")');
			}
			if (tt.selectedIndex == 5) {
				saveharlog();
				tt.selectedIndex = tt.prevselectedIndex;
				return;
			}
			if (tt.selectedIndex == 6) {
				if (
					(survivharplayerconfig.silly1 = !survivharplayerconfig.silly1) ===
					false
				) {
					dosomething_with_send_disable();
					window.zzpseudoalert("silly pseudo 3d is now: DISABLED");
				} else {
					window.zzpseudoalert("silly pseudo 3d is now: ENABLED");
				}
				tt.selectedIndex = tt.prevselectedIndex;
                return;
			}
            if(tt.selectedIndex == 7) {
				if (
					(survivharplayerconfig.disableAutoFill = !survivharplayerconfig.disableAutoFill) ===
					false
				) {
					disableAutoFill();
					window.zzpseudoalert("no autofill override: DISABLED");
				} else {
					disableAutoFill();
					window.zzpseudoalert("no autofill override: ENABLED");
				}
				tt.selectedIndex = tt.prevselectedIndex;
                return;
            }
			tt.prevselectedIndex = tt.selectedIndex;
			if (tt.selectedIndex == 0) {
				if (window.WebSocket.name == "ReplayWebSocket")
					window.WebSocket = RecordingWebSocket;
			} else {
				if (window.WebSocket.name == "RecordingWebSocket")
					window.WebSocket = ReplayWebSocket;
			}
			if (tt.selectedIndex == 1) {
				applyParsedHarFile(log);
			}
			if (tt.value == "Replay from prompt") {
				let pp = window.harRead.ui.window.prompt(
					"input har file, or http/file address"
				);
				let done = false;
				if (
					pp.startsWith("file://") ||
					pp.startsWith("http") ||
					!pp.startsWith('{"log"')
				) {
					window.zzpseudoalert("trying to fetch file");
					fetch(pp)
						.then((r) => r.json())
						.then((e) => applyParsedHarFile(e));
				} else doa(pp);
				function doa(pp) {
					if (pp.startsWith('{"log"')) {
						try {
							let yi = JSON.parse(pp);
							applyParsedHarFile(yi);
						} catch (e) {
							window.zzpseudoalert("could not read/parse... failed");
						}
					}
				}
			}
		}
	}

    var xpassblock = document.getElementById("pass-block");
    if(xpassblock.parentElement.id == 'pass-wrapper') xpassblock=(document.getElementById("ad-block-left"));
	("var ifsurviv=true");
	var xwin, x;
	if (!xpassblock) {
		xwin = window.open("", "");
		x = xwin.document.createElement("div");
		xwin.document.body.append(x);
		("ifsurviv=false");
	} else {
		x = document.createElement("div");
		xpassblock.insertBefore(x, xpassblock.firstElementChild);
	}

	var mn = document.createElement("div");
	mn.style = "display:inline-flex;width:100%";
	x.insertBefore(mn, x.firstElementChild);

	var tt = document.createElement("select");
	tt.id = "modepl";
	tt.style = `
      background: rgb(122, 122, 122);
      box-shadow: rgb(62 62 62) 0px -3px inset;
      color: rgb(255, 255, 255);
      cursor: pointer;
      width: 100%;
      border: none;
      border-radius: 5px;
      font-size: 18px;
      padding: 10px 20px;
      text-align: center;
    `;
	tt.innerHTML =
		"<option>Record</option><option>Replay recent</option><option>Replay from file</option><option>Replay from prompt</option><option>Tab/untab</option><option>save har log</option><option>silly 3d toggle</option><option>disable autofill toggle</option>";
	let captt = tt;
	tt.onchange = (e) => selmodech(captt, e, "change");
	tt.onclick = (e) => selmodech(captt, e, "click");
	x.appendChild(tt);

	var tt = document.createElement("button");
	tt.style = `
      background-image: url(https://surviv.io/img/gui/link.svg);
      background-size: 27px;
      background-repeat: no-repeat;
      background-position: center 42%;
      width: 100%;
      height: 45px;
      background-color: #cd3232;
      box-shadow: #781e0a 0px -4px inset;
      color: #fff;
      cursor: pointer;
      text-shadow: rgb(0 0 0 / 25%) 0px 1px 2px;
      border: none;
      border-radius: 5px;
      font-size: 18px;
      margin: 0 3px 0 0;
    `;
	tt.onclick = tabuntab;
	mn.appendChild(tt);

	var tt = document.createElement("button");
	tt.innerHTML = "unlink & record";
	tt.style = `
      height: 45px;
      background-color: rgb(205, 50, 50);
      box-shadow: rgb(120 30 10) 0px -4px inset;
      color: rgb(255, 255, 255);
      cursor: pointer;
      text-shadow: rgb(0 0 0 / 25%) 0px 1px 2px;
      border: none;
      border-radius: 5px;
      font-size: 18px;
      width:300%;
    `;
	tt.onclick = unlinkfromUI;
	mn.appendChild(tt);

	var tt = document.createElement("select");
	tt.id = "xyz";
	window.harRead.ui.matchSelectorComboBox = tt;
	tt.style =
		"background: rgb(122, 122, 122); box-shadow: rgb(62, 62, 62) 0px -2px inset; color: rgb(255, 255, 255); cursor: pointer; width: 100%; border: none; border-radius: 5px; font-size: 18px; padding: 5px 20px; margin: 5px 0px;";
	tt.style = `
      width: 100%;
      height: 45px;
      background-color: #7a7a7a;
      box-shadow: #3e3e3e 0px -4px inset;
      color: #fff;
      cursor: pointer;
      text-shadow: rgb(0 0 0 / 25%) 0px 1px 2px;
      border: none;
      border-radius: 5px;
      font-size: 18px;
      margin: 3px 0 3px 0;
    `;
	x.appendChild(tt);
	var t = document.createElement("input");
	t.type = "file";
	t.style = `
      position: absolute;
      margin: 62px 0 0 -126px;
      opacity: 0;
      cursor: pointer;
      transform: scale(2);
    `;
	t.multiple = true;
	t.accept = ".har";
	t.id = "ssk";
	x.appendChild(t);
	if (ifsurviv) {
		document.getElementById("missions-name").style = "display:none";
		document.getElementById("pass-quest-wrapper").style = "display:none";
	}
	var skk = document.createElement("button");
	skk.id = "skk";
	skk.onclick = function () {
		document.getElementById("ssk").click();
	};
	skk.innerHTML = "Choose file";
	skk.style =
		"background: rgb(30, 144, 255);box-shadow: rgb(24, 113, 200) 0px -5px inset;color: rgb(255, 255, 255);cursor: pointer;text-shadow: rgba(0, 0, 0, 0.25) 0px 1px 2px;font-weight: 700;width: 100%;border: none;border-radius: 5px;padding: 12px 20px;font-size: 18px;";
	x.appendChild(skk);
	tt.onchange = function () {
		harRead.selected = tt.value;
	};

	let applySVGtext = function (nam, jo) {
		let ii = nam.indexOf("---");
		if (ii >= 0) nam = nam.slice(0, ii);
		let blob1 = new Blob([jo], {
			type: "image/svg+xml",
		});
		let url1 = URL.createObjectURL(blob1);
		nam = nam.replace(".svg", ".img");
		PIXI.utils.TextureCache[nam] = PIXI.Texture.fromImage(
			url1,
			undefined,
			undefined,
			2
		);
	};
	let applyParsedHarFile = function (jo) {
		window.harRead.games = {};

		window.harRead.files.push(jo);
		tt.innerHTML = "";
		if (1) {
			var ectr = 0;
			jo.log.entries.forEach((x) => {
				if (x.request.url.slice(0, 2) == "ws") {
					if (x._webSocketMessages.length < 2) return;
					var time =
						x._webSocketMessages.slice(-1)[0].time -
						x._webSocketMessages[0].time;
					time =
						((time / 60) | 0) +
						":" +
						String((time | 0) % 60).padStart(2, "0") +
						" ";
					console.log(time, x._webSocketMessages.length, x.request.url);
					var ww = x.request.url;
					var w0 = ww.lastIndexOf("=");
					var wi = ww.lastIndexOf("=");

					if (w0 >= 0 || wi >= 0 || ww.includes(".snake.io:9092")) {
						var output = time;
						if (w0 >= 0 || wi >= 0)
							output += " " + ww.slice(6, 6 + 9) + ww.slice(wi);
						var qi = document.createElement("option");
						qi.value = ectr;
						qi.innerHTML = output + "";
						tt.appendChild(qi);
						harRead.games[ectr] = x;
					}
				}
				ectr += 1;
			});
		}
	};
	t.onchange = function handleFileSelect1(evt) {
		var filesreset = false;
		tt.innerHTML = "<option>loading...</option>";
		var files = evt.target.files;
		var output = [];
		for (var i = 0, f; (f = files[i]); i++) {
			if (f.name.slice(-4) == ".har" && !filesreset) {
				window.harRead.files = [];
				window.harRead.games = {};
				filesreset = true;
			}
			var fr = new FileReader();
			harRead.files.push(f);
			if (f.name.slice(-4) == ".har")
				fr.onload = function (evt) {
					if (evt.loaded == evt.total) {
						tt.innerHTML = "<option>parsing...</option>";
						try {
							var jo = JSON.parse(evt.currentTarget.result);
						} catch (e) {
							harRead.alert(
								"This file is not a valid .har file, or gabled/truncated"
							);
						}
						applyParsedHarFile(jo);
					}
				};
			else if (f.name.slice(-4) == ".svg") {
				let fname_temp = f.name;
				fr.onload = function (evt) {
					if (evt.loaded == evt.total) {
						applySVGtext(fname_temp, evt.currentTarget.result);
					}
				};
			} else
				harRead.alert(
					"I do not know what to do with " +
						f.name +
						"\nI expect .har or .svg files"
				);
			fr.readAsText(f);
		}
		console.log(output);
	};
};
if (document.readyState === "complete" || document.readyState === "interactive")
	foo();
else window.addEventListener("DOMContentLoaded", foo);

function dosomething_with_send_disable() {
	cvs.style.transform = "";
}

function dosomething_with_send(e) {
	if (survivharplayerconfig.silly1 != true) return;

	let survivOneEps = 1.0001;
	function delerpOne10(e) {
		return (-1 + 2 * (e / 1023)) * survivOneEps;
	}

	let x = from_base64s(e);
	console.info(x);
	try {
		let i = 3 + 3 * !!(x[2] & 0x80);
		let xdiv, ydiv;
		xdiv = x[i] + (x[i + 1] & 3) * 256;
		ydiv = (x[i + 1] >> 2) + (x[i + 2] & 15) * 64;
		xdiv = delerpOne10(xdiv);
		ydiv = delerpOne10(ydiv);

		let az = Math.atan2(xdiv, ydiv);
		let cs = Math.cos(az);
		let sn = Math.sin(az);
		let pz = 0.9;
		let cz = Math.cos(pz);
		let zn = Math.sin(pz);
		cvs.style.transform = `perspective(800px) matrix3d(${cs}, ${-sn * cz}, ${
			-sn * zn
		}, 0,  ${sn}, ${cs * cz}, ${
			cs * zn
		}, 0, 0, ${-zn}, ${cz}, 0, 0, 0, 0  , 1)`;
	} catch (e) {
		console.error(e);
	}
}

function from_base64s(base64) {
	var raw = atob(base64);
	var rawLength = raw.length;
	var array = new Uint8Array(new ArrayBuffer(rawLength));
	for (let i = 0; i < rawLength; i++) {
		array[i] = raw.charCodeAt(i);
	}
	return array;
}

function from_base64s_ab(base64) {
	var raw = atob(base64);
	var rawLength = raw.length;
	var arrayb = new ArrayBuffer(rawLength);
	var array = new Uint8Array(arrayb);
	for (let i = 0; i < rawLength; i++) {
		array[i] = raw.charCodeAt(i);
	}
	return arrayb;
}
if (1 && "weboverload") {
	("var RecordingWebSocket");
	var oldWebSocket = window.WebSocket;
	var ReplayWebSocket = function (a) {
		console.info("ws creating:", a);
		if (a) a = a.replace("wss://", "ws://");
		this.sndwo = 0;
		if (a.indexOf("-p1.surviv.io") >= 0) {
			this.binaryType = "blob";
			this.stupidsum = this.static_counter + 4;
			this.static_counter = (this.static_counter + 3) % 19;
			this.url = a;
			this.__type = "echo";
			setTimeout(() => this.__onopen(), 100);
		} else if (a.includes("?gameId=") || a.includes(".snake.io:9092")) {
			this.__type = "game";
			this.wsh = harRead.games[window.harRead.ui.matchSelectorComboBox.value];
			this.wso = this.wsh["_webSocketMessages"];
			this.wslen = this.wso.length;
			this.wsi = 0;
			this.binaryType = "blob";
			this.url = a;
			setTimeout(() => this.__onopen(), 100);
		}
	};
	ReplayWebSocket.name = "ReplayWebSocket";
	ReplayWebSocket.prototype = {
		OPEN: 1,
		CLOSED: 3,
		static_counter: 10,
		__onopen: function () {
			this.readyState = this.OPEN;
			this.onopen && this.onopen();
		},
		__onclose: function () {
			this.readyState = this.CLOSED;
			this.onclose && this.onclose();
		},
		__onmessage: function () {
			if (this.readyState != this.OPEN) return;
			this.onmessage &&
				this.onmessage({
					data: new Uint8Array(1),
				});
		},
		__onharmessage: function (e) {
			if (this.readyState != this.OPEN || !this.onmessage) return;
			try {
				this.onmessage(this.wsmsg);
			} catch (e) {
				console.error("onharmessage error");
				console.error(this.wsi);
				console.error(this.wsmsg);
				console.error(e);
			}
			if (this.nextmsg()) {
			} else "todo:close";
		},
		nextmsg: function () {
			while (++this.wsi < this.wslen) {
				if (this.wso[this.wsi].type == "receive") break;
				else if (this.wso[this.wsi].type == "send") {
					dosomething_with_send(this.wso[this.wsi].data);
				}
			}
			if (this.wsi >= this.wslen) return false;
			var a1 = this.wso[this.wsi];
			var datat = a1.data;
			this.wsmsg = {
				data: from_base64s_ab(datat),
			};
			time =
				this.wso[this.wsi].time * 1000 +
				this.__timediff -
				new Date().getTime() * slowmo;
			if (time < -1000 || time > 4000) {
				this.__timediff =
					30 - this.wso[this.wsi].time * 1000 + new Date().getTime() * slowmo;
				time = 30;
			}
			if (time > 1) setTimeout(() => this.__onharmessage(), time);
			else this.__onharmessage();
			return true;
		},
		send: function (a) {
			this.sndwo = 0;
			if (this.__type == "echo")
				setTimeout(() => this.__onmessage(), this.stupidsum);
			if (this.__type == "game")
				if (this.__starttime == undefined) {
					this.__starttime = new Date().getTime() + 100;
					this.__timediff = this.__starttime * slowmo - this.wso[0].time * 1000;
					this.nextmsg();
				}
		},
		close: function () {
			this.readyState = this.CLOSED;
		},
	};
	("window.WebSocket = ReplayWebSocket");
}

let oraf=window.requestAnimationFrame;
let wast=undefined;

window.requestAnimationFrame=function(user) {
	let temp=user;
	function mymethod(x) {
		if(wast==undefined) { wast=x; return user.call(this,x); }
		let tea=(window.slowmo??1);
		if(window.WebSocket.name!="ReplayWebSocket") tea=1;

		return user.call(this,/*(x-wast)*(window.slowmo??1) + wast*/(x-wast)*tea );
	};
	oraf.call(this,mymethod);
};