Greasy Fork is available in English.

Sploop.io Socket API

Provides a basic understanding of how sploop handles websockets, lets connect a bot via console

// ==UserScript==
// @name Sploop.io Socket API
// @author Murka
// @description Provides a basic understanding of how sploop handles websockets, lets connect a bot via console
// @icon https://sploop.io/img/ui/favicon.png
// @version 1.0
// @match *://sploop.io/
// @run-at document-start
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/919633
// ==/UserScript==
/* jshint esversion:6 */

/*
    Author: Murka
    Github: https://github.com/Murka007
    Greasyfork: https://greasyfork.org/users/919633
    Discord: https://discord.gg/cPRFdcZkeD

    Connect a bot via console using `SocketAPI.createClient();`
*/

(function() {
    "use strict";

    const log = console.log;

    localStorage.removeItem("_adIds");
    const getAngle = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1);
    const hashing = {
        9303: function(t, n) {
            t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]];
            n = [n[0] >>> 16, 65535 & n[0], n[1] >>> 16, 65535 & n[1]];
            const i = [0, 0, 0, 0];
            i[3] += t[3] + n[3];
            i[2] += i[3] >>> 16;
            i[3] &= 65535;

            i[2] += t[2] + n[2];
            i[1] += i[2] >>> 16;
            i[2] &= 65535;

            i[1] += t[1] + n[1];
            i[0] += i[1] >>> 16;
            i[1] &= 65535;

            i[0] += t[0] + n[0];
            i[0] &= 65535;
            return [i[0] << 16 | i[1], i[2] << 16 | i[3]];
        },

        3235: function(t, n) {
            t = [t[0] >>> 16, 65535 & t[0], t[1] >>> 16, 65535 & t[1]];
            n = [n[0] >>> 16, 65535 & n[0], n[1] >>> 16, 65535 & n[1]];
            const i = [0, 0, 0, 0];
            i[3] += t[3] * n[3];
            i[2] += i[3] >>> 16;
            i[3] &= 65535;

            i[2] += t[2] * n[3];
            i[1] += i[2] >>> 16;
            i[2] &= 65535;

            i[2] += t[3] * n[2];
            i[1] += i[2] >>> 16;
            i[2] &= 65535;

            i[1] += t[1] * n[3];
            i[0] += i[1] >>> 16;
            i[1] &= 65535;

            i[1] += t[2] * n[2];
            i[0] += i[1] >>> 16;
            i[1] &= 65535;

            i[1] += t[3] * n[1];
            i[0] += i[1] >>> 16;
            i[1] &= 65535;

            i[0] += t[0] * n[3] + t[1] * n[2] + t[2] * n[1] + t[3] * n[0];
            i[0] &= 65535
            return [i[0] << 16 | i[1], i[2] << 16 | i[3]];
        },

        9869: function(t, n) {
            n %= 64;
            if (32 == n) return [t[1], t[0]];
            if (n < 32) return [t[0] << n | t[1] >>> 32 - n, t[1] << n | t[0] >>> 32 - n];

            n -= 32;
            return [t[1] << n | t[0] >>> 32 - n, t[0] << n | t[1] >>> 32 - n];
        },

        1318: function(t, n) {
            n %= 64;
            if (0 == n) return t;
            if (n < 32) return [t[0] << n | t[1] >>> 32 - n, t[1] << n];
            return [t[1] << n - 32, 0];
        },

        6217: function(t, n) {
            return [t[0] ^ n[0], t[1] ^ n[1]];
        },

        1552: function(t) {
            const o = hashing[3235];
            const e = hashing[6217];
            t = e(t, [0, t[0] >>> 1]);
            t = o(t, [4283543511, 3981806797]);
            t = e(t, [0, t[0] >>> 1]);
            t = o(t, [3301882366, 444984403]);
            return e(t, [0, t[0] >>> 1]);
        },

        6112: function(t, i) {
            const e = hashing[9303];
            const r = hashing[3235];
            const c = hashing[9869];
            const a = hashing[1318];
            const s = hashing[6217];
            const h = hashing[1552];

            i = i || 0;
            const u = (t = t || "").length % 16;
            const f = t.length - u;
            let l = [0, i];
            let d = [0, i];
            let g = [0, 0];
            let w = [0, 0];

            const b = [2277735313, 289559509];
            const M = [1291169091, 658871167];
            let v;
            for (v = 0; v < f; v += 16) {
                g = [255 & t.charCodeAt(v + 4) | (255 & t.charCodeAt(v + 5)) << 8 | (255 & t.charCodeAt(v + 6)) << 16 | (255 & t.charCodeAt(v + 7)) << 24, 255 & t.charCodeAt(v) | (255 & t.charCodeAt(v + 1)) << 8 | (255 & t.charCodeAt(v + 2)) << 16 | (255 & t.charCodeAt(v + 3)) << 24];
                w = [255 & t.charCodeAt(v + 12) | (255 & t.charCodeAt(v + 13)) << 8 | (255 & t.charCodeAt(v + 14)) << 16 | (255 & t.charCodeAt(v + 15)) << 24, 255 & t.charCodeAt(v + 8) | (255 & t.charCodeAt(v + 9)) << 8 | (255 & t.charCodeAt(v + 10)) << 16 | (255 & t.charCodeAt(v + 11)) << 24];
                g = r(g, b); g = c(g, 31); g = r(g, M);
                l = s(l, g); l = c(l, 27); l = e(l, d);
                l = e(r(l, [0, 5]), [0, 1390208809]); w = r(w, M); w = c(w, 33); w = r(w, b);
                d = s(d, w); d = c(d, 31); d = e(d, l); d = e(r(d, [0, 5]), [0, 944331445]);
            }

            g = [0, 0];
            w = [0, 0];
            switch (u) {
                case 15:
                    w = s(w, a([0, t.charCodeAt(v + 14)], 48));

                case 14:
                    w = s(w, a([0, t.charCodeAt(v + 13)], 40));

                case 13:
                    w = s(w, a([0, t.charCodeAt(v + 12)], 32));

                case 12:
                    w = s(w, a([0, t.charCodeAt(v + 11)], 24));

                case 11:
                    w = s(w, a([0, t.charCodeAt(v + 10)], 16));

                case 10:
                    w = s(w, a([0, t.charCodeAt(v + 9)], 8));

                case 9:
                    w = s(w, [0, t.charCodeAt(v + 8)]), w = r(w, M), w = c(w, 33), w = r(w, b), d = s(d, w);

                case 8:
                    g = s(g, a([0, t.charCodeAt(v + 7)], 56));

                case 7:
                    g = s(g, a([0, t.charCodeAt(v + 6)], 48));

                case 6:
                    g = s(g, a([0, t.charCodeAt(v + 5)], 40));

                case 5:
                    g = s(g, a([0, t.charCodeAt(v + 4)], 32));

                case 4:
                    g = s(g, a([0, t.charCodeAt(v + 3)], 24));

                case 3:
                    g = s(g, a([0, t.charCodeAt(v + 2)], 16));

                case 2:
                    g = s(g, a([0, t.charCodeAt(v + 1)], 8));

                case 1:
                    g = s(g, [0, t.charCodeAt(v)]), g = r(g, b), g = c(g, 31), g = r(g, M), l = s(l, g);
            }

            l = s(l, [0, t.length]);
            d = s(d, [0, t.length]);
            l = e(l, d); d = e(d, l);
            l = h(l); d = h(d);
            l = e(l, d); d = e(d, l);
            return ("00000000" + (l[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (l[1] >>> 0).toString(16)).slice(-8) + ("00000000" + (d[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (d[1] >>> 0).toString(16)).slice(-8);
        },

        3028: function(t, n, i, o) {
            return function() {
                let e = (t >>>= 0) + (n >>>= 0) | 0;
                t = n ^ n >>> 9;
                n = (i >>>= 0) + (i << 3) | 0;
                i = (i = i << 21 | i >>> 11) + (e = e + (o = 1 + (o >>>= 0) | 0) | 0) | 0;
                return (e >>> 0) / 4294967296;
            }
        },

        9623: function(t) {
            for (var o = 0, e = 1779033703 ^ t.length; o < t.length; o++) {
                e = (e = Math.imul(e ^ t.charCodeAt(o), 3432918353)) << 13 | e >>> 19;
            }
            return function() {
                e = Math.imul(e ^ e >>> 16, 2246822507);
                e = Math.imul(e ^ e >>> 13, 3266489909);
                return (e ^= e >>> 16) >>> 0;
            }
        },

        1122: function(t) {
            const o = hashing[3028];
            const e = hashing[9623];
            t = e(t || "");
            return o(t(), t(), t(), t());
        },

        9629: function(t, n) {
            const o = hashing[6112];
            const e = hashing[1122];
            const i = o("" + t, "" + n);
            const r = e(i);
            return [~~(246 * r()), ~~(255 * r()), ~~(255 * r()), ~~(255 * r())];
        }
    }

    class Vector {
        constructor(x = 0, y = 0) {
            this.x = x;
            this.y = y;
        }

        static fromAngle(angle, length = 1) {
            return new Vector(Math.cos(angle) * length, Math.sin(angle) * length);
        }

        add(vec) {
            if (vec instanceof Vector) {
                this.x += vec.x;
                this.y += vec.y;
            } else {
                this.x += vec;
                this.y += vec;
            }
            return this;
        }

        sub(vec) {
            if (vec instanceof Vector) {
                this.x -= vec.x;
                this.y -= vec.y;
            } else {
                this.x -= vec;
                this.y -= vec;
            }
            return this;
        }

        mult(vec) {
            if (vec instanceof Vector) {
                this.x *= vec.x;
                this.y *= vec.y;
            } else {
                this.x *= vec;
                this.y *= vec;
            }
            return this;
        }

        div(vec) {
            if (vec instanceof Vector) {
                this.x /= vec.x;
                this.y /= vec.y;
            } else {
                this.x /= vec;
                this.y /= vec;
            }
            return this;
        }

        get length() {
            return Math.sqrt(this.x * this.x + this.y * this.y);
        }

        normalize() {
            return this.length > 0 ? this.div(this.length) : this;
        }

        dot(vec) {
            return this.x * vec.x + this.y * vec.y;
        }

        proj(vec) {
            const k = this.dot(vec) / vec.dot(vec);
            return vec.copy().mult(k);
        }

        setXY(x, y) {
            this.x = x;
            this.y = y;
            return this;
        }

        setVec(vec) {
            return this.setXY(vec.x, vec.y);
        }

        setLength(value) {
            return this.normalize().mult(value);
        }

        copy() {
            return new Vector(this.x, this.y);
        }

        distance(vec) {
            return this.copy().sub(vec).length;
        }

        angle(vec) {
            const copy = vec.copy().sub(this);
            return Math.atan2(copy.y, copy.x);
        }

        direction(angle, length) {
            return this.copy().add(Vector.fromAngle(angle, length));
        }

        isEqual(vec) {
            return this.x === vec.x && this.y === vec.y;
        }

        stringify() {
            return this.x + ":" + this.y;
        }

        min(vec) {
            this.x = Math.min(this.x, vec.x);
            this.y = Math.min(this.y, vec.y);
            return this;
        }

        max(vec) {
            this.x = Math.max(this.x, vec.x);
            this.y = Math.max(this.y, vec.y);
            return this;
        }

        trunc() {
            this.x = Math.trunc(this.x);
            this.y = Math.trunc(this.y);
            return this;
        }
    }

    const formatButton = (button) => {
        if (0 === button) return "LBTN";
        if (1 === button) return "MBTN";
        if (2 === button) return "RBTN";
        if (3 === button) return "BBTN";
        if (4 === button) return "FBTN";
        throw new Error(`formatButton Error: "${button}" is not valid button`);
    }

    const InputHandler = new class InputHandler {
        mouse = new Vector;
        innerSize = new Vector;
        angle = 0;
        canvas = {};
        startMouse = false;

        attach() {
            const canvas = document.querySelector("#game-canvas");
            this.canvas.mousedown = canvas.onmousedown;
            this.canvas.mouseup = canvas.onmouseup;
            canvas.onmousedown = null
            canvas.onmouseup = null;

            window.addEventListener("mousemove", event => {
                this.mouse.setXY(event.clientX, event.clientY);
                this.angle = getAngle(innerWidth / 2, innerHeight / 2, this.mouse.x, this.mouse.y);
            });
            canvas.addEventListener("mousedown", event => this.mousedown(event));
            window.addEventListener("mouseup", event => this.mouseup(event));

            const resize = () => {
                const w = window.innerWidth;
                const h = window.innerHeight;
                this.mouse.x *= w / this.innerSize.x;
                this.mouse.y *= h / this.innerSize.y;
                this.innerSize.setXY(w, h);
            }
            window.addEventListener("resize", resize);
            resize();
        }

        cursorPosition(target) {
            const w = window.innerWidth;
            const h = window.innerHeight;
            const scale = Math.max(w / 1824, h / 1026);
            const cursorX = (this.mouse.x - w / 2) / scale;
            const cursorY = (this.mouse.y - h / 2) / scale;
            return new Vector(target.x + cursorX, target.y + cursorY);
        }

        mousedown(event) {
            const { ModuleHandler, myPlayer, clients } = playerClient;
            const button = formatButton(event.button);
            if (button === "LBTN" && !this.startMouse) {
                this.startMouse = true;
                this.canvas.mousedown(event);

                /*ModuleHandler.attacking = 1;
                const pos = this.cursorPosition(myPlayer.position);
                for (const client of clients) {
                    const angle = client.myPlayer.position.angle(pos);
                    client.PacketHandler.attack(angle);
                }*/
            }
        }

        mouseup(event) {
            const { ModuleHandler, clients } = playerClient;
            const button = formatButton(event.button);
            if (button === "LBTN" && this.startMouse) {
                this.startMouse = false;
                this.canvas.mouseup(event);

                /*if (ModuleHandler.attacking !== 0) {
                    for (const client of clients) {
                        client.PacketHandler.stopAttack();
                    }
                }*/
            }
        }
    }

    window.addEventListener("load", () => {
        InputHandler.attach();
    });

    const HookedArrays = {
        joins: null,
    }

    class TempData {
        constructor(client) {
            this.client = client;
        }

        setAttacking(attacking) {
            /*const { ModuleHandler } = this.client;
            if (ModuleHandler.attacking === attacking) {
                return;
            }
            ModuleHandler.attacking = attacking;
            if (0 !== attacking) {
                ModuleHandler.attackingState = attacking;
            }*/
        }
    }

    class ClanJoiner {
        constructor(client) {
            this.client = client;
            this.joinCount = 0;
        }

        postTick() {
            const { myPlayer, owner, PacketHandler } = this.client;
            const ownerClan = owner.myPlayer.clan;
            if (ownerClan === myPlayer.clan) return;

            if (this.joinCount === 0) {
                if (myPlayer.clan === 0) {
                    owner.pendingJoins.add(myPlayer.globalID);
                    PacketHandler.joinClan(ownerClan);
                } else {
                    PacketHandler.leaveClan();
                }
            }
            this.joinCount = (this.joinCount + 1) % 10;
        }
    }

    class Movement {
        constructor(client) {
            this.client = client;
        }

        postTick() {
            const { myPlayer, owner, ModuleHandler } = this.client;
            const pos1 = this.client.myPlayer.position;
            const pos2 = InputHandler.cursorPosition(this.client.owner.myPlayer.position);
            const distance = pos1.distance(pos2);
            const angle = pos1.angle(pos2);
            const currentAngle = distance > 0 ? angle: null;
            ModuleHandler.targetAngle = angle;
            ModuleHandler.moveByAngle(currentAngle);
        }
    }

    class AutoAccept {
        constructor(client) {
            this.client = client;
        }

        postTick() {
            const { myPlayer, PlayerManager, owner, pendingJoins, PacketHandler } = this.client;
            if (!PlayerManager.isClanOwner(myPlayer) || myPlayer.joinRequests.length === 0) return;

            const id = myPlayer.joinRequests[0];
            if (pendingJoins.size !== 0) {
                PacketHandler.acceptDecline(pendingJoins.has(id));
                this.client.removeJoin(id);
            }
        }
    }

    class AutoHeal {
        constructor(client) {
            this.client = client;
        }

        postTick() {
            if (this.client.myPlayer.health === 100) return;

            setTimeout(() => {
                this.client.ModuleHandler.heal();
            }, 120);
        }
    }

    class UpdateAngle {
        constructor(client) {
            this.client = client;
        }

        /*getAngle() {
            if (this.client.isOwner) return InputHandler._angle;
            return null;
        }*/

        postTick() {
            const { ModuleHandler } = this.client;

            const angle = ModuleHandler.targetAngle;
            if (angle !== null) {
                ModuleHandler.updateAngle(angle);
            }
        }
    }

    class ModuleHandler {
        constructor(client) {
            this.client = client;
            this.reset();

            this.statisModules = {
                tempData: new TempData(client),
            };

            this.botModules = [
                new ClanJoiner(client),
                new Movement(client),
            ];

            this.modules = [
                new AutoAccept(client),
                new AutoHeal(client),
                new UpdateAngle(client),
            ];

            this.rotation = true;
            this.mouse = {
                x: 0,
                y: 0,
                lockX: 0,
                lockY: 0,
                angle: 0,
                _angle: 0,
            }
        }

        reset() {
            this.weapon = 0;
            this.moveAngle = null;
            this.targetAngle = null;
            this.currentAngle = 0;
            this.attacking = 0;
            this.attackingState = 0;
        }

        attachMouse() {
            window.addEventListener("mousemove", event => {
                this.mouse.x = event.clientX;
                this.mouse.y = event.clientY;
                const angle = getAngle(innerWidth / 2, innerHeight / 2, this.mouse.x, this.mouse.y);
                this.mouse._angle = angle;
                if (this.rotation) {
                    this.mouse.lockX = event.clientX;
                    this.mouse.lockY = event.clientY;
                    this.mouse.angle = angle;
                }
            });
        }

        moveByAngle(angle) {
            if (angle === this.moveAngle) return;
            this.moveAngle = angle;

            const { PacketHandler } = this.client;
            if (angle === null) {
                PacketHandler.stopMovement();
            } else {
                PacketHandler.moveByAngle(angle);
            }
        }

        updateAngle(angle) {
            if (angle === this.currentAngle) return;
            this.currentAngle = angle;

            const { PacketHandler } = this.client;
            PacketHandler.updateAngle(angle);
        }

        whichWeapon(type = this.weapon) {
            if (this.weapon === type) return;
            this.weapon = type;

            this.client.PacketHandler.selectItemByType(type);
        }

        placeItemByType(type) {
            this.client.PacketHandler.selectItemByType(type);
            this.client.PacketHandler.attack(this.mouse.angle);
            this.client.PacketHandler.stopAttack();
            this.whichWeapon();
        }

        heal() {
            this.placeItemByType(2);
        }

        postTick() {
            if (!this.client.isOwner) {
                for (const botModule of this.botModules) {
                    botModule.postTick();
                }
            }

            for (const module of this.modules) {
                module.postTick();
            }
            this.attackingState = this.attacking;
        }
    }

    class Player {
        type = 0;
        globalID = 0;
        id = 0;
        position = new Vector;
        angle = 0;
        health = 100;
        clan = 0;

        constructor(client) {
            this.client = client;
        }

        update(type, globalID, id, x, y, angle, health, clanID) {
            this.type = type;
            this.globalID = globalID;
            this.id = id;
            this.position.setXY(x, y);
            this.angle = angle;
            this.health = health;
            this.clan = clanID;
        }
    }

    class ClientPlayer extends Player {
        constructor(client) {
            super(client);

            this.joinRequests = [];
            this.upgradeList = [];
            this.reset();
        }

        reset() {
            this.inGame = false;
            this.upgradeList.length = 0;
            this.upgradeIndex = 0;
            this.upgradeAge = 0;
            this.prevAge = 0;
            this.age = 0;
            this.food = 0;
            this.wood = 0;
            this.stone = 0;
            this.gold = 0;
        }

        handleUpgrade(items) {
            if (this.client.isOwner) return;
            const { upgradeList } = this.client.owner.myPlayer;
            const currentItem = upgradeList[this.upgradeIndex];
            if (upgradeList.includes(currentItem)) {
                this.upgradeItem(currentItem);
            }
        }

        upgradeItem(id) {
            this.upgradeAge += 1;

            if (this.client.isOwner) {
                this.upgradeList.push(id);

                for (const client of this.client.clients) {
                    const { age, upgradeAge, upgradeIndex } = client.myPlayer;
                    if (age > upgradeAge) {
                        const item = this.upgradeList[upgradeIndex];
                        client.myPlayer.upgradeItem(item);
                    }
                }
                return;
            }

            this.client.PacketHandler.upgradeItem(id);
            this.upgradeIndex += 1;
        }

        updateAge() {
            //log(this.age, this.upgradeAge);
        }

        handleStats(age, food, wood, stone, gold) {
            this.food = food;
            this.wood = wood;
            this.stone = stone;
            this.gold = gold;

            if (age === 0) return;
            this.prevAge = this.age;
            this.age = age;

            if (this.age > this.prevAge) {
                this.updateAge();
            }
        }

        postTick() {
            if (this.inGame) {
                this.client.ModuleHandler.postTick();
            }
        }
    }

    class PlayerManager {
        playerData = new Map();
        clanData = new Map();

        constructor(client) {
            this.client = client;
        }

        isClanOwner(player) {
            const clan = this.clanData.get(player.clan);
            return clan && clan.owner === player.globalID;
        }

        getPlayer(id) {
            if (this.playerData.has(id)) {
                return this.playerData.get(id);
            }

            const player = new Player();
            this.playerData.set(id, player);
            return player;
        }

        createClan(id, owner, name) {
            const clanData = this.clanData;
            if (!clanData.has(id)) {
                clanData.set(id, {});
            }

            const clan = clanData.get(id);
            clan.id = id;
            clan.owner = owner;
            clan.name = name;
        }
    }

    const random = (min, max) => {
        return Math.floor(Math.random() * (max - min + 1) + min);
    }

    const PI = Math.PI;
    const PI2 = PI * 2;
    const formatAge = (age) => Math.floor(Math.log(1 + Math.max(0, age)) ** 2.4 / 13);
    const formatAngle = (byte) => byte / 255 * PI2 - PI;
    const encodeAngle = (angle) => {
        angle = 65535 * (angle + PI) / PI2;
        return [255 & angle, angle >> 8 & 255];
    }

    const getRandomString = (len, numbers) => {
        let output = "";
        const nums = "0123456789";
        const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        const text = numbers ? nums : alphabet;
        for (let i=0;i<len;i++) {
            const index = Math.floor(Math.random() * (text.length - 1));
            const char = text[index];
            output += char;
        }
        return output;
    }

    const StoreState = {
        BUY: 0,
        EQUIP: 1,
        UNEQUIP: 2,
    }

    const EntityState = {
        UPDATE: 0,
        Nr: 1,
        DELETE: 2,
        IN_WATER: 4,
        UNDER_ROOF: 8,
        Vr: 16,
        TRAPPED: 32,
        ON_PLATFORM: 64,
        Yr: 128
    }

    const SocketServer = {
        UPDATE_MOVEMENT: 20,
        MY_PLAYER_INIT: 35,
        CONNECT: 25,
        Xn: 26, // not used in the code
        $n: 9, // most likely old player init, with toggle mechanism or death marker, [byte, byte, byte, byte]
        UPDATE_INVENTORY: 2,
        PLAYER_SPAWNED: 32,
        ATTACK_ANIMATION: 29,
        LEADERBOARD: 3,
        DEFAULT_DATA: 33,
        ri: 31, // most likely unvalid shit, used in rendering, some boss or icon shit
        DAMAGE: 6,
        KILL_UPDATE: 22,
        DIED: 19,
        PLAYER_RESOURCES: 8,
        KILL_MESSAGE: 28,
        ch: 13, // somehow related to clans
        UPDATE_STORE: 5,
        TEXT_MESSAGE: 30,
        UPGRADE: 14,
        CLEAR_UPGRADE: 34, // some init, reset upgrade bar
        wi: 11, // not used in the code
        PING_REQUEST: 0,
        PING_RESPONSE: 15,
        CLAN_JOIN: 24,
        CLAN_LEAVE: 27,
        CLAN_CREATED: 4,
        CLAN_DELETED: 1,
        JOIN_REQUEST: 17,
        CLAN_UPDATED: 16,
        ITEM_COUNTER: 36,
        MINIMAP_DATA: 10,
        BOSS_SPAWNED: 37,
        BOSS_KILLED: 21,
        cy: 23, // some shit with text rendering
        HANDSHAKE: 12, // some hash shit
        Ui: 7, // some server event or boss shit
        CLAN_MESSAGE: 18
    };

    const SocketClient = {
        VERIFICATION: 11,
        MOVE_BITMASK: 6,
        UPDATE_ANGLE: 13,
        SELECT_BY_ID: 2,
        START_ATTACK: 19,
        STOP_ATTACK: 18,
        LOGIN: 10,
        UPGRADE_SCYTHE: 20,
        SELECT_BY_TYPE: 0,
        SWITCH_HAT: 5,
        CHAT: 7,
        UPGRADE_ITEM: 14,
        Ri: 12, // not used in the code
        PING_CALLBACK: 3,
        TOGGLE_AUTOATTACK: 23,
        MOVE_ANGLE: 1,
        STOP_MOVEMENT: 15,
        touchAttackStart: 9,
        Wi: 4, // not used in the code
        touchAttackStop: 8, // some shit with angle
        LEAVE_CLAN: 24,
        JOIN_CLAN: 21,
        ACCEPT_DECLINE: 17,
        KICK: 25,
        CREATE_CLAN: 22,
        io: 16, // not used in the code
    }

    class PacketHandler {
        constructor(client) {
            this.client = client;
        }

        encoder = new TextEncoder();
        sendData(data) {
            const socket = this.client.SocketManager.socket;
            if (socket !== null && socket.readyState === 1) {
                socket.send(data, true);
            }
        }

        sendString(data) {
            this.sendData(JSON.stringify(data));
        }

        send(...bytes) {
            this.sendData(new Uint8Array(bytes));
        }

        moveByBitmask(bitmask) {
            this.send(SocketClient.MOVE_BITMASK, bitmask);
        }

        updateAngle(angle) {
            this.send(SocketClient.UPDATE_ANGLE, ...encodeAngle(angle));
        }

        selectByID(itemID) {
            this.send(SocketClient.SELECT_BY_ID, itemID);
        }

        attack(angle) {
            this.send(SocketClient.START_ATTACK, ...encodeAngle(angle));
        }

        stopAttack() {
            this.send(SocketClient.STOP_ATTACK);
        }

        login(nickname = "", skin = "0", accessory = "0", back = "0", email = "", token = "") {
            this.sendString([SocketClient.LOGIN, nickname, skin + "", "FFFFFEEEEGGBBBAAA", accessory + "", email, token, back + ""]);
        }

        upgradeScythe(goldenCowID) {
            this.send(SocketClient.UPGRADE_SCYTHE, 255 & goldenCowID, goldenCowID >> 8);
        }

        selectItemByType(type) {
            this.send(SocketClient.SELECT_BY_TYPE, type);
        }

        switchHat(hat) {
            this.send(SocketClient.SWITCH_HAT, hat);
        }

        chat(message) {
            const bytes = this.encoder.encode(message);
            this.send(SocketClient.CHAT, ...bytes);
        }

        upgradeItem(id) {
            this.send(SocketClient.UPGRADE_ITEM, id);
        }

        pingCallback(value) {
            this.send(SocketClient.PING_CALLBACK, value);
        }

        autoAttack(state) {
            this.send(SocketClient.TOGGLE_AUTOATTACK, Number(state));
        }

        moveByAngle(angle) {
            this.send(SocketClient.MOVE_ANGLE, ...encodeAngle(angle));
        }

        stopMovement() {
            this.send(SocketClient.STOP_MOVEMENT);
        }

        touchAttackStart(angle) {
            this.send(SocketClient.touchAttackStart, ...encodeAngle(angle));
        }

        touchAttackStop(angle) {
            this.send(SocketClient.touchAttackStop, ...encodeAngle(angle));
        }

        leaveClan() {
            this.send(SocketClient.LEAVE_CLAN);
        }

        joinClan(id) {
            this.send(SocketClient.JOIN_CLAN, id);
        }

        acceptDecline(state) {
            this.send(SocketClient.ACCEPT_DECLINE, Number(state));
        }

        kick(id) {
            this.send(SocketClient.KICK, id);
        }

        createClan(name) {
            const bytes = this.encoder.encode(name);
            this.send(SocketClient.CREATE_CLAN, ...bytes);
        }
    }

    class SocketManager {
        constructor(client) {
            this.client = client;
            this.socket = null;
            this.decoder = new TextDecoder();
        }

        init(socket) {
            this.socket = socket;

            socket.addEventListener("message", event => {
                const data = event.data;
                if (typeof data === "string") {
                    this.handleStringMessage(JSON.parse(data));
                } else {
                    this.handleByteMessage(new Uint8Array(data));
                }
            });

            const that = this;
            const _send = socket.send;
            socket.send = function(data, ignore) {
                if (!ignore && typeof data !== "string" && that.client.isOwner) {
                    const { myPlayer, clients } = that.client;
                    switch (data[0]) {
                        case SocketClient.START_ATTACK: {
                            const pos = InputHandler.cursorPosition(myPlayer.position);
                            for (const client of clients) {
                                const angle = client.myPlayer.position.angle(pos);
                                client.PacketHandler.attack(angle);
                            }
                            break;
                        }

                        case SocketClient.STOP_ATTACK: {
                            for (const client of clients) {
                                client.PacketHandler.stopAttack();
                            }
                            break;
                        }

                        case SocketClient.UPGRADE_ITEM: {
                            const item = data[1];
                            that.client.myPlayer.upgradeItem(item);
                            break;
                        }
                    }
                }
                return _send.call(this, data);
            }
        }

        selfMessage(data) {
            this.socket.onmessage({ data });
        }

        handleStringMessage(data) {
            const { myPlayer, PlayerManager, PacketHandler } = this.client;
            switch (data[0]) {
                case SocketServer.MY_PLAYER_INIT: {
                    const id = data[1];
                    const nickname = data[2];
                    // const ageXP = data[3];
                    const inventory = data[4];
                    const resources = data[5];
                    // const hatData = data[6];
                    // const someEvent = data[7];
                    //myPlayer.id = id;
                    //myPlayer.nickname = nickname;
                    //myPlayer.inGame = true;

                    const { myPlayer } = this.client;
                    myPlayer.id = id;
                    myPlayer.inGame = true;
                    //log("MY_PLAYER_INIT", id, nickname);
                    break;
                }

                case SocketServer.PLAYER_SPAWNED: {
                    const globalID = data[1];
                    const nickname = data[2];
                    //log("PLAYER_SPAWNED", globalID, nickname);
                    break;
                }

                case SocketServer.LEADERBOARD: {
                    for (const [id, score] of data[1]) {

                    }
                    break;
                }

                case SocketServer.DEFAULT_DATA: {
                    const globalID = data[1];
                    // const serverSize = data[2];
                    const players = data[3]; // [globalID, nickname, id]
                    const clanData = data[4]; // [clanID, ownerID, clanName]
                    for (const [id, owner, name] of clanData) {
                        PlayerManager.createClan(id, owner, name);
                    }
                    // const someState = data[5];

                    myPlayer.globalID = globalID;
                    PlayerManager.playerData.set(globalID, myPlayer);

                    log("DEFAULT_DATA", globalID, data);
                    if (!this.client.isOwner) {
                        this.client.connection();
                    }
                    break;
                }

                case SocketServer.DAMAGE: {
                    const damage = data[1];
                    const id = data[2];
                    const isHeal = data[3];
                    //log("DAMAGE", damage, id, isHeal);
                    break;
                }

                case SocketServer.KILL_UPDATE: {
                    const [ kills, nuggets ] = data[1];
                    //log("KILL_UPDATE", kills, nuggets);
                    break;
                }

                case SocketServer.DIED: {
                    myPlayer.reset();
                    if (!this.client.isOwner) {
                        PacketHandler.login("Murka");
                    }
                    //log("DIED");
                    break;
                }

                case SocketServer.KILL_MESSAGE: {
                    const text = data[1]; // Killed (nickname)
                    //log(text);
                    break;
                }

                case SocketServer.UPGRADE: {
                    const items = data[1];
                    /*for (const item of items) {

                    }*/
                    myPlayer.handleUpgrade(items);
                    break;
                }

                case SocketServer.HANDSHAKE: {
                    const someHash = data[1];
                    if (!this.client.isOwner) {
                        PacketHandler.sendData("");//window.solve(0));
                    }
                    break;
                }

                default: {
                    log("default data", data);
                    break;
                }
            }
        }

        handleByteMessage(buffer) {
            const { myPlayer, PlayerManager, PacketHandler } = this.client;
            switch (buffer[0]) {
                case SocketServer.UPDATE_MOVEMENT: {
                    for (let i=1;i<buffer.length;i+=19) {
                        const type = buffer[i];
                        const globalID = buffer[i + 1];
                        const id = buffer[i + 2] | buffer[i + 3] << 8;
                        const x = buffer[i + 4] | buffer[i + 5] << 8;
                        const y = buffer[i + 6] | buffer[i + 7] << 8;
                        const state = buffer[i + 8];
                        const angle = formatAngle(buffer[i + 9]);
                        const item = buffer[i + 10];
                        const hat = buffer[i + 11];
                        const clanID = buffer[i + 12];
                        const health = buffer[i + 13];
                        const skin = buffer[i + 14];
                        const accessory = buffer[i + 15];
                        const rank = buffer[i + 16];
                        const back = buffer[i + 17];
                        const a9 = buffer[i + 18];
                        if (type === 0) {
                            const player = PlayerManager.getPlayer(globalID);
                            player.update(type, globalID, id, x, y, angle, health / 255 * 100, clanID);
                        }
                    }
                    myPlayer.postTick();
                    break;
                }

                case SocketServer.UPDATE_INVENTORY: {
                    for (let i=1;i<buffer.length;i++) {
                        const id = buffer[i];
                    }
                    break;
                }

                case SocketServer.CONNECT: { // executed only once
                    const num = buffer[1];

                    // Necessary handshakre, otherwise the game wont let to connect
                    if (!this.client.isOwner) {
                        const bytes = hashing[9629](num, getRandomString(16));
                        const tokenBytes = new Array(24).fill(0);
                        PacketHandler.send(SocketClient.VERIFICATION, num, ...bytes, ...tokenBytes);
                    }
                    break;
                }

                case SocketServer.ATTACK_ANIMATION: {
                    for (let i=1;i<buffer.length;i+=5) {
                        const type = buffer[i];
                        const id = buffer[i + 1] | buffer[i + 2] << 8;
                        const isObject = buffer[3];
                        const weapon = buffer[4];
                        //log(type, id, isObject, weapon);
                    }
                    break;
                }

                case SocketServer.PLAYER_RESOURCES: {
                    const b = buffer;
                    const age = formatAge(b[1] | b[2] << 8 | b[3] << 16 | b[4] << 24);
                    const food = b[5] | b[6] << 8 | b[7] << 16 | b[8] << 24;
                    const wood = b[9] | b[10] << 8 | b[11] << 16 | b[12] << 24;
                    const stone = b[13] | b[14] << 8 | b[15] << 16 | b[16] << 24;
                    const gold = b[17] | b[18] << 8 | b[19] << 16 | b[20] << 24;
                    myPlayer.handleStats(age, food, wood, stone, gold);
                    break;
                }

                case SocketServer.UPDATE_STORE: {
                    for (let i=1;i<buffer.length;i+=2) {
                        const hatID = buffer[i];
                        const state = buffer[i + 1];
                        //log(hatID, state);
                    }
                    break;
                }

                case SocketServer.TEXT_MESSAGE: {
                    const id = buffer[1] | buffer[2] << 8;
                    const text = this.decoder.decode(buffer.subarray(3));
                    //log(id, text);
                    break;
                }

                case SocketServer.CLEAR_UPGRADE: {
                    //log("clear upgrade bar");
                    break;
                }

                case SocketServer.PING_REQUEST: {
                    const value = buffer[1];
                    //log("PING_REQUEST", value);
                    break;
                }

                case SocketServer.PING_RESPONSE: {
                    const ping = buffer[1] | buffer[2] << 8;
                    //myPlayer.ping = ping;
                    //log("PING_RESPONSE", ping);
                    break;
                }

                case SocketServer.CLAN_JOIN: {
                    const clanID = buffer[1];
                    const isOwner = buffer[2];
                    /*myPlayer.clan.id = clanID;
                    myPlayer.clan.isOwner = isOwner;
                    myPlayer.clan.name = clanData.get(clanID).name;
                    for (let i=3;i<buffer.length;i++) {
                        const id = buffer[i];
                        if (id !== myPlayer.id) {
                            myPlayer.clan.members.push(id);
                        }
                    }*/
                    //log("CLAN_JOIN", clanID, isOwner, buffer.slice(3));
                    break;
                }

                case SocketServer.CLAN_LEAVE: {
                    /*myPlayer.clan.id = 0;
                    myPlayer.clan.isOwner = 0;
                    myPlayer.clan.members.length = 0;
                    myPlayer.clan.name = "";*/
                    break;
                }

                case SocketServer.CLAN_CREATED: {
                    const clanID = buffer[1];
                    const ownerID = buffer[2];
                    const clanName = this.decoder.decode(buffer.subarray(3));

                    PlayerManager.createClan(clanID, ownerID, clanName);
                    //log("CLAN_CREATED", { clanID, ownerID, clanName });
                    break;
                }

                case SocketServer.CLAN_DELETED: {
                    const clanID = buffer[1];
                    PlayerManager.clanData.delete(clanID);
                    //log("CLAN_DELETED", { clanID });
                    break;
                }

                case SocketServer.JOIN_REQUEST: {
                    const globalID = buffer[1];
                    myPlayer.joinRequests.push(globalID);
                    break;
                }

                case SocketServer.CLAN_UPDATED: {
                    /*const myPlayer = this.client.myPlayer;
                    myPlayer.clan.members.length = 0;
                    const clanID = buffer[1];
                    for (let i=2;i<buffer.length;i++) {
                        const id = buffer[i];
                        if (id !== myPlayer.id) {
                            myPlayer.clan.members.push(id);
                        }
                    }
                    log("clan updated", myPlayer.clan);*/
                    break;
                }

                case SocketServer.ITEM_COUNTER: {
                    break;
                }

                case SocketServer.MINIMAP_DATA: {
                    for (let i=1;i<buffer.length;i+=3) {
                        const id = buffer[i + 0];
                        const x = buffer[i + 1] / 255;
                        const y = buffer[i + 2] / 255;
                        //log("MINIMAP_DATA", id, x, y);
                    }
                    break;
                }

                case SocketServer.BOSS_SPAWNED: {
                    const type = buffer[1];
                    const x = buffer[2] / 255;
                    const y = buffer[3] / 255;
                    //log("BOSS_SPAWNED", type, x, y);
                    break;
                }

                case SocketServer.BOSS_KILLED: {
                    //log("BOSS_KILLED", buffer);
                    break;
                }

                case SocketServer.CLAN_MESSAGE: {
                    const id = buffer[1] | buffer[2] << 8;
                    const text = this.decoder.decode(buffer.subarray(3));
                    break;
                }

                default: {
                    log("default byte", buffer);
                    break;
                }
            }
        }
    }

    class PlayerClient {
        constructor(owner = null) {
            this.owner = owner;
            this.clients = new Set();
            this.pendingJoins = new Set();
            this.PacketHandler = new PacketHandler(this);
            this.SocketManager = new SocketManager(this);
            this.ModuleHandler = new ModuleHandler(this);
            this.myPlayer = new ClientPlayer(this);
            this.PlayerManager = new PlayerManager(this);
        }

        get isOwner() {
            return this.owner === null;
        }

        addClient(client) {
            this.clients.add(client);
        }

        removeClient(client) {
            this.clients.delete(client);
        }

        init(socket) {
            this.SocketManager.init(socket);
        }

        disconnect() {
            const socket = this.SocketManager.socket;
            if (socket !== null) {
                socket.close();
            }
        }

        connection() {
            this.SocketManager.socket.dispatchEvent(new Event("connected"));
        }

        removeJoin(globalID) {
            this.pendingJoins.delete(globalID);
            this.myPlayer.joinRequests.shift();
            if (this.isOwner) HookedArrays.joins.shift();
        }
    }

    const _WebSocket = WebSocket;
    const playerClient = new PlayerClient();
    const createClient = () => {
        const socket = new _WebSocket(playerClient.SocketManager.socket.url);
        socket.binaryType = "arraybuffer";
        socket.onopen = () => {
            //log("Opened");
            const client = new PlayerClient(playerClient);
            client.init(socket);

            const onconnect = () => {
                //log("Connected");
                playerClient.addClient(client);
                client.PacketHandler.login("Murka");
            }
            socket.addEventListener("connected", onconnect, { once: true });
            socket.onclose = () => {
                log("Closed");
                playerClient.removeClient(client);
            }
        }
    }

    window.SocketAPI = {
        myClient: playerClient,
        createClient,
        HookedArrays,
    }

    let hooked = false;
    const defaultHooks = () => {
        if (hooked) return;
        hooked = true;

        Array.prototype.push = new Proxy(Array.prototype.push, {
            apply(target, _this, args) {
                if (args[0] === 255) {
                    HookedArrays.joins = _this;
                    Array.prototype.push = target;
                    return;
                }
                return target.apply(_this, args);
            }
        });

        playerClient.SocketManager.selfMessage([SocketServer.JOIN_REQUEST, 255]);
    }

    window.WebSocket = new Proxy(WebSocket, {
        construct(target, args) {
            const socket = new target(...args);
            playerClient.init(socket);

            Object.defineProperty(socket, "onmessage", {
                set(callback) {
                    delete socket.onmessage;
                    socket.onmessage = callback;
                    defaultHooks();
                },
                configurable: true
            });
            return socket;
        }
    });

})();

/*
    Author: Мurkа
    Github: github.com/Murka007
    Greasyfork: greasyfork.org/users/919633
    Discord: discord.gg/cPRFdcZkeD
*/