359 lines
27 KiB
JavaScript
359 lines
27 KiB
JavaScript
|
// ==UserScript==
|
||
|
// @name Terminal 00 Multiuser
|
||
|
// @namespace https://*.angusnicneven.com/*
|
||
|
// @version 20240418.1
|
||
|
// @description Probe with friends!
|
||
|
// @author tgpholly
|
||
|
// @match https://*.angusnicneven.com/*
|
||
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=angusnicneven.com
|
||
|
// @grant none
|
||
|
// ==/UserScript==
|
||
|
|
||
|
let continueRunningScript = true;
|
||
|
if (!window.TE_ACTIVE) {
|
||
|
if (window.location.href.includes("www.")) {
|
||
|
window.location.href = window.location.href.replace("www.", "");
|
||
|
continueRunningScript = false;
|
||
|
}
|
||
|
|
||
|
let didWeNukeIt = false;
|
||
|
function nukeCC() {
|
||
|
if (didWeNukeIt) {
|
||
|
return;
|
||
|
}
|
||
|
try {
|
||
|
const doWeHaveCC = document.getElementById("cursor-container");
|
||
|
if (doWeHaveCC) {
|
||
|
doWeHaveCC.remove();
|
||
|
didWeNukeIt = true;
|
||
|
}
|
||
|
} catch (e) {}
|
||
|
}
|
||
|
setInterval(nukeCC, 1000);
|
||
|
}
|
||
|
|
||
|
(function() {
|
||
|
'use strict';
|
||
|
|
||
|
if (!continueRunningScript) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// This is a minified version of the code at https://git.eusv.net/tgpholly/bufferStuff/releases/tag/1.5.0 + FunkyArray
|
||
|
class BrowserBuffer{constructor(dataOrSize){if(typeof dataOrSize==="number"){this.buffer=new Uint8Array(dataOrSize)}else if(typeof dataOrSize==="object"){this.buffer=new Uint8Array(dataOrSize)}else{this.buffer=new Uint8Array(0)}this.dataView=new DataView(this.buffer.buffer)}static allocUnsafe(size){return this.alloc(size)}static allocUnsafeSlow(size){return this.alloc(size)}static alloc(size){return new BrowserBuffer(size)}static concat(buffers,totalLength){let joinedArrays;if(totalLength!==undefined){joinedArrays=new Uint8Array(totalLength)}else{let arraysLength=0;for(const buffer of buffers){arraysLength+=buffer.length}joinedArrays=new Uint8Array(arraysLength)}let offset=0;for(const buffer of buffers){joinedArrays.set(buffer.buffer,offset);offset+=buffer.length}return new BrowserBuffer(joinedArrays)}static from(data){if(typeof data==="string"){throw new Error("BrowserBuffer does not currently support creating buffers from strings.")}return new BrowserBuffer(data)}static of(){}static isBuffer(){}static isEncoding(){}static byteLength(){}static copyBytesFrom(){}static compare(){}get length(){return this.buffer.length}checkRanged(value,valueName,lowRange,highRange){if(value<lowRange||value>highRange){throw new Error(`The value of "${valueName}" is out of range. It must be >= ${lowRange} and <= ${highRange}. Received ${value}`)}}checkValue(value,low,high){this.checkRanged(value,"value",low,high)}checkOffset(offset){if(offset){this.checkRanged(offset,"offset",0,this.buffer.length-1)}}writeInt8(value,offset){this.checkValue(value,-128,127);this.checkOffset(offset);this.dataView.setInt8(offset,value);return this}writeUInt8(value,offset){this.checkValue(value,0,255);this.checkOffset(offset);this.dataView.setUint8(offset,value);return this}writeInt16LE(value,offset){this.checkValue(value,-32768,32767);this.checkOffset(offset);this.dataView.setInt16(offset,value,true);return this}writeUInt16LE(value,offset){this.checkValue(value,0,65535);this.checkOffset(offset);this.dataView.setUint16(offset,value,true);return this}writeInt32LE(value,offset){this.checkValue(value,-2147483648,2147483647);this.checkOffset(offset);this.dataView.setInt32(offset,value,true);return this}writeUInt32LE(value,offset){this.checkValue(value,0,4294967295);this.checkOffset(offset);this.dataView.setUint32(offset,value,true);return this}writeBigInt64LE(value,offset){if(typeof value==="number"){value=BigInt(value)}this.checkValue(value,-9223372036854775808n,9223372036854775808n);this.checkOffset(offset);this.dataView.setBigInt64(offset,value,true);return this}writeBigUint64LE(value,offset){if(typeof value==="number"){value=BigInt(value)}this.checkValue(value,0n,18446744073709551616n);this.checkOffset(offset);this.dataView.setBigUint64(offset,value,true);return this}writeFloatLE(value,offset){this.checkOffset(offset);this.dataView.setFloat32(offset,value,true);return this}writeDoubleLE(value,offset){this.checkOffset(offset);this.dataView.setFloat64(offset,value,true);return this}writeInt16BE(value,offset){this.checkValue(value,-32768,32767);this.checkOffset(offset);this.dataView.setInt16(offset,value,false);return this}writeUInt16BE(value,offset){this.checkValue(value,0,65535);this.checkOffset(offset);this.dataView.setUint16(offset,value,false);return this}writeInt32BE(value,offset){this.checkValue(value,-2147483648,2147483647);this.checkOffset(offset);this.dataView.setInt32(offset,value,false);return this}writeUInt32BE(value,offset){this.checkValue(value,0,4294967295);this.checkOffset(offset);this.dataView.setUint32(offset,value,false);return this}writeBigInt64BE(value,offset){if(typeof value==="number"){value=BigInt(value)}this.checkValue(value,-(2n**63n),2n**63n);this.checkOffset(offset);this.dataView.setBigInt64(offset,value,false);return this}writeBigUint64BE(value,offset){if(typeof value==="number"){value=BigInt(value)}this.checkValue(value,0n,2n**64n);this.checkOffset(offset);this.dataView.setBigUint64(offset,value,false);return this}writeFloatBE(value,offset){this.checkOffset(offset);this.dataView.setFloat32(offset,value,false);return this}writeDouble
|
||
|
|
||
|
const MessageType = {
|
||
|
KeepAlive: 0,
|
||
|
ClientDetails: 1,
|
||
|
CursorPos: 2,
|
||
|
ClientJoined: 3,
|
||
|
Clients: 4,
|
||
|
ClientLeft: 5,
|
||
|
Ping: 6
|
||
|
};
|
||
|
|
||
|
const styles = document.createElement("style");
|
||
|
styles.innerHTML = `
|
||
|
#otherCursors {
|
||
|
position: absolute;
|
||
|
top:0px;
|
||
|
left:0px;
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
z-index: 999999999;
|
||
|
pointer-events: none;
|
||
|
overflow:hidden;
|
||
|
}
|
||
|
|
||
|
@keyframes ping {
|
||
|
0% {
|
||
|
scale: 0;
|
||
|
opacity: 1;
|
||
|
}
|
||
|
|
||
|
100% {
|
||
|
scale: 2;
|
||
|
opacity: 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
.ping {
|
||
|
opacity: 0;
|
||
|
position: absolute;
|
||
|
animation: ping 2s linear;
|
||
|
animation-iteration-count: 1;
|
||
|
image-rendering: pixelated;
|
||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAXVJREFUeJztm8ESwyAIRLX//8/2EjKKmhhFUeDN9NbK7kIybaPerSU0vs9PVbGwUGI4hDb/3meypumcsfDtEhsuGCsv8Pw5Us2UiwXnUvEDnaxOTrQmiXaKRRLjk7qVTRVVEKMCA7WglppX4bjusptmLCJc7uF1pIae5HDXl6ePWKqHu+s18DTMKTK7AAHTNJ5gHiDXepJ5gEzzieaBYe0nmwdePfwWijkKCd0HPnuRZB6oerJLACGx+0DRm01AhOTuA5lH9ROgPgBAw/gDiVf1E2ABcAvYAU3XP3B7Vj8BFgC3AG4sAG4B3FgA3AK4sQC4BeyCpm+D9mswxgLgFrATGu4D9qcoRn0AGMmXgT0ZakXiFNjT4a9ImoJuLxJCsD1Co5w8Bar3CqreLap6v/DSHeM7BdG9Xd4OTPQWdfzTwHZkJhGB0qdY87XmVTiu213Tjs2NfBiRBOGcnoOTmKxbdzHhR2dLVDv5xOmHp59ovVMv0/UHCfuPGCq5foYAAAAASUVORK5CYII=");
|
||
|
width: 64px;
|
||
|
height: 64px;
|
||
|
}
|
||
|
`.split("\n").join("").split("\r").join("").split("\t").join("");
|
||
|
document.head.appendChild(styles);
|
||
|
|
||
|
const otherCursors = document.createElement("div");
|
||
|
otherCursors.id = "otherCursors";
|
||
|
document.body.appendChild(otherCursors);
|
||
|
|
||
|
const keepAlivePacket = createWriter(Endian.LE, 1).writeByte(MessageType.KeepAlive).toBuffer();
|
||
|
|
||
|
let remoteClients = new FunkyArray();
|
||
|
|
||
|
let ws;
|
||
|
let ready = false;
|
||
|
let currentMouseX = 0;
|
||
|
let currentMouseY = 0;
|
||
|
let oldMouseX = 0;
|
||
|
let oldMouseY = 0;
|
||
|
let lastSendTime = 0;
|
||
|
let username = "";
|
||
|
if (localStorage["t00mp_username"]) {
|
||
|
username = localStorage["t00mp_username"];
|
||
|
}
|
||
|
|
||
|
class RemoteClient {
|
||
|
constructor(name) {
|
||
|
this.name = name;
|
||
|
this.element = document.createElement("div");
|
||
|
this.element.style.position = "absolute";
|
||
|
this.element.style.transform = `translate(-50%, -50%)`;
|
||
|
const image = new Image();
|
||
|
let cursorImage = window.getComputedStyle(document.body).cursor.replace("url(","").split('"').join("").split("'").join("").split(")")[0];
|
||
|
if (cursorImage === "auto") {
|
||
|
cursorImage = "https://angusnicneven.com/cursor/rrw.png";
|
||
|
}
|
||
|
image.src = cursorImage;
|
||
|
this.element.appendChild(image);
|
||
|
const clientName = document.createElement("div");
|
||
|
clientName.style = "position:absolute;left:100%;top:100%;background-color:black;padding:4px 8px;color:white;font-size:12px";
|
||
|
clientName.innerText = name;
|
||
|
this.element.appendChild(clientName);
|
||
|
otherCursors.appendChild(this.element);
|
||
|
this.targetX = 0;
|
||
|
this.targetY = 0;
|
||
|
this.actualX = 0;
|
||
|
this.actualY = 0;
|
||
|
this.oldActualX = 0;
|
||
|
this.oldActualY = 0;
|
||
|
}
|
||
|
|
||
|
rawSetPos(x, y) {
|
||
|
this.targetX = Math.round(x * window.innerWidth);
|
||
|
this.targetY = y;
|
||
|
}
|
||
|
|
||
|
updateCursor() {
|
||
|
const x = Math.round(this.actualX);
|
||
|
const y = Math.round(this.actualY);
|
||
|
if (y !== this.oldActualY) {
|
||
|
this.element.style.top = `${y}px`;
|
||
|
this.oldActualY = y;
|
||
|
}
|
||
|
if (x !== this.oldActualX) {
|
||
|
this.element.style.left = `${x}px`;
|
||
|
this.oldActualX = x;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createPing(x, y) {
|
||
|
const pingDiv = document.createElement("div");
|
||
|
pingDiv.className = "ping";
|
||
|
pingDiv.style.top = `${y}px`;
|
||
|
pingDiv.style.left = `${x}px`;
|
||
|
otherCursors.appendChild(pingDiv);
|
||
|
setTimeout(() => pingDiv.remove(), 1999);
|
||
|
}
|
||
|
|
||
|
function removeClient(id) {
|
||
|
if (remoteClients.has(id)) {
|
||
|
const client = remoteClients.get(id);
|
||
|
remoteClients.remove(id);
|
||
|
client.element.remove();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let rawMouseX = 0;
|
||
|
let rawMouseY = 0;
|
||
|
|
||
|
window.onmousemove = (e) => {
|
||
|
currentMouseX = (rawMouseX = e.clientX) + document.body.scrollLeft;
|
||
|
currentMouseY = (rawMouseY = e.clientY) + document.body.scrollTop;
|
||
|
}
|
||
|
|
||
|
window.onscroll = () => {
|
||
|
currentMouseX = rawMouseX + document.body.scrollLeft;
|
||
|
currentMouseY = rawMouseY + document.body.scrollTop;
|
||
|
}
|
||
|
|
||
|
window.onkeypress = (e) => {
|
||
|
if (e.key === "p") {
|
||
|
if (ws && ready) {
|
||
|
ws.send(createWriter(Endian.LE, 9).writeByte(MessageType.Ping).writeFloat((rawMouseX + document.body.scrollLeft - 32) / window.innerWidth).writeInt(rawMouseY + document.body.scrollTop - 32).toBuffer());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function lerp(value1, value2, amount) {
|
||
|
return value1 + (value2 - value1) * amount;
|
||
|
}
|
||
|
|
||
|
let timeLastFrame = performance.now();
|
||
|
let frameDeltaTime = 0;
|
||
|
function animate() {
|
||
|
frameDeltaTime = (performance.now() - timeLastFrame) * 0.001;
|
||
|
|
||
|
if (ws && ready) {
|
||
|
if (currentMouseX !== oldMouseX || currentMouseY !== oldMouseY) {
|
||
|
if ((performance.now() - lastSendTime) >= 41) {
|
||
|
lastSendTime = performance.now();
|
||
|
oldMouseX = currentMouseX;
|
||
|
oldMouseY = currentMouseY;
|
||
|
ws.send(createWriter(Endian.LE, 9).writeByte(MessageType.CursorPos).writeFloat(oldMouseX / window.innerWidth).writeInt(currentMouseY).toBuffer());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
remoteClients.forEach(remoteUser => {
|
||
|
remoteUser.actualX = lerp(remoteUser.actualX, remoteUser.targetX, 20 * frameDeltaTime);
|
||
|
remoteUser.actualY = lerp(remoteUser.actualY, remoteUser.targetY, 20 * frameDeltaTime);
|
||
|
remoteUser.updateCursor();
|
||
|
});
|
||
|
|
||
|
requestAnimationFrame(animate);
|
||
|
timeLastFrame = performance.now();
|
||
|
}
|
||
|
animate();
|
||
|
|
||
|
function doConnect() {
|
||
|
const Buffer = getBufferClass();
|
||
|
|
||
|
ws = new WebSocket("wss://ws.eusv.net/t00mp");
|
||
|
let keepAliveInterval;
|
||
|
ws.onopen = () => {
|
||
|
const currentPage = window.location.href.split("/").slice(3).join("/");
|
||
|
ws.send(createWriter(Endian.LE, 4 + username.length + currentPage.length).writeByte(MessageType.ClientDetails).writeShortString(username).writeString(currentPage).toBuffer());
|
||
|
keepAliveInterval = setInterval(() => {
|
||
|
ws.send(keepAlivePacket);
|
||
|
}, 5000);
|
||
|
}
|
||
|
|
||
|
function onMessage(buf) {
|
||
|
const reader = createReader(Endian.LE, Buffer.from(buf));
|
||
|
switch (reader.readByte()) {
|
||
|
case MessageType.Clients:
|
||
|
{
|
||
|
const clientCount = reader.readUShort();
|
||
|
for (let i = 0; i < clientCount; i++) {
|
||
|
const clientId = reader.readUInt();
|
||
|
const clientName = reader.readShortString();
|
||
|
remoteClients.set(clientId, new RemoteClient(clientName));
|
||
|
}
|
||
|
ready = true;
|
||
|
break;
|
||
|
}
|
||
|
case MessageType.ClientJoined:
|
||
|
{
|
||
|
const clientId = reader.readUInt();
|
||
|
const clientName = reader.readShortString();
|
||
|
remoteClients.set(clientId, new RemoteClient(clientName));
|
||
|
break;
|
||
|
}
|
||
|
case MessageType.CursorPos:
|
||
|
{
|
||
|
const clientId = reader.readUInt();
|
||
|
if (remoteClients.has(clientId)) {
|
||
|
const cursorX = reader.readFloat();
|
||
|
const cursorY = reader.readInt();
|
||
|
remoteClients.get(clientId).rawSetPos(cursorX, cursorY);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case MessageType.ClientLeft:
|
||
|
{
|
||
|
const clientId = reader.readUInt();
|
||
|
removeClient(clientId);
|
||
|
}
|
||
|
case MessageType.Ping:
|
||
|
{
|
||
|
const cursorX = reader.readFloat();
|
||
|
const cursorY = reader.readInt();
|
||
|
createPing(cursorX * window.innerWidth, cursorY);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ws.onmessage = (e) => {
|
||
|
e.data.arrayBuffer().then(onMessage);
|
||
|
}
|
||
|
|
||
|
function onCloseAndError() {
|
||
|
if (keepAliveInterval) {
|
||
|
clearInterval(keepAliveInterval);
|
||
|
keepAliveInterval = undefined;
|
||
|
}
|
||
|
ws = undefined;
|
||
|
ready = false;
|
||
|
setTimeout(doConnect, 5000);
|
||
|
}
|
||
|
ws.onclose = onCloseAndError;
|
||
|
ws.onerror = onCloseAndError;
|
||
|
}
|
||
|
|
||
|
function createOnlineDialog() {
|
||
|
const bg = document.createElement("div");
|
||
|
bg.style = "z-index:1000000000;position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:rgba(0,0,0,0.5)";
|
||
|
const dialog = document.createElement("div");
|
||
|
dialog.style = "position:absolute;top:50%;left:50%;width:15rem;height:9rem;background-color:#343434;transform:translate(-50%,-50%);text-align:center;color:white";
|
||
|
bg.appendChild(dialog);
|
||
|
const title = document.createElement("h4");
|
||
|
title.innerText = "T00 MultiUser";
|
||
|
dialog.appendChild(title);
|
||
|
const submitFunction = (event) => {
|
||
|
// Jank
|
||
|
if (event && event.keyCode !== 13) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (username.value.trim() === "") {
|
||
|
alert("Username must be valid");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
localStorage["t00mp_username"] = username.value;
|
||
|
bg.remove();
|
||
|
window.location.href = window.location.href;
|
||
|
};
|
||
|
const username = document.createElement("input");
|
||
|
username.placeholder = "Enter Username";
|
||
|
username.maxLength = 32;
|
||
|
username.style.width = "12rem";
|
||
|
username.onkeypress = submitFunction;
|
||
|
if (localStorage["t00mp_username"]) {
|
||
|
username.value = localStorage["t00mp_username"];
|
||
|
}
|
||
|
dialog.appendChild(username);
|
||
|
const buttons = document.createElement("div");
|
||
|
buttons.style.marginTop = "1rem";
|
||
|
dialog.appendChild(buttons);
|
||
|
const submitButton = document.createElement("button");
|
||
|
submitButton.innerText = "Connect";
|
||
|
submitButton.onclick = () => submitFunction(null);
|
||
|
buttons.appendChild(submitButton);
|
||
|
const doNotButton = document.createElement("button");
|
||
|
doNotButton.innerText = "Close";
|
||
|
doNotButton.onclick = () => bg.remove();
|
||
|
doNotButton.style.marginLeft = "1rem";
|
||
|
buttons.appendChild(doNotButton);
|
||
|
document.body.appendChild(bg);
|
||
|
}
|
||
|
|
||
|
const openMenuButton = document.createElement("button");
|
||
|
openMenuButton.style = "opacity:0.25;position:fixed;top:0px;right:0px;z-index:9999999;margin:4px;background-color:black;color:white;border:1px solid white";
|
||
|
openMenuButton.innerText = "MultiUser Menu";
|
||
|
openMenuButton.onclick = () => createOnlineDialog();
|
||
|
document.body.appendChild(openMenuButton);
|
||
|
|
||
|
if (username !== "") {
|
||
|
doConnect();
|
||
|
}
|
||
|
})();
|