APM

Standard for hooking into the client -> server connection in arras.io

// ==UserScript==
// @name         APM
// @version      1.0.2
// @author       ABC & Ray Adams
// @namespace    github.com/ABCxFF
// @description  Standard for hooking into the client -> server connection in arras.io
// @match        *://arras.io/
// @match        *://arras.netlify.app/
// @grant        none
// @run-at       document-start
// @license      GPL-3.0
// ==/UserScript==
 
/*
 * Copyright (C) 2021  ABC & Ray Adams
 * Licensed under GNU General Public License v3.0
 */
 
const arras = (() => {
    const u32 = new Uint32Array(1);
    const u16 = new Uint16Array(1);
    const c16 = new Uint8Array(u16.buffer);
    const c32 = new Uint8Array(u32.buffer);
    const f32 = new Float32Array(u32.buffer);
 
    Array.prototype.remove = function (index) {
        if (index === this.length - 1) return this.pop();
        this[index] = this.pop();
    };
 
    function encode(message) {
        let headers = []
        let headerCodes = []
        let contentSize = 0
        let lastTypeCode = 0b1111
        let repeatTypeCount = 0
        for (let block of message) {
            let typeCode = 0
            if (block === 0 || block === false) {
                typeCode = 0b0000
            } else if (block === 1 || block === true) {
                typeCode = 0b0001
            } else if (typeof block === 'number') {
                if (!Number.isInteger(block) || block < -0x100000000 || block >= 0x100000000) {
                    typeCode = 0b1000
                    contentSize += 4
                } else if (block >= 0) {
                    if (block < 0x100) {
                        typeCode = 0b0010
                        contentSize++
                    } else if (block < 0x10000) {
                        typeCode = 0b0100
                        contentSize += 2
                    } else if (block < 0x100000000) {
                        typeCode = 0b0110
                        contentSize += 4
                    }
                } else {
                    if (block >= -0x100) {
                        typeCode = 0b0011
                        contentSize++
                    } else if (block >= -0x10000) {
                        typeCode = 0b0101
                        contentSize += 2
                    } else if (block >= -0x100000000) {
                        typeCode = 0b0111
                        contentSize += 4
                    }
                }
            } else if (typeof block === 'string') {
                let hasUnicode = false
                for (let i = 0; i < block.length; i++) {
                    if (block.charAt(i) > '\xff') {
                        hasUnicode = true
                    } else if (block.charAt(i) === '\x00') {
                        console.error('Null containing string', block)
                        throw new Error('Null containing string')
                    }
                }
                if (!hasUnicode && block.length <= 1) {
                    typeCode = 0b1001
                    contentSize++
                } else if (hasUnicode) {
                    typeCode = 0b1011
                    contentSize += block.length * 2 + 2
                } else {
                    typeCode = 0b1010
                    contentSize += block.length + 1
                }
            } else {
                console.error('Unencodable data type', block)
                throw new Error('Unencodable data type')
            }
            headers.push(typeCode)
            if (typeCode === lastTypeCode) {
                repeatTypeCount++
            } else {
                headerCodes.push(lastTypeCode)
                if (repeatTypeCount >= 1) {
                    while (repeatTypeCount > 19) {
                        headerCodes.push(0b1110)
                        headerCodes.push(15)
                        repeatTypeCount -= 19
                    }
                    if (repeatTypeCount === 1)
                        headerCodes.push(lastTypeCode)
                    else if (repeatTypeCount === 2)
                        headerCodes.push(0b1100)
                    else if (repeatTypeCount === 3)
                        headerCodes.push(0b1101)
                    else if (repeatTypeCount < 20) {
                        headerCodes.push(0b1110)
                        headerCodes.push(repeatTypeCount - 4)
                    }
                }
                repeatTypeCount = 0
                lastTypeCode = typeCode
            }
        }
        headerCodes.push(lastTypeCode)
        if (repeatTypeCount >= 1) {
            while (repeatTypeCount > 19) {
                headerCodes.push(0b1110)
                headerCodes.push(15)
                repeatTypeCount -= 19
            }
            if (repeatTypeCount === 1)
                headerCodes.push(lastTypeCode)
            else if (repeatTypeCount === 2)
                headerCodes.push(0b1100)
            else if (repeatTypeCount === 3)
                headerCodes.push(0b1101)
            else if (repeatTypeCount < 20) {
                headerCodes.push(0b1110)
                headerCodes.push(repeatTypeCount - 4)
            }
        }
        headerCodes.push(0b1111)
        if (headerCodes.length % 2 === 1)
            headerCodes.push(0b1111)
    
        let output = new Uint8Array((headerCodes.length >> 1) + contentSize)
        for (let i = 0; i < headerCodes.length; i += 2) {
            let upper = headerCodes[i]
            let lower = headerCodes[i + 1]
            output[i >> 1] = (upper << 4) | lower
        }
        let index = headerCodes.length >> 1
        for (let i = 0; i < headers.length; i++) {
            let block = message[i]
            switch (headers[i]) {
                case 0b0000:
                case 0b0001:
                    break
                case 0b0010:
                case 0b0011:
                    output[index++] = block
                    break
                case 0b0100:
                case 0b0101:
                    u16[0] = block
                    output.set(c16, index)
                    index += 2
                    break
                case 0b0110:
                case 0b0111:
                    u32[0] = block
                    output.set(c32, index)
                    index += 4
                    break
                case 0b1000:
                    f32[0] = block
                    output.set(c32, index)
                    index += 4
                    break
                case 0b1001:
                    {
                        let byte = block.length === 0 ? 0 : block.charCodeAt(0)
                        output[index++] = byte
                    }
                    break
                case 0b1010:
                    for (let i = 0; i < block.length; i++) {
                        output[index++] = block.charCodeAt(i)
                    }
                    output[index++] = 0
                    break
                case 0b1011:
                    for (let i = 0; i < block.length; i++) {
                        let charCode = block.charCodeAt(i)
                        output[index++] = charCode & 0xff
                        output[index++] = charCode >> 8
                    }
                    output[index++] = 0
                    output[index++] = 0
                    break
            }
        }
    
        return output
    };
 
    function decode(packet) {
        let data = new Uint8Array(packet)
        if (data[0] >> 4 !== 0b1111)
            return null
    
        let headers = []
        let lastTypeCode = 0b1111
        let index = 0
        let consumedHalf = true
        while (true) {
            if (index >= data.length)
                return null
            let typeCode = data[index]
    
            if (consumedHalf) {
                typeCode &= 0b1111
                index++
            } else {
                typeCode >>= 4
            }
            consumedHalf = !consumedHalf
    
            if ((typeCode & 0b1100) === 0b1100) {
                if (typeCode === 0b1111) {
                    if (consumedHalf)
                        index++
                    break
                }
    
                let repeat = typeCode - 10 // 0b1100 - 2
                if (typeCode === 0b1110) {
                    if (index >= data.length)
                        return null
                    let repeatCode = data[index]
    
                    if (consumedHalf) {
                        repeatCode &= 0b1111
                        index++
                    } else {
                        repeatCode >>= 4
                    }
                    consumedHalf = !consumedHalf
    
                    repeat += repeatCode
                }
    
                for (let i = 0; i < repeat; i++)
                    headers.push(lastTypeCode)
            } else {
                headers.push(typeCode)
                lastTypeCode = typeCode
            }
        }
    
        let output = []
        for (let header of headers) {
            switch (header) {
                case 0b0000:
                    output.push(0)
                    break
                case 0b0001:
                    output.push(1)
                    break
                case 0b0010:
                    output.push(data[index++])
                    break
                case 0b0011:
                    output.push(data[index++] - 0x100)
                    break
                case 0b0100:
                    c16[0] = data[index++]
                    c16[1] = data[index++]
                    output.push(u16[0])
                    break
                case 0b0101:
                    c16[0] = data[index++]
                    c16[1] = data[index++]
                    output.push(u16[0] - 0x10000)
                    break
                case 0b0110:
                    c32[0] = data[index++]
                    c32[1] = data[index++]
                    c32[2] = data[index++]
                    c32[3] = data[index++]
                    output.push(u32[0])
                    break
                case 0b0111:
                    c32[0] = data[index++]
                    c32[1] = data[index++]
                    c32[2] = data[index++]
                    c32[3] = data[index++]
                    output.push(u32[0] - 0x100000000)
                    break
                case 0b1000:
                    c32[0] = data[index++]
                    c32[1] = data[index++]
                    c32[2] = data[index++]
                    c32[3] = data[index++]
                    output.push(f32[0])
                    break
                case 0b1001:
                    {
                        let byte = data[index++]
                        output.push(byte === 0 ? '' : String.fromCharCode(byte))
                    }
                    break
                case 0b1010:
                    {
                        let string = ''
                        let byte = 0
                        while (byte = data[index++]) {
                            string += String.fromCharCode(byte)
                        }
                        output.push(string)
                    }
                    break
                case 0b1011:
                    {
                        let string = ''
                        let byte = 0
                        while (byte = data[index++] | (data[index++] << 8)) {
                            string += String.fromCharCode(byte)
                        }
                        output.push(string)
                    }
                    break
            }
        }
    
        return output
    };
    
    function rotator(packet) {
        return {
            i: 0,
            arr: packet,
            get(index) {
                return packet[index];
            },
            set(index, value) {
                return (packet[index] = value);
            },
            nex() {
                if (this.i === this.arr.length) {
                    console.error(new Error('End reached'), this.arr)
                    return -1;
                }
                return packet[this.i++];
            }
        }
    };
 
    class BroadcastParser {
        constructor() {
            this.leaderboard = [];
            this.teamMinimap = [];
            this.globalMinimap = [];
        }
    
        parse(packet) {
            const rot = rotator(packet);
    
            if (rot.nex() !== 'b') throw new TypeError('Invalid packet header; expected packet `b`');
    
            this._array(rot, () => {
                const del = rot.nex();
    
                this.globalMinimap.remove(this.globalMinimap.findIndex(({ id }) => id === del));
            });
    
            this._array(rot, () => {
                const dot = {
                    id: rot.nex(),
                    type: rot.nex(),
                    x: rot.nex(),
                    y: rot.nex(),
                    color: rot.nex(),
                    size: rot.nex()
                };
    
                let index = this.globalMinimap.findIndex(({ id }) => id === dot.id);
                if (index === -1) index = this.globalMinimap.length;
    
                this.globalMinimap[index] = dot;
            });
    
            this._array(rot, () => {
                const del = rot.nex();
    
                this.teamMinimap.remove(this.teamMinimap.findIndex(({ id }) => id === del));
            });
    
            this._array(rot, () => {
                const dot = {
                    id: rot.nex(),
                    x: rot.nex(),
                    y: rot.nex(),
                    size: rot.nex()
                };
    
                let index = this.teamMinimap.findIndex(({ id }) => id === dot.id);
                if (index === -1) index = this.teamMinimap.length;
    
                this.teamMinimap[index] = dot;
            });
    
            this._array(rot, () => {
                const del = rot.nex();
    
                this.leaderboard.remove(this.leaderboard.findIndex(({ id }) => id === del));
            });
    
            this._array(rot, () => {
                const champ = {
                    id: rot.nex(),
                    score: rot.nex(),
                    index: rot.nex(),
                    name: rot.nex(),
                    color: rot.nex(),
                    barColor: rot.nex()
                };
    
                let index = this.leaderboard.findIndex(({ id }) => id === champ.id);
                if (index === -1) index = this.leaderboard.length;
    
                this.leaderboard[index] = champ;
            });
    
            this.leaderboard.sort((c1, c2) => c2.score - c1.score);
    
            return this;
        }
    
        _array(rot, read, length = rot.nex()) {
            const out = Array(Math.max(0, length));
    
            for (let i = 0; i < length; ++i) out[i] = read.call(this, i, rot);
    
            return out;
        }
    };
 
    class RecordParser {
        constructor() {
            this.score = null;
            this.seconds = null;
            this.killCount = {
                players: null,
                assists: null,
                bosses: null
            };
            this.killersLength = null;
            this.killers = [];
        }
    
        parse(packet) {
            if (packet.shift() !== 'F') throw new TypeError('Invalid packet header; expected packet `F`');
    
            this.score = packet.shift();
            this.seconds = packet.shift();
    
            this.killCount.players = packet.shift();
            this.killCount.assists = packet.shift();
            this.killCount.bosses = packet.shift();
    
            this.killersLength = packet.shift();
            this.killers = packet.slice(0, this.killersLength);
    
            return this;
        }
    };
 
    class UpdateParser {
        constructor(doEntities = true) {
            this.camera = { x: null, y: null, vx: null, vy: null, fov: null };
            this.now = 0;
            this.player = {
                fps: 1,
                body: {
                    type: null,
                    color: null,
                    id: null,
                },
                score: null,
                points: null,
                upgrades: [],
                stats: [],
                skills: null,
                accel: null,
                top: null,
                party: null
            }
            this.entities = doEntities ? [] : false;
        }
        parse(packet) {
            const rot = rotator(packet);
    
            if (rot.nex() !== 'u') throw new TypeError('Invalid packet header; expected packet `u`');
    
            this.now = rot.nex();
    
            const version = this.now === 0 ? 2 : 1;
    
            this.camera.x = rot.nex();
            this.camera.y = rot.nex();
            this.camera.fov = rot.nex();
            this.camera.vx = rot.nex();
            this.camera.vy = rot.nex();
    
            const flags = rot.nex();
            if (flags & 0x0001) this.player.fps = rot.nex();
            if (flags & 0x0002) {
                this.player.body.type = rot.nex();
                this.player.body.color = rot.nex();
                this.player.body.id = rot.nex();
            }
            if (flags & 0x0004) this.player.score = rot.nex();
            if (flags & 0x0008) this.player.points = rot.nex();
            if (flags & 0x0010) this.player.upgrades = Array(Math.max(0, rot.nex())).fill(-1).map(() => rot.nex());
            if (flags & 0x0020) this.player.stats = Array(30).fill(0).map(() => rot.nex());
            if (flags & 0x0040) {
                const result = rot.nex();
    
                this.player.skills = [
                    (result / 0x1000000000 & 15),
                    (result / 0x0100000000 & 15),
                    (result / 0x0010000000 & 15),
                    (result / 0x0001000000 & 15),
                    (result / 0x0000100000 & 15),
                    (result / 0x0000010000 & 15),
                    (result / 0x0000001000 & 15),
                    (result / 0x0000000100 & 15),
                    (result / 0x0000000010 & 15),
                    (result / 0x0000000001 & 15)
                ]
            }
            if (flags & 0x0080) this.player.accel = rot.nex();
            if (flags & 0x0100) this.player.top = rot.nex();
            if (flags & 0x0200) this.player.party = rot.nex();
            if (flags & 0x0400) this.player.speed = rot.nex();
    
            if (version === 2 && this.entities !== false) {
                this._parseEnts(rot)
            } else if (version !== 2 && this.entities !== false) {
                this.entities = false;
                console.error('Invalid version, expected version 2. Disabling entities');
            }
            return this;
        }
        _table(rot, read) {
            const out = [];
            for (let id = rot.nex(); id !== -1; id = rot.nex()) {
                out[out.length] = read.call(this, id, rot)
            }
            return out
        }
        _parseEnts(rot) {
            if (rot.nex() !== -1) return console.warn('uhhhh-cancelling', rot.arr);
    
            this._table(rot, (id) => {
                const index = this.entities.findIndex(ent => ent.id === id);
                if (index === -1) {
                    return console.warn('Possible desync, deletion of non existant entity ' + id, this.entities.findIndex(ent => ent.id2 === id), JSON.stringify(this.entities));
                }
                this.entities[index] = this.entities[this.entities.length - 1]
                --this.entities.length;
            });
 
            this._table(rot, (id) => {
                let index = this.entities.findIndex(ent => ent.id === id)
                if (index === -1) this.entities[index = this.entities.length] = { id };
    
                const ent = this.entities[index];
                this._parseEnt(ent, rot)
            });
        }
 
        _parseEnt(ent, rot) {
            const flags = rot.nex();
            if (!ent) console.log(this.entities.length, rot.get(rot.i - 1));
            if (flags & 0x0001) {
                let { x: lastX, y: lastY } = ent;
                ent.x = rot.nex() * 0.0625;
                ent.y = rot.nex() * 0.0625;
                if (typeof lastX !== 'undefined') {
                    ent.vx = (ent.x - lastX);
                    ent.vy = (ent.y - lastY);
                } else ent.vx = ent.vy = 0;
            }
            if (flags & 0x0002) ent.facing = rot.nex() * (360 / 256);
            if (flags & 0x0004) ent.flags = rot.nex();
            if (flags & 0x0008) ent.health = rot.nex() / 255;
            if (flags & 0x0010) ent.shield = Math.max(0, rot.nex() / 255);
            if (flags & 0x0020) ent.alpha = rot.nex() / 255;
            if (flags & 0x0040) ent.size = rot.nex() * 0.0625;
            if (flags & 0x0080) ent.score = rot.nex();
            if (flags & 0x0100) ent.name = rot.nex();
            if (flags & 0x0200) ent.id2 = rot.nex();
            if (flags & 0x0400) ent.color = rot.nex();
            if (flags & 0x0800) ent.layer = rot.nex();
            if (flags & 0x1000) {
                if (!ent.guns) ent.guns = []
    
                this._table(rot, (index) => {
                    const flag = rot.nex();
                    if (!ent.guns[index]) ent.guns[index] = {};
                    if (flag & 1) ent.guns[index].time = rot.nex();
                    if (flag & 2) ent.guns[index].power = Math.sqrt(rot.nex()) / 20;
                });
            }
            if (flags & 0x2000) {
                if (!ent.turrets) ent.turrets = [];
    
                ent.turrets = this._table(rot, (index) => {
                    let i = ent.turrets.findIndex(ent => ent.index === index)
                    if (i === -1) ent.turrets[i = ent.turrets.length] = { index };
                    const turret = ent.turrets[i];
    
                    return this._parseEnt(turret, rot);
                });
            }
    
            return ent;
        }
    };
 
    const coder = { encode, decode };
 
    const hijack = () => {
        if (window['%arras']) return window['%arras'];
        
        window['%arras'] = new Promise(r => {
            const _send = WebSocket.prototype.send;
            window.WebSocket = class ArrasSocket extends WebSocket {
                constructor(...args) {
                    super(...args);
                    this.isntArras = true;
                    if (Array.isArray(args[1])) {
                        this.isntArras = false;
                        this._hook();
                        
                        this.onopen = () => r(this);
                        this.sendHooks = [];
                        this.msgHooks = [];
                    }
                }
                
                _hook() {
                    if (this.isntArras) throw 'sus';
                    
                    let send = this.send;
                    this.send = function(buf) {
                        return send.call(this, coder.encode(this.sendHooks.reduce((data, hook) => hook(data) || data, coder.decode(buf))));
                    }
                    
                    let adv = this.addEventListener;
                    this.addEventListener = function(type, cb, pro=false) {
                        if (pro) return adv.call(this, type, cb, pro);
                        
                        if (type === 'message') {
                            adv.call(this, 'message', (event) => {
                                cb(new MessageEvent('message', {
                                    data: coder.encode(this.msgHooks.reduce((data, hook) => hook(data) || data, coder.decode(new Uint8Array(event.data)))).buffer
                                }));
                            });
                        } else return adv.call(this, type, cb, pro);
                    }
                }
                
                hookSend(...funcs) {
                    this.sendHooks.push.apply(this.sendHooks, funcs)
                }
                hookMsg(...funcs) {
                    this.msgHooks.push.apply(this.msgHooks, funcs)
                }
                
                directTalk(...data) {
                    _send.call(this, coder.encode(data));
                }
                
                talk(...data) {
                    this.send(coder.encode(data));
                }
            }
        });
        
        return window['%arras']
    };
    
    return { encode, decode, BroadcastParser, RecordParser, UpdateParser, hijack };
})()