`
404.html:
<!--
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
-->

<!DOCTYPE html>

<html>
    <head>
        <link href="https://fonts.googleapis.com/css?family=Ubuntu:700" rel="stylesheet" type="text/css" />
        <style>
            body {
                background-color: black;
            }

            .err {
                color: red;
                font-family: 'Ubuntu';
                font-size: 11rem;
                text-align: center;
                margin: auto;
            }

            .msg {
                text-align: center;
                font-family: 'Ubuntu';
                font-size: 1.6rem;
                margin: auto;
                width: 75%;
                color: white;
            }

            a {
                text-decoration: none;
                color: rgb(0, 102, 255);
            }

            a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    
    <body>
        <div class="err">404</div>
        <div class="msg">The file you were looking for does not exist<p>Let's go <a href="/">home</a> and try from there</p></div>
    </body>
</html>

config.js:
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

const BUILD = "6f59094d60f98fafc14371671d3ff31ef4d75d9e";
const CDN = "https://static.diep.io/";
const API_URL = `${window.location.href}api/`;

const CHANGELOG = [
    "Updated recently",
    "",
    "Check out the GitHub repository!: github.com/ABCxFF/diepcustom",
    "Join our Discord server: discord.gg/SyxWdxgHnT :)"
];

const ASM_CONSTS = {
    1024: "createCanvasCtxWithAlpha",
    3781: "createImage",
    4211: "websocketSend",
    4388: "wipeContext",
    4528: "modulo",
    4551: "wipeSocket",
    4777: "setTextInput",
    4827: "wipeImage",
    4902: "reloadWindowTimeout",
    4968: "existsInWindowObject",
    5011: "empty", // ads
    5093: "empty", // ads
    5124: "empty", // ads
    5443: "empty", // ads
    5506: "empty", // ads
    5563: "empty", // ads
    5827: "getQueries",
    6547: "empty", // ads
    6927: "empty", // ads
    7068: "getLocalStorage",
    7293: "deleteLocalStorage",
    7343: "removeChildNode",
    7449: "checkElementProperty",
    7675: "existsQueryOrIsBlank",
    7886: "empty", // ads
    8008: "setLocalStorage",
    8254: "empty", // ads
    8472: "empty", // ads
    9178: "empty", // ads
    9362: "getGamepad",
    9558: "toggleFullscreen",
    10188: "getCanvasSize",
    10299: "setCursorDefault",
    10363: "setCursorPointer",
    10427: "setCursorText",
    10488: "getTextInput",
    10530: "enableTyping",
    11181: "disableTyping",
    11296: "focusCanvas",
    11444: "setCanvasSize",
    11529: "empty", // anti cheat
    11586: "copyUTF8",
    11637: "alert",
    11884: "saveContext",
    11952: "restoreContext",
    12020: "scaleContextAlpha",
    12063: "empty", // ads
    12501: "empty", // ads
    12976: "empty", // ads
    13247: "empty", // ads
    13331: "empty", // ads
    13414: "setContextFillStyle",
    13493: "setContextTransform",
    13560: "contextFillRect",
    13611: "contextBeginPath",
    13645: "contextClip",
    13674: "contextFill",
    13703: "setContextLineJoinRound",
    13744: "setContextLineJoinBevel",
    13785: "setContextLineJoinMiter",
    13826: "setContextLineWidth",
    13863: "setContextStrokeStyle",
    13939: "setContextTransformBounds",
    14002: "contextStroke",
    14033: "contextRect",
    14072: "getFontsLoaded",
    14174: "setContextFont",
    14240: "measureContextTextWidth",
    14308: "setContextAlpha",
    14347: "contextFillText",
    14402: "contextStrokeText",
    14459: "setContextTextBaselineTop",
    14502: "setContextTextBaselineHanging",
    14549: "setContextTextBaselineMiddle",
    14595: "setContextTextBaselineAlphabetic",
    14645: "setContextTextBaselineIdeographic",
    14696: "setContextTextBaselineBottom",
    15084: "setContextTransformNormalize",
    15137: "contextMoveTo",
    15178: "contextLineTo",
    15215: "contextClosePath",
    15288: "contextArc",
    16282: "copyToKeyboard",
    16505: "setLocation",
    16959: "contextDrawImage",
    24403: "getImage",
    24601: "contextDrawCanvas",
    24706: "setContextLineCapButt",
    24745: "setContextLineCapRound",
    24785: "setContextLineCapSquare",
    25012: "contextStrokeRect",
    25057: "contextDrawFullCanvas",
    25399: "isContextPatternAvailable",
    25462: "createContextPattern",
    25748: "contextGetPixelColor",
    25862: "contextDrawCanvasSourceToPixel",
    25954: "contextFillRectWithPattern",
    26051: "wipePattern",
    26281: "empty", // ?
    26345: "empty", // ?
    26430: "existsQuery",
    26890: "empty", // anti cheat
    26958: "canvasHasSamePropertyAsDocumentBody",
    27065: "existsDocumentBodyProperty",
    27120: "existsDocumentBodyProperty2",
    27166: "existsDivPropertyAndEqualsPropertyOnDocumentBody",
    27326: "empty", // anti cheat
    27575: "empty", // anti cheat
    27656: "empty", // anti cheat
    27778: "acCheckWindow", // anti cheat
    27886: "getDocumentBody",
    27945: "empty", // anti cheat
    28057: "empty", // anti cheat
    28125: "getUserAgent",
    28186: "empty", // anti cheat
    28407: "getQuerySelectorToString",
    28509: "getFillTextToString",
    28632: "getStrokeRectToString",
    28757: "getStrokeTextToString",
    28882: "getScaleToString",
    29002: "getTranslateToString",
    29126: "getFillRectToString",
    29249: "getRotateToString",
    29370: "getGetImageDataToString",
    29518: "empty", // ads
    29633: "contextClearRect",
    29740: "createCanvasCtx",
    29980: "setContextMiterLimit",
    30178: "getWindowLocation",
    30225: "setLoadingStatus",
    30352: "m28nReply",
    30767: "isSSL",
    30836: "createWebSocket",
    31548: "findServerById",
    31923: "invalidPartyId",
    31954: "wipeLocation",
    32047: "getGamepadAxe",
    32156: "getGamepadButtonPressed",
    32290: "pollWebSocketEvent",
    32446: "updateToNewVersion",
    32506: "empty", // pow
    32813: "reloadWindow",
    32840: "getWindowLocationSearch",
    32889: "getWindowReferrer",
    33085: "empty", // fingerprinting
    33169: "empty", // fingerprinting
    33222: "empty", // fingerprinting
    33330: "empty", // fingerprinting
    33425: "empty", // fingerprinting
    33488: "empty", // fingerprinting
    33528: "empty", // fingerprinting
};

const WASM_IMPORTS = {
    "i": "assertFail",
    "q": "mapFile",
    "p": "sysMunmap",
    "b": "abort",
    "d": "asmConstsDII",
    "a": "asmConstsIII",
    "j": "exitLive",
    "m": "exitForce",
    "g": "getNow",
    "n": "memCopyBig",
    "e": "random",
    "f": "resizeHeap",
    "r": "setMainLoop",
    "k": "envGet",
    "l": "envSize",
    "h": "fdWrite",
    "c": "roundF",
    "o": "timeString",
    "memory": "wasmMemory",
    "table": "wasmTable"
};

const WASM_EXPORTS = {
    "s": "wasmCallCtors", // constructors
    "t": "connect", // used to connect to ip, unused
    "u": "hasTank", // does player have a tank
    "v": "setConvar", // sets console var
    "w": "getConvar", // gets console var
    "x": "execute", // execute console cmd
    "y": "printConsoleHelp", // prints console help
    "z": "main", // main function
    "A": "checkWS", // polls ws
    "B": "free", // frees memory
    "C": "videoAdsLoaded", // event for ads loaded, unused
    "D": "videoAdsDone", // event for ad done, unused
    "E": "mouse", // sets mouse pos
    "F": "keyDown", // sets key down
    "G": "keyUp", // sets key up
    "H": "resetKeys", // resets key listeners
    "I": "preventRightClick", // should prevent right click if true
    "J": "flushInputHooks", // flushes all inputs (keys, mouse, gamepad)
    "K": "mouseWheel", // sets mouse wheel delta
    "L": "cp5Idle", // idle mode, unused
    "M": "cp5Destroy", // destroy cp5 obj, unused
    "N": "powResult", // pow result, unused 
    "O": "restReply", // server finder reply 
    "P": "getErrorLocation", // gets pointer for setting error code
    "Q": "malloc", // allocates memory
    "R": "dynCallVI", // dynamic func call (v = void, i = integer as arg, d = double as arg, f = float as arg)
    "S": "dynCallV" // dynamic func call (v = void, i = integer as arg, d = double as arg, f = float as arg)
};

const MOD_CONFIG = {
    "wasmFunctions": {
        "loadGamemodeButtons": 296,
        "loadVectorDone": 22,
        "loadChangelog": 447,
        "loadTankDefinitions": 277,
        "getTankDefinition": 101,
        "findCommand": 496,
        "decodeComponentList": 221,
        "createEntityAtIndex": 114
    },
    "memory": {
        "gamemodeDisabledText": 16420,
        "gamemodeButtons": 113480,
        "changelog": 167328,
        "changelogLoaded": 168632,
        "tankDefinitions": 166572,
        "tankDefinitionsCount": 166576,
        "commandList": 53064,
        "netColorTable": 49584
    },
    "wasmFunctionHookOffset": {
        "gamemodeButtons": 33,
        "changelog": 28
    }
};

const ADDON_MAP = {
    "trapLauncher": 147,
    "auto3": 148,
    "smasher": 149,
    "pronounced": 150,
    "landmine": 151,
    "auto5": 153,
    "autoturret": 154, // Auto Trapper (154) & Auto Gunner (152)
    "autosmasher": 155,
    "spike": 156,
    "launcher": 157, // Skimmer (157) & Rocketeer (158)
    "dombase": 159,
    "dompronounced": 160 // Dom1 (160) & Dom2 (161) 
};

const CUSTOM_ADDONS = {
    // This is a tutorial addon made for showcasing how custom addon renders are to be defined.
    "tutorial": entity => {
        // This if statement isnt totally necessary but might help your IDE recognize the type of "entity" which simplifies development later. It can be removed.
        if(!(entity instanceof $Entity)) return;
        
        /*
        We are currently on the root level meaning we can only access the "entity" the addon is placed upon. This means that "entity" is either a barrel or a tank.
        It is possible to change its data even at the root level.

        Each instance of "$Entity" has the following properties:
        - positionData
            -> Stores data about the entitie's position and angle, if the entity has a parent the x and y coordinates will be relative to the parents position.
        - physicsData
            -> Stores data about the entitie's collision properties (its size, sides, width, if it is a trapezoid).
        - styleData
            -> Stores data about how the entity is rendered, whether or not its visible, its color and so on.
        - barrelData
            -> This field is only defined if the entity actually is a barrel. It only has the shootingAngle property.
        - parent
            -> If this $Entity instance has been created via a createChild call, then this property stores a reference to its creator / parent. (ONLY DIRECT PARENT!)
        - children
            -> If this entity created children via createChild calls, then this property stores references to those children. (ONLY DIRECT CHILDREN!)
        As well as the following methods:
        - clone(source: $Entity)
            -> Clones the sources position, physics, style and barrelData to the current entity.
        - createChild(isBarrel: boolean)
            -> Creates either a regular object entity or a barrel entity and sets its parent as this entity.
        - defaults()
            -> Sets the default values for position, physics, style and barrelData. Check out the default values in "./dma.js".
        
        Each of the "[...]Data" fields has similarly .clone(source: Self) and defaults() methods.
        
        You may not set the properties of $Entity manually. Instead use the "[...]Data" fields or .clone() / .defaults().
        So don't do this: "someEntity.positionData = someOtherEntity.positionData;", but instead do this: "someEntity.positionData.clone(someOtherEntity.positionData);".
        */
    },
    "auto2": entity => {
        if(!(entity instanceof $Entity)) return;
                
        const rotator = entity.createChild(false);
        rotator.defaults();
        rotator.physicsData.size = 5;
        rotator.positionData.angle = 0.01;
        rotator.positionData.isAngleAbsolute = true;
        rotator.styleData.isVisible = false;

        const count = 2;
        for(let i = 0; i < count; ++i) {
            const socket = rotator.createChild(false);
            socket.defaults();
            
            socket.positionData.angle = i * Math.PI * 2 / count;
            socket.positionData.x = Math.cos(socket.positionData.angle) * 40;
            socket.positionData.y = Math.sin(socket.positionData.angle) * 40;
            socket.physicsData.size = 25;
            // Color.Barrel
            socket.styleData.color = 1;

            const barrel = socket.createChild(true);
            barrel.defaults();
            barrel.physicsData.size = 55;
            barrel.physicsData.sides = 2;
            barrel.physicsData.width = 0.7 * 42;
            // angle + shootingAngle
            barrel.positionData.angle = 0;
            // Math.cos(angle) * (size / 2 + distance) - Math.sin(angle) * offset
            barrel.positionData.x = Math.cos(0) * (barrel.physicsData.size / 2 + 0) - Math.sin(0) * 0;
            // Math.sin(angle) * (size / 2 + distance) - Math.cos(angle) * offset
            barrel.positionData.y = Math.sin(0) * (barrel.physicsData.size / 2 + 0) - Math.cos(0) * 0;
            // Color.Barrel
            barrel.styleData.color = 1;
        }
    },
    "arrasspawnerbarrel": entity => {
        if(!(entity instanceof $Entity)) return;

        const rect1 = entity.createChild(false);
        rect1.defaults();
        rect1.styleData.color = 1;
        rect1.styleData.showsAboveParent = true;
        rect1.physicsData.sides = 2;
        rect1.physicsData.width = entity.physicsData.width * 1.25;
        rect1.physicsData.size = entity.physicsData.size * (10 / 50);
        rect1.positionData.x = (entity.physicsData.size - rect1.physicsData.size) / 2;

        const rect2 = entity.createChild(false);
        rect2.defaults();
        rect2.styleData.color = 1;
        rect2.styleData.showsAboveParent = true;
        rect2.physicsData.sides = 2;
        rect2.physicsData.width = entity.physicsData.width * 1.25;
        rect2.physicsData.size = entity.physicsData.size * (35 / 50);
        rect2.positionData.x = (-entity.physicsData.size + rect2.physicsData.size) / 2;
    }
}

const CUSTOM_COMMANDS = [
    {
        "id": "util_test",
        "description": "Test command to check if custom commands are working, prints 'Hello World' to the console",
        "callback": args => { // array of strings, you need to parse them yourself
            console.log("Hello World");
        }
    }, {
        "id": "util_set_dev_password",
        "usage": "[password]",
        "description": "Sets the dev password (reconnect required)",
        "callback": args => {
            if(!args[0]) return;
            window.localStorage.setItem("password", args[0]);
        }
    }, {
        "id": "util_change_scheduler",
        "usage": "[?scheduler]",
        "description": "Changes the game's frame scheduler (default: requestAnimationFrame)",
        "callback": args => {
            // possible alternative would be setTimeout eg.
            Module.scheduler = typeof window[args[0]] === "function" ? window[args[0]] : window.requestAnimationFrame;
        }
    }, {
        "id": "util_reload_servers",
        "usage": "[?interval]",
        "description": "Sets the interval in which gamemodes are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadServersInterval = -1;
                        case "connect":
                            return Module.reloadServersInterval = -2;
                    }
                }
                return Module.reloadServersInterval = num;
            }
            Game.reloadServers();
        }
    }, {
        "id": "util_reload_tanks",
        "usage": "[?interval]",
        "description": "Sets the interval in which tanks are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadTanksInterval = -1;
                        case "connect":
                            return Module.reloadTanksInterval = -2;
                    }
                }
                return Module.reloadTanksInterval = num;
            }
            Game.reloadTanks();
        }
    }, {
        "id": "util_reload_commands",
        "usage": "[?interval]",
        "description": "Sets the interval in which commands are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadCommandsInterval = -1;
                        case "connect":
                            return Module.reloadCommandsInterval = -2;
                    }
                }
                return Module.reloadCommandsInterval = num;
            }
            Game.reloadCommands();
        }
    }, {
        "id": "util_set_changelog",
        "usage": "[line 1\\n] [line 2] ...",
        "description": "Sets the changelog to the given text, remember to use \\n before and after each line",
        "callback": args => {
            Game.changeChangelog(args.join(' ').split("\\n"));
        }
    }
];

const COMMANDS_LOOKUP = {
    "con_toggle": 52952,
    "game_spawn": 52992,
    "help": 49956,
    "lb_reconnect": 50056,
    "net_replace_color": 50152,
    "net_replace_colors": 50192,
    "ui_replace_colors": 49916
};

const WASM_TABLE = {
    "initial": 687,
    "element": "anyfunc"
};

const INITIAL_MEMORY = 67108864;
const WASM_PAGE_SIZE = 65536; // A WebAssembly page has a constant size of 65,536 bytes, i.e., 64KiB

const DYNAMIC_BASE = 5426112; // start of dynmic memory
const DYNAMIC_TOP_PTR = 183072; // points to start of dynamic memory

const WASM_MEMORY = {
    "initial": INITIAL_MEMORY / WASM_PAGE_SIZE,
    "maximum": INITIAL_MEMORY / WASM_PAGE_SIZE
};

const FIELD_OFFSETS = {
    basic: {
        owner: 20,
        parent: 32
    },
    position: {
        y: 8,
        x: 40,
        angle: 72,
        flags: 104
    },
    collidable: {
        size: 16,
        sides: 48,
        width: 64,
        flags: 104
    },
    renderable: {
        color: 12,
        flags: 20,
        borderWidth: 32,
        opacity: 64
    },
    cannon: {
        shootingAngle: 8
    }
};

const FLAGS = {
    absoluteRotation: 1 << 0,
    isTrapezoid: 1 << 0,
    isVisible: 1 << 0,
    renderFirst: 1 << 3,
    isStar: 1 << 4,
    isCachable: 1 << 5,
    showsAboveParent: 1 << 6
};

dma.js:
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

class $Entity {
    #$;
    #$basic;
    #$position;
    #$collidable;
    #$renderable;
    #$cannon;
    constructor($, $parent = null) {
        this.#$ = $;
        this.parent = null;
        this.children = [];

        this.#$basic = $[0x10][0x20][0x18][0 * 4].$;
        this.#$position = $[0x10][0x20][0x18][10 * 4].$;
        this.#$collidable = $[0x10][0x20][0x18][3 * 4].$;
        this.#$renderable = $[0x10][0x20][0x18][11 * 4].$;
        this.#$cannon = $[0x10][0x20][0x18][2 * 4].$;
        
        if($parent) {
            if (!this.#$basic.at) throw "Invalid Basic component reference.";
            this.#$basic[FIELD_OFFSETS.basic.parent].u32 = $parent[52].u32;
            this.#$basic[FIELD_OFFSETS.basic.parent][4].u16 = $parent[56].u16;
            this.#$basic[FIELD_OFFSETS.basic.owner].u32 = $parent[52].u32;
            this.#$basic[FIELD_OFFSETS.basic.owner][4].u16 = $parent[56].u16;
        }
    }

    createChild(isCannon = false) {
        const ptr = $EntityManager.createNewEntity(this.#$[48].at);
        if (!ptr) throw "Out of entityIds";

        const componentList = $EntityManager.createComponentList({
            createBasic: true,
            createCollidable: true,
            createPosition: true,
            createRenderable: true,
            createCannon: isCannon
        });
        if (!componentList) throw "Invalid component list reference.";
        Module.rawExports.decodeComponentList(ptr, componentList);
        Module.exports.free($(componentList)[0].i32);
        Module.exports.free(componentList);

        const entity = new $Entity($(ptr), this.#$);
        entity.parent = this;
        this.children.push(entity);

        return entity;
    }

    get positionData() {
        return this.#$position ? new $Position(this.#$position) : null;
    }

    set positionData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.positionData.clone(yourFirstEntity.positionData); instead.";
    }

    get physicsData() {
        return this.#$collidable ? new $Collidable(this.#$collidable) : null;
    }

    set physicsData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.physicsData.clone(yourFirstEntity.physicsData); instead.";
    }

    get styleData() {
        return this.#$renderable ? new $Renderable(this.#$renderable) : null;
    }

    set styleData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.styleData.clone(yourFirstEntity.styleData); instead.";
    }

    get barrelData() {
        return this.#$cannon ? new $Cannon(this.#$collidable) : null;
    }

    set barrelData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.barrelData.clone(yourFirstEntity.barrelData); instead.";
    }

    clone(source) {
        if (!(source instanceof $Entity)) throw "Failed to clone data from an unknown object, please provide an instance of $Entity.";
        if(source.positionData) this.positionData?.clone(source.positionData);
        if(source.physicsData) this.physicsData?.clone(source.physicsData);
        if(source.styleData) this.styleData?.clone(source.styleData);
        if(source.barrelData) this.barrelData?.clone(source.barrelData);
    }

    defaults() {
        this.positionData?.defaults();
        this.physicsData?.defaults();
        this.styleData?.defaults();
        this.barrelData?.defaults();
    }
}

class $Component {
    #$;
    constructor($) {
        this.entity = new $Entity($.$);
        this.#$ = $;
    }
}

class $Position extends $Component {
    #$x;
    #$y;
    #$angle;
    #$flags;
    constructor($) {
        super($);
        this.#$x = $[FIELD_OFFSETS.position.x];
        this.#$y = $[FIELD_OFFSETS.position.y];
        this.#$angle = $[FIELD_OFFSETS.position.angle];
        this.#$flags = $[FIELD_OFFSETS.position.flags];
    }

    get x() {
        return this.#$x.f32;
    }

    set x(value) {
        this.#$x.f32 = value;
    }

    get y() {
        return this.#$y.f32;
    }

    set y(value) {
        this.#$y.f32 = value;
    }

    get angle() {
        return this.#$angle.f32;
    }

    set angle(value) {
        this.#$angle.f32 = value;
    }

    get isAngleAbsolute() {
        return Boolean(this.#$flags.u32 & FLAGS.absoluteRotation);
    }

    set isAngleAbsolute(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.absoluteRotation;
        else this.#$flags.u32 &= ~FLAGS.absoluteRotation;
    }

    clone(source) {
        if (!(source instanceof $Position)) throw "Failed to clone data from an unknown object, please provide an instance of $Position.";
        this.x = source.x;
        this.y = source.y;
        this.angle = source.angle;
        this.isAngleAbsolute = source.isAngleAbsolute;
    }

    defaults() {
        this.x = 0;
        this.y = 0;
        this.angle = 0;
        this.isAngleAbsolute = false;
    }
}

class $Collidable extends $Component {
    #$sides;
    #$size;
    #$width;
    #$flags;
    constructor($) {
        super($);
        this.#$sides = $[FIELD_OFFSETS.collidable.sides];
        this.#$size = $[FIELD_OFFSETS.collidable.size];
        this.#$width = $[FIELD_OFFSETS.collidable.width];
        this.#$flags = $[FIELD_OFFSETS.collidable.flags];
    }

    get sides() {
        return this.#$sides.u32;
    }

    set sides(value) {
        this.#$sides.u32 = value;
    }

    get size() {
        return this.#$size.f32;
    }

    set size(value) {
        this.#$size.f32 = value;
    }

    get width() {
        return this.#$width.f32;
    }

    set width(value) {
        this.#$width.f32 = value;
    }

    get isTrapezoid() {
        return Boolean(this.#$flags.u32 & FLAGS.isTrapezoid);
    }

    set isTrapezoid(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isTrapezoid;
        else this.#$flags.u32 &= ~FLAGS.isTrapezoid;
    }

    clone(source) {
        if (!(source instanceof $Collidable)) throw "Failed to clone data from an unknown object, please provide an instance of $Collidable.";
        this.sides = source.sides;
        this.size = source.size;
        this.width = source.width;
        this.isTrapezoid = source.isTrapezoid;
    }

    defaults() {
        this.sides = 1;
        this.size = 50.0;
        this.width = 0;
        this.isTrapezoid = false;
    }
}

class $Renderable extends $Component {
    #$color;
    #$borderWidth;
    #$opacity;
    #$flags;
    constructor($) {
        super($);
        this.#$color = $[FIELD_OFFSETS.renderable.color];
        this.#$borderWidth = $[FIELD_OFFSETS.renderable.borderWidth];
        this.#$opacity = $[FIELD_OFFSETS.renderable.opacity];
        this.#$flags = $[FIELD_OFFSETS.renderable.flags];
    }

    get color() {
        return this.#$color.u32;
    }

    set color(value) {
        this.#$color.u32 = value;
    }

    get borderWidth() {
        return this.#$borderWidth.f32;
    }

    set borderWidth(value) {
        this.#$borderWidth.f32 = value;
    }

    get opacity() {
        return this.#$opacity.f32;
    }

    set opacity(value) {
        this.#$opacity.f32 = value;
    }

    get isVisible() {
        return Boolean(this.#$flags.u32 & FLAGS.isVisible);
    }

    set isVisible(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isVisible;
        else this.#$flags.u32 &= ~FLAGS.isVisible;
    }

    get renderFirst() {
        return Boolean(this.#$flags.u32 & FLAGS.renderFirst);
    }

    set renderFirst(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.renderFirst;
        else this.#$flags.u32 &= ~FLAGS.renderFirst;
    }

    get isStar() {
        return Boolean(this.#$flags.u32 & FLAGS.isStar);
    }

    set isStar(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isStar;
        else this.#$flags.u32 &= ~FLAGS.isStar;
    }

    get isCachable() {
        return Boolean(this.#$flags.u32 & FLAGS.isCachable);
    }

    set isCachable(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isCachable;
        else this.#$flags.u32 &= ~FLAGS.isCachable;
    }

    get showsAboveParent() {
        return Boolean(this.#$flags.u32 & FLAGS.showsAboveParent);
    }

    set showsAboveParent(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.showsAboveParent;
        else this.#$flags.u32 &= ~FLAGS.showsAboveParent;
    }

    clone(source) {
        if (!(source instanceof $Renderable)) throw "Failed to clone data from an unknown object, please provide an instance of $Renderable.";
        this.color = source.color;
        this.borderWidth = source.borderWidth;
        this.opacity = source.borderWidth;
        this.isVisible = source.isVisible;
        this.renderFirst = source.renderFirst;
        this.isStar = source.isStar;
        this.isCachable = source.isCachable;
        this.showsAboveParent = source.showsAboveParent;
    }

    defaults() {
        this.color = 0;
        this.borderWidth = 7.5;
        this.opacity = 1;
        this.isVisible = true;
        this.renderFirst = false;
        this.isStar = false;
        this.isCachable = true;
        this.showsAboveParent = false;
    }
}

class $Cannon extends $Component {
    #$shootingAngle;
    constructor($) {
        super($);
        this.#$shootingAngle = $[FIELD_OFFSETS.cannon.shootingAngle];
    }

    get shootingAngle() {
        return this.#$shootingAngle.f32;
    }

    set shootingAngle(value) {
        this.#$shootingAngle.f32 = value;
    }

    clone(source) {
        if (!(source instanceof $Cannon)) throw "Failed to clone data from an unknown object, please provide an instance of $Cannon.";
        this.shootingAngle = source.shootingAngle;
    }

    defaults() {
        this.shootingAngle = 0;
    }
}

class $LinkedList {
    constructor(ptr, type, typeSize) {
        this.ptr = ptr;
        this.type = type;
        this.typeSize = typeSize;
    }

    push(...entries) {
        for (const entry of entries) this._append(this._end, entry);
        this.length += entries.length;
    }

    forEach(cb) {
        let current = $(this.ptr);
        while (current.i32) {
            cb(current.i32);
            current = current.$;
        }
    }

    get length() {
        return $(this.ptr)[4].i32;
    }

    set length(val) {
        $.write(this.ptr + 4, "u32", val);
    }

    get _end() {
        let current = $(this.ptr);
        while (current.i32) current = current.$;
        return current.at;
    }

    _append(last, entry) {
        const ptr = Module.exports.malloc(this.typeSize);
        $.write(ptr, this.type, entry);
        $.write(last, "$", ptr);
    }
}

class $Vector {
    constructor(ptr, type, typeSize) {
        this.ptr = ptr;
        this.type = type;
        this.typeSize = typeSize;
        this.start = $(ptr).i32;
        this.end = $(ptr)[4].i32;
        this.maxCapacity = $(ptr)[8].i32;
        this.maxEntries = (this.maxCapacity - this.start) / this.typeSize;
        this.totalSize = (this.end - this.start) / this.typeSize;
        this.entries = [];
    }

    push(...entries) {
        this.entries.push(...entries);
        this.totalSize = this.entries.length * this.typeSize;
        if (this.entries.length > this.maxEntries) this._malloc();
        else this._write();
    }

    clear() {
        this.entries = [];
        this.end = this.start;
        Module.HEAPU8.subarray(this.start, this.maxCapacity).fill(0);
    }

    destroy() {
        Module.exports.free(this.start);
        this.start = 0;
        this.end = 0;
        this.maxCapacity = 0;
        $.write(this.ptr, "$", this.start);
        $.write(this.ptr + 4, "$", this.end);
        $.write(this.ptr + 8, "$", this.maxCapacity);
        this.maxEntries = 0;
        this.totalSize = 0;
        this.entries = [];
    }

    _write() {
        for (const entry of this.entries) {
            $.write(this.end, this.type, entry);
            this.end += this.typeSize;
        }
        $.write(this.ptr + 4, "$", this.end);
    }

    _malloc() {
        this.maxEntries = (this.maxCapacity - this.start) / this.typeSize;
        this.totalSize = this.entries.length * this.typeSize;
        this.start = Module.exports.malloc(this.totalSize);
        this.end = this.start;
        this.maxCapacity = this.start + this.totalSize;
        Module.HEAPU8.subarray(this.start, this.maxCapacity).fill(0);
        $.write(this.ptr, "$", this.start);
        this._write();
        $.write(this.ptr + 8, "$", this.maxCapacity);
    }
}

class $EntityManager {
    static createNewEntity(simulation) {
        for (let i = 0; i < 16385; ++i) {
            if (!$(simulation).$[i][720].u8) {
                return Module.rawExports.createEntityAtIndex($(simulation).i32 + 12, i);
            }
        }
        return 0;
    }

    static getComponent(entity, name) {
        // These offsets don't change over versions (unless the components are reordered)
        switch (name) {
            case "Basic":
                return $(entity)[0x10][0x20][0x18][0 * 4].i32;
            case "CPUControllable":
                return $(entity)[0x10][0x20][0x18][1 * 4].i32;
            case "Cannon":
                return $(entity)[0x10][0x20][0x18][2 * 4].i32;
            case "Collidable":
                return $(entity)[0x10][0x20][0x18][3 * 4].i32;
            case "Destroyable":
                return $(entity)[0x10][0x20][0x18][4 * 4].i32;
            case "Dominator":
                return $(entity)[0x10][0x20][0x18][5 * 4].i32;
            case "ExampleComponent":
                return $(entity)[0x10][0x20][0x18][6 * 4].i32;
            case "MapInfo":
                return $(entity)[0x10][0x20][0x18][7 * 4].i32;
            case "Namable":
                return $(entity)[0x10][0x20][0x18][8 * 4].i32;
            case "PlayerInfo":
                return $(entity)[0x10][0x20][0x18][9 * 4].i32;
            case "Position":
                return $(entity)[0x10][0x20][0x18][10 * 4].i32;
            case "Renderable":
                return $(entity)[0x10][0x20][0x18][11 * 4].i32;
            case "Tagged":
                return $(entity)[0x10][0x20][0x18][12 * 4].i32;
            case "Tank":
                return $(entity)[0x10][0x20][0x18][13 * 4].i32;
            case "TeamInfo":
                return $(entity)[0x10][0x20][0x18][14 * 4].i32;
            default:
                return 0;
        }
    }


    /**
     * You need to free the returned ptr and ptr.$
     */
    static createComponentList({
        createBasic,
        createCPUControllable,
        createCannon,
        createCollidable,
        createDestroyable,
        createDominator,
        createExampleComponent,
        createMapInfo,
        createNamable,
        createPlayerInfo,
        createPosition,
        createRenderable,
        createTagged,
        createTank,
        createTeamInfo
    }) {
        let at = -1;
        const data = [];
        if (createBasic) {
            data.push((0 - at) ^ 1);
            at = 0;
        }
        if (createCPUControllable) {
            data.push((1 - at) ^ 1);
            at = 1;
        }
        if (createCannon) {
            data.push((2 - at) ^ 1);
            at = 2;
        }
        if (createCollidable) {
            data.push((3 - at) ^ 1);
            at = 3;
        }
        if (createDestroyable) {
            data.push((4 - at) ^ 1);
            at = 4;
        }
        if (createDominator) {
            data.push((5 - at) ^ 1);
            at = 5;
        }
        if (createExampleComponent) {
            data.push((6 - at) ^ 1);
            at = 6;
        }
        if (createMapInfo) {
            data.push((7 - at) ^ 1);
            at = 7;
        }
        if (createNamable) {
            data.push((8 - at) ^ 1);
            at = 8;
        }
        if (createPlayerInfo) {
            data.push((9 - at) ^ 1);
            at = 9;
        }
        if (createPosition) {
            data.push((10 - at) ^ 1);
            at = 10;
        }
        if (createRenderable) {
            data.push((11 - at) ^ 1);
            at = 11;
        }
        if (createTank) {
            data.push((12 - at) ^ 1);
            at = 12;
        }
        if (createTagged) {
            data.push((13 - at) ^ 1);
            at = 13;
        }
        if (createTeamInfo) {
            data.push((14 - at) ^ 1);
            at = 14;
        }
        data.push(1);
        const binview = Module.exports.malloc(12);
        $(binview)[0].i32 = Module.exports.malloc(data.length);
        Module.HEAPU8.set(data, $(binview)[0].i32);
        $(binview)[4].i32 = data.length;
        $(binview)[8].i32 = 0;
        return binview;
    }
}

const Decoder = new TextDecoder();
const Encoder = new TextEncoder();

const getter = ({ ptr }, prop) => {
    switch(prop) {
        case "at":
        case "ptr": return ptr;
        case "u8": return Module.HEAPU8[ptr];
        case "i8": return Module.HEAP8[ptr];
        case "u16": return Module.HEAPU16[ptr >> 1];
        case "i16": return Module.HEAP16[ptr >> 1];
        case "u32": return Module.HEAPU32[ptr >> 2];
        case "i32": return Module.HEAP32[ptr >> 2];
        case "u64": return Module.HEAPU64[ptr >> 3];
        case "f32": return Module.HEAPF32[ptr >> 2];
        case "f64": return Module.HEAPF64[ptr >> 3];
        case "i64": return Module.HEAP64[ptr >> 3];
        case "u64": return Module.HEAPU64[ptr >> 3];
        case "utf8": return Decoder.decode(Module.HEAPU8.subarray(ptr, Module.HEAPU8.indexOf(0, ptr)));
        case "cstr": {
            let strAt = ptr;
            let length = Module.HEAPU8[ptr + 11];
            if (length === 0x80) {
                length = Module.HEAP32[(ptr + 4) >> 2];
                strAt = Module.HEAP32[ptr >> 2];
            }
            return Decoder.decode(Module.HEAPU8.subarray(strAt, strAt + length));
        }
        case "vector": return (type, size, args = null) => {
            const vector = [];
            for(let i = Module.HEAPU32[ptr >> 2]; i < Module.HEAPU32[(ptr + 4) >> 2]; i += size) vector.push(args ? $(i)[type](args) : $(i)[type])
            return vector;
        }
        case "struct": return fields => Object.fromEntries(fields.map(({name, offset, type, args}) => [name, args ? $(ptr + offset)[type](...args) : $(ptr + offset)[type]]));
        case "sll": return nodeOffset => { // singly linked list
            let node = $(ptr);
            const nodes = [];
            while(node[nodeOffset].u32 !== 0) nodes.push((node = node[nodeOffset].$));
            return nodes;
        }
        case "$cb": return cb => cb($(ptr));
        case "$": return $(Module.HEAPU32[ptr >> 2]);
    }
    const offset = parseInt(prop, 10);
    if(!Number.isNaN(offset)) return $(ptr + offset);
}

const setter = ({ ptr }, prop, to) => {
    if (to === undefined || to === null) return true;
    switch(prop) {
        case "u8":
            Module.HEAPU8[ptr] = to;
            return true;
        case "i8": 
            Module.HEAP8[ptr] = to;
            return true;
        case "u16":
            Module.HEAPU16[ptr >> 1] = to;
            return true;
        case "i16":
            Module.HEAP16[ptr >> 1] = to;
            return true;
        case "u32": 
            Module.HEAPU32[ptr >> 2] = to;
            return true;
        case "$": 
        case "i32":
            Module.HEAP32[ptr >> 2] = to;
            return true;
        case "u64": 
            Module.HEAPU64[ptr >> 3] = to;
            return true;
        case "f32": 
            Module.HEAPF32[ptr >> 2] = to;
            return true;
        case "f64":
            Module.HEAPF64[ptr >> 3] = to;
            return true;
        case "i64": 
            Module.HEAP64[ptr >> 3] = to;
            return true;
        case "u64": 
            Module.HEAPU64[ptr >> 3] = to;
            return true;
        case "utf8": {
            const buf = Encoder.encode(to.toString());
            Module.HEAPU8.set(buf, ptr);
            Module.HEAPU8[ptr + buf.byteLength] = 0;
            return true;
        }
        case "cstr": {
            const textBuf = Encoder.encode(to.toString());
            const len = textBuf.length;
            if(Module.HEAPU8[(ptr + 11) >> 0] === 0x80 && Module.HEAP32[ptr >> 2] < Module.HEAPU8 && Module.HEAP32[ptr >> 2] > DYNAMIC_TOP_PTR) Module.exports.free(Module.HEAP32[ptr >> 2]);
            Module.HEAPU8.set(new Uint8Array(12), ptr);
            if(len < 11) {
                Module.HEAPU8.set(textBuf, ptr);
                Module.HEAPU8[ptr + 11] = len;
                return true;
            }
            const strPtr = Module.exports.malloc(len + 1);
            Module.HEAPU8.set(textBuf, strPtr);
            Module.HEAPU8[strPtr + len] = 0;
            Module.HEAP32.set([strPtr, len, -0x7FFFFFE0], ptr >> 2);
            return true;
        }
        default:
            console.warn("Invalid property " + prop);
            return true;
    }
}

window.$ = ptr => new Proxy({ ptr }, { get: getter, set: setter });

$.write = (ptr, type, value) => {
    switch (type) {
        case "struct":
            return $.writeStruct(ptr, value);
        case "vector":
            return new $Vector(ptr, value.type, value.typeSize).push(...value.entries);
        case "cstr":
            Module.HEAP32.set([0, 0, 0], )
        default:
            return $(ptr)[type] = value;
    }
}

$.writeStruct = (ptr, fields) => {
    for (const {
            offset,
            type,
            value
        }
        of fields)
        $.write(ptr + offset, type, value);
}

index.html:
<!--
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
-->

<!DOCTYPE html>

<html>
    <head>
        <link rel="icon" href="https://raw.githubusercontent.com/ABCxFF/diepcustom/main/icon.png"/>
        <title>diep.io custom-server</title>
        
        <style>
          body {
            background-color: #000000;
          }

          #canvas {
            border: 0;
            margin: 0;
            padding: 0;
            overflow: hidden;
          }
    
          #loading {
            color: #FFFFFF;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 48pt;
            font-family: sans-serif;
            font-weight: bold;
            cursor: default;
          }
    
          #canvas {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            width: 100%;
            height: 100%;
            cursor: default;
          }
    
          #textInputContainer {
            display: none;
            position: absolute;
          }
    
          #textInput {
            background-color: transparent;
            font-family: "Ubuntu";
            padding: 0;
            border: 0;
            outline: none;
          }
        </style>

        <link href="https://fonts.googleapis.com/css?family=Ubuntu:700" rel="stylesheet" type="text/css" />
    </head>

    <body>
        <span id="loading">Loading...</span>
        <canvas id="canvas" width="800" height="800"></canvas>
        <div style="font-family:'Ubuntu'">&nbsp;</div>
        <div id="textInputContainer"><input id="textInput"></div>
        
        <!-- Third party library for dynmically modifying wasm files on the fly-->
        <script src ="https://cdn.jsdelivr.net/gh/Qwokka/wail.min.js@5e32d36bd7a5e0830d1ff4b64d3587aea13f77da/wail.min.js" crossorigin="anonymous"></script>

        <script src="./config.js"></script>
        <script src="./input.js"></script>
        <script src="./dma.js"></script>
        <script src="./loader.js"></script>
    </body>
</html>

input.js:
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

window.setupInput = () => {
    window.input = {
        mouse: Module.exports.mouse,
        keyDown: Module.exports.keyDown,
        keyUp: Module.exports.keyUp,
        blur: Module.exports.resetKeys,
        wheel: Module.exports.mouseWheel,
        prevent_right_click: Module.exports.preventRightClick,
        flushInputHooks: Module.exports.flushInputHooks,
        print_convar_help: Module.exports.printConsoleHelp,
        should_prevent_unload: Module.exports.hasTank,
        get_convar: key => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const res = Module.exports.getConvar(keyPtr);
            Module.exports.free(keyPtr);
            return res ? Module.UTF8ToString(res) : null;
        },
        set_convar: (key, val) => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const valPtr = Module.allocateUTF8(val.toString());
            const res = Boolean(Module.exports.setConvar(keyPtr, valPtr));
            Module.exports.free(keyPtr);
            Module.exports.free(valPtr);
            return res;
        },
        execute: cmd => {
            const cmdPtr = Module.allocateUTF8(cmd.toString());
            Module.exports.execute(cmdPtr);
            Module.exports.free(cmdPtr);
        }
    };

    const onMouseWheel = e => window.input.wheel(e.wheelDelta / -120);

    /firefox/i.test(navigator.userAgent) ? document.addEventListener("DOMMouseScroll", onMouseWheel) : document.body.onmousewheel = onMouseWheel;

    let isTyping = false;

    const scale = window.localStorage.getItem("no_retina") ? 1 : window.devicePixelRatio;
    const canvas = document.getElementById("canvas");
    const loading = document.getElementById('loading');

    canvas.onmousemove = e => window.input.mouse(e.clientX * scale, e.clientY * scale);
    
    canvas.onmousedown = e => {
        window.input.flushInputHooks();
        window.input.keyDown(e.button + 1);
    }

    canvas.onmouseup = e => {
        window.input.flushInputHooks();
        window.input.keyUp(e.button + 1);
    }

    window.onkeydown = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyDown(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    window.onkeyup = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyUp(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    canvas.onclick = window.onclick = () => window.input.flushInputHooks();

    canvas.ondragstart = e => e.preventDefault();

    canvas.oncontextmenu = e => window.input.prevent_right_click() ? e.preventDefault() : null;
    
    window.setLoadingStatus = str => loading.innerText = str;

    window.setTyping = val => isTyping = val;

    window.unscale = val => val / scale;

    window.onresize = () => {
        canvas.width = window.innerWidth * scale;
        canvas.height = window.innerHeight * scale;
    }

    window.onresize()
}

loader.js:
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

// make module globally accessable
window.Module = {};

// todolist
Module.todo = [];

// todo status
Module.status = null;

// is the todo list done?
Module.isRunning = false;

// has the module been aborted?
Module.isAborted = false;

// exception name
Module.exception = null;

// function index for dynamic calling of the main func
Module.mainFunc = null;

// content contexts
Module.cp5 = null;

// client input
window.input = null;

// arenas
Module.servers = null;

// colors
Module.colors = null;

// tanks
Module.tankDefinitions = null;
Module.tankDefinitionsTable = null;

// commands
Module.executeCommandFunctionIndex = null;
Module.executionCallbackMap = {};
Module.commandDefinitions = null;

// name input
Module.textInput = document.getElementById("textInput");
Module.textInputContainer = document.getElementById("textInputContainer");

// permission level is sent to client in the accept packet
Module.permissionLevel = -1;

// (polling) intervals, can be a number (ms), -1 aka never or -2 aka whenever a new connection is initiated  
Module.reloadServersInterval = 60000;
Module.reloadTanksInterval = -2;
Module.reloadCommandsInterval = -2;
Module.reloadColorsInterval = -2;

// Run frames via requestAnimationFrame or setTimeout
Module.scheduler = window.requestAnimationFrame;

// abort client
Module.abort = cause => {
    Module.isAborted = true;
    Module.isRunning = false;
    throw new WebAssembly.RuntimeError(`abort(${cause})`);
};

// run ASMConst method, basically replaces a lot of "real wasm imports"
Module.runASMConst = (code, sigPtr, argbuf) => {
    const args = [];
    let char;
    argbuf >>= 2;
    while(char = Module.HEAPU8[sigPtr++]) {
        const double = char < 105;
        if(double && argbuf & 1) argbuf++;
        args.push(double ? Module.HEAPF64[argbuf++ >> 1] : Module.HEAP32[argbuf])
        ++argbuf;
    }
    return ASMConsts[ASM_CONSTS[code]].apply(null, args);
};

// initializing the looper
Module.setLoop = func => {
    if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
    Module.mainFunc = func;
    Module.scheduler.apply(null, [Module.loop]);
};

// process todo
Module.run = async () => {
    let args = [];
    while(Module.todo.length) {
        const [func, isAsync] = Module.todo.shift();
        if(isAsync) args = await Promise.all(func(...args));
        else args = func(...args);
        console.log(`Running stage ${Module.status} done`);
    }
};

// looper, 1 animation frame = 1 main call, except for stack unwinds
Module.loop = () => {
    if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
    switch(Module.exception) {
        case null:
            Module.exports.dynCallV(Module.mainFunc);
            return Module.scheduler.apply(null, [Module.loop]);
        case "quit":
            return;
        case "unwind":
            Module.exception = null;
            return Module.scheduler.apply(null, [Module.loop]);
    }
};

// exit runtime (no unwind, originally unwind would be catched here)
Module.exit = status => {
    Module.exception = "quit";
    Module.isRunning = false;
    throw `Stopped runtime with status ${status}`;
};

// read utf8 from memory
Module.UTF8ToString = ptr => ptr ? Decoder.decode(Module.HEAPU8.subarray(ptr, Module.HEAPU8.indexOf(0, ptr))) : "";

// i/o write used for console, not fully understood
Module.fdWrite = (stream, ptr, count, res) => {
    let out = 0;
    for(let i = 0; i < count; i++) out += Module.HEAP32[(ptr + (i * 8 + 4)) >> 2];
    Module.HEAP32[res >> 2] = out;
};

// write utf8 to memory
Module.allocateUTF8 = str => {
    if(!str) return 0;
    const encoded = Encoder.encode(str);
    const ptr = Module.exports.malloc(encoded.byteLength + 1); // stringNT aka *char[]
    if(!ptr) return;
    Module.HEAPU8.set(encoded, ptr);
    Module.HEAPU8[ptr + encoded.byteLength] = 0;
    return ptr;
};

// Refreshes UI Components
Module.loadGamemodeButtons = () => {
    const vec = new $Vector(MOD_CONFIG.memory.gamemodeButtons, "struct", 28);
    if(vec.start) vec.destroy(); // remove old arenas
    // map server response to memory struct
    vec.push(...Module.servers.map(server => ([
        { offset: 0, type: "cstr", value: server.gamemode }, 
        { offset: 12, type: "cstr", value: server.name }, 
        { offset: 24, type: "i32", value: 0 }
    ])));
    Module.rawExports.loadVectorDone(MOD_CONFIG.memory.gamemodeButtons + 12); // toggle vector memory guard
};

// Refreshes UI Components
Module.loadChangelog = (changelog) => {
    const vec = new $Vector(MOD_CONFIG.memory.changelog, "cstr", 12);
    if(vec.start) vec.destroy(); // remove old changelog
    vec.push(...(changelog || CHANGELOG)); // either load custom or default
    $(MOD_CONFIG.memory.changelogLoaded).i8 = 1; // not understood
};

// Replaces current colors with serverside ones
Module.loadColors = () => {
    if(!window.input || !Module.colors) return;
    for(const [idx, color] of Object.entries(Module.colors)) {
        window.input.execute(`net_replace_color ${idx} ${color}`);
    }
};

// Ignore Hashtable, instead read from custom table
Module.getTankDefinition = tankId => {
    if(!Module.tankDefinitions) return 0;
    if(!Module.tankDefinitionsTable) Module.loadTankDefinitions(); // load tankdefs dynmically when requested
    if(!Module.tankDefinitionsTable[tankId]) return 0;
    return Module.tankDefinitionsTable[tankId] + 12; // 12 bytes for tankIds
};

Module.getCommand = cmdIdPtr => COMMANDS_LOOKUP[$(cmdIdPtr).cstr] || 0;

Module.loadTankDefinitions = () => {
    const writeTankDef = (ptr, tank) => {
        // Please note that this is not the full tank/barrel struct but just the portion needed for the client to function properly
        const barrels = tank.barrels ? tank.barrels.map(barrel => { // barrel fields
            return [
                { offset: 0, type: "f32", value: barrel.angle },
                { offset: 4, type: "f32", value: barrel.delay },
                { offset: 8, type: "f32", value: barrel.size },
                { offset: 12, type: "f32", value: barrel.offset },
                { offset: 16, type: "u8", value: Number(barrel.isTrapezoid) },
                { offset: 24, type: "f32", value: barrel.width / 42 },
                { offset: 56, type: "f32", value: barrel.bullet.sizeRatio },
                { offset: 60, type: "f32", value: barrel.trapezoidDirection },
                { offset: 64, type: "f32", value: barrel.reload },
                { offset: 96, type: "u32", value: ADDON_MAP[barrel.addon] || 0 }
            ];
        }) : [];

        const fields = [ // tankdef fields
            { offset: 4, type: "u32", value: tank.id },
            { offset: 8, type: "u32", value: tank.id },
            { offset: 12, type: "u32", value: tank.id },
            { offset: 16, type: "cstr", value: tank.name.toString() || "" },
            { offset: 28, type: "cstr", value: tank.upgradeMessage.toString() || "" },
            { offset: 40, type: "vector", value: { type: "u32", typeSize: 4, entries: tank.upgrades || [] } },
            { offset: 52, type: "vector", value: { type: "struct", typeSize: 100, entries: barrels } },
            { offset: 64, type: "u32", value: tank.levelRequirement || 0 },
            { offset: 76, type: "u8", value: Number(tank.sides === 4) },
            { offset: 93, type: "u8", value: Number(tank.sides === 16) },
            { offset: 96, type: "u32", value: ADDON_MAP[tank.preAddon] || 0 },
            { offset: 100, type: "u32", value: ADDON_MAP[tank.postAddon] || 0 },
        ];

        $.writeStruct(ptr, fields);
    };

    // TODO Rewrite with new $LinkedList datastructure
    Module.tankDefinitionsTable = new Array(Module.tankDefinitions.length).fill(0); // clear memory
    let lastPtr = MOD_CONFIG.memory.tankDefinitions;
    for(const tank of Module.tankDefinitions) {
        if(!tank) continue;
        const ptr = Module.exports.malloc(244); // length of a tankdef
        Module.HEAPU8.subarray(ptr, ptr + 244).fill(0);
        $(lastPtr).i32 = ptr;
        writeTankDef(ptr, tank);
        Module.tankDefinitionsTable[tank.id] = ptr;
        lastPtr = ptr;
    }

    $(MOD_CONFIG.memory.tankDefinitionsCount).i32 = Module.tankDefinitions.filter(e => Boolean(e)).length; // tankId xor based off this
};

// Executes a command callback from a command context
Module.executeCommand = execCtx => {
    const cmd = $(execCtx)[0].cstr;
    const tokens = $(execCtx)[12].vector("cstr", 12);
    
    if(!cmd || !tokens.length) throw `Invalid execution context (ptr: ${execCtx}) received`;
    if(typeof Module.executionCallbackMap[tokens[0]] !== "function") {
        if(!Module.commandDefinitions.find(({ id }) => id === tokens[0])) {
            throw `${Module.executionCallbackMap[tokens]} for command ${cmd} is an invalid callback`;
        }
        return Game.socket.send(new Uint8Array([
            6,
            ...Encoder.encode(tokens[0]), 0,
            tokens.slice(1).length,
            ...tokens.slice(1).flatMap(token => [...Encoder.encode(token), 0])
        ]));
    }

    // [id, ...args], we only need args
    Module.executionCallbackMap[tokens[0]](tokens.slice(1));
};

/*
    Command object: { id, usage, description, callback }
    The execute command function will not check for validity of arguments, you need to do that on your own
*/
Module.loadCommands = (commands = CUSTOM_COMMANDS) => {
    const cmdList = new $LinkedList(MOD_CONFIG.memory.commandList, "struct", 24);
    for(let { id, usage, description, callback, permissionLevel } of commands) {
        if(COMMANDS_LOOKUP[id] || permissionLevel > Module.permissionLevel) continue; // ignore duplicates

        // allocate Command
        const cmdPtr = Module.exports.malloc(40);
        $.writeStruct(cmdPtr, [
            { offset: 0, type: "cstr", value: id },
            { offset: 12, type: "cstr", value: usage || "" },
            { offset: 24, type: "cstr", value: description || "" },
            { offset: 36, type: "u32", value: Module.executeCommandFunctionIndex } // we handle every custom command with the same function
        ]);

        COMMANDS_LOOKUP[id] = cmdPtr;
        if(callback) Module.executionCallbackMap[id] = callback;

        // allocate HashNode
        cmdList.push([
            { offset: 0, type: "u32", value: 0 }, // next node
            { offset: 4, type: "u32", value: 0 }, // hash
            { offset: 8, type: "cstr", value: id }, // command id
            { offset: 20, type: "$", value: cmdPtr } // command def ptr
        ]);
    }
};

const wasmImports = {
    assertFail: (condition, filename, line, func) => Module.abort("Assertion failed: " + Module.UTF8ToString(condition) + ", at: " + [filename ? Module.UTF8ToString(filename) : "unknown filename", line, func ? Module.UTF8ToString(func) : "unknown function"]),
    mapFile: () => -1, // unused
    sysMunmap: (addr, len) => addr === -1 || !len ? -28 : 0, // not really used
    abort: Module.abort,
    asmConstsDII: Module.runASMConst,
    asmConstsIII: Module.runASMConst,
    exitLive: () => Module.exception = "unwind", // unwind stack
    exitForce: () => Module.exit(1), // exit / quit
    getNow: () => performance.now(),
    memCopyBig: (dest, src, num) => { Module.HEAPU8.copyWithin(dest, src, src + num) }, // for large packets
    random: () => Math.random(),
    resizeHeap: () => Module.abort("OOM"), // unable to resize wasm memory
    setMainLoop: Module.setLoop,
    envGet: () => 0, // unused
    envSize: () => 0, // unused
    fdWrite: Module.fdWrite, // used for diep client console
    roundF: d => d >= 0 ? Math.floor(d + 0.5) : Math.ceil(d - 0.5), // no, default Math.round doesn't work :D
    timeString: () => 0, // unused
    wasmMemory: new WebAssembly.Memory(WASM_MEMORY),
    wasmTable: new WebAssembly.Table(WASM_TABLE)
};

Module.todo.push([() => {
    Module.status = "PREPARE";
    // map imports to config
    Module.imports = { a: Object.fromEntries(Object.entries(WASM_IMPORTS).map(([key, name]) => [key, wasmImports[name]])) };
    return [];
}, false]);

Module.todo.push([() => {
    Module.status = "FETCH";
    // fetch necessary info and build
    return [
        fetch(`${CDN}build_${BUILD}.wasm.wasm`).then(res => res.arrayBuffer()),
        fetch(`${API_URL}servers`).then(res => res.json()),
        fetch(`${API_URL}tanks`).then(res => res.json())
    ];
}, true]);

Module.todo.push([(dependency, servers, tanks) => {
    Module.status = "INSTANTIATE";
    Module.servers = servers;
    Module.tankDefinitions = tanks;
    
    const parser = new WailParser(new Uint8Array(dependency));
    
    // original function, we want to modify these
    const originalVectorDone = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadVectorDone);
    const originalLoadChangelog = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadChangelog);
    const originalLoadGamemodeButtons = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadGamemodeButtons);
    const originalLoadTankDefs = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadTankDefinitions);
    const originalGetTankDef = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.getTankDefinition);
    const originalFindCommand = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.findCommand);
    const originalDecodeComponentList = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.decodeComponentList);
    const originalCreateEntityAtIndex = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.createEntityAtIndex);

    // function types
    const types = {
        // void []
        vn: parser.addTypeEntry({
            form: "func",
            params: [],
            returnType: null
        }),
        // void [int]
        vi: parser.addTypeEntry({
            form: "func",
            params: ["i32"],
            returnType: null
        }),
        // int [int]
        ii: parser.addTypeEntry({
            form: "func",
            params: ["i32"],
            returnType: "i32"
        })
    }

    // custom imports
    const imports = {
        loadGamemodeButtons: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "loadGamemodeButtons",
            kind: "func",
            type: types.vn 
        }),
        loadChangelog: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "loadChangelog",
            kind: "func",
            type: types.vn
        }),
        getTankDefinition: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "getTankDefinition",
            kind: "func",
            type: types.ii
        }),
        findCommand: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "findCommand",
            kind: "func",
            type: types.ii
        }),
        executeCommand: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "executeCommand",
            kind: "func",
            type: types.vi
        })
    };

    Module.imports.mods = {
        loadGamemodeButtons: Module.loadGamemodeButtons,
        loadChangelog: Module.loadChangelog,
        getTankDefinition: Module.getTankDefinition,
        findCommand: Module.getCommand,
        executeCommand: Module.executeCommand
    };

    for(const addonId of Object.keys(CUSTOM_ADDONS)) {
        imports["_addon_" + addonId] = parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "_addon_" + addonId,
            kind: "func",
            type: types.vi
        });

        parser.addExportEntry(imports["_addon_" + addonId], {
            fieldStr: "_addon_" + addonId,
            kind: "func"
        });

        Module.imports.mods["_addon_" + addonId] = (ptr) => {
            const input = $(ptr);
            if(!ptr || !input.i32) throw "Invalid pointer received on addon callback";
            CUSTOM_ADDONS[addonId](new $Entity(input.i32 !== ptr ? input.$ : input));
        };
    }

    parser.addExportEntry(imports.executeCommand, {
        fieldStr: "executeCommand",
        kind: "func"
    });

    parser.addExportEntry(originalVectorDone, {
        fieldStr: "loadVectorDone",
        kind: "func"
    });
    
    parser.addExportEntry(originalCreateEntityAtIndex, {
        fieldStr: "createEntityAtIndex",
        kind: "func"
    });

    parser.addExportEntry(originalDecodeComponentList, {
        fieldStr: "decodeComponentList",
        kind: "func"
    });

    const findConsecutiveSequenceIndex = (array, sequence) => {
        const indexes = [];
        for(let i = 0; i < array.length - sequence.length + 1; i++) {
            let found = true;
            for(let j = 0; j < sequence.length; j++) {
                if(array[i + j] !== sequence[j]) {
                    found = false;
                    break;
                }
            }
            if(found) {
                indexes.push(i);
            }
        }
        return indexes;
    }
    
    // parses & modifies code function by function
    parser.addCodeElementParser(null, function({ index, bytes }) {
        const ptrPattern = VarUint32ToArray(MOD_CONFIG.memory.netColorTable);
        const geuPattern = [OP_I32_CONST, 19, OP_I32_GE_U];
        const ltuPattern = [OP_I32_CONST, 19, OP_I32_LT_U];
        for(const idx of findConsecutiveSequenceIndex(bytes, ptrPattern)) {
            const arr = Array.from(bytes); // convert to normal array for splicing
            arr.splice(idx, ptrPattern.length, ...VarUint32ToArray(DYNAMIC_TOP_PTR + 4)); // using "empty" space
            bytes = new Uint8Array(arr); // convert back to u8[]
            let check = findConsecutiveSequenceIndex(bytes, geuPattern); // <= kMaxColors
            if(!check.length) {
                check = findConsecutiveSequenceIndex(bytes, ltuPattern); // invert, > kMaxColors
                for(const idx of check) {
                    const arr = Array.from(bytes);
                    arr.splice(idx, ltuPattern.length, OP_I32_CONST, ...VarSint32ToArray(-2), OP_I32_NE); // color code -2 = kMaxColors
                    bytes = new Uint8Array(arr);
                }
            } else {
                for(const idx of check) {
                    const arr = Array.from(bytes);
                    arr.splice(idx, geuPattern.length, OP_I32_CONST, ...VarSint32ToArray(-2), OP_I32_EQ); // color code -2 = kMaxColors
                    bytes = new Uint8Array(arr);
                }
            }
        }
        
        switch(index) {
            // modify load changelog function
            case originalLoadChangelog.i32(): // we only need the part where it checks if the changelog is already loaded to avoid too many import calls
                return new Uint8Array([
                    ...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.changelog),
                    OP_CALL, ...VarUint32ToArray(imports.loadChangelog.i32()),
                    OP_RETURN,
                    ...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.changelog)
                ]);
            // modify load gamemode buttons function
            case originalLoadGamemodeButtons.i32(): // we only need the part where it checks if the buttons are already loaded to avoid too many import calls
                return new Uint8Array([
                    ...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons),
                    OP_CALL, ...VarUint32ToArray(imports.loadGamemodeButtons.i32()),
                    OP_RETURN,
                    ...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons)
                ]);
            // overwrite get tankdef function
            case originalGetTankDef.i32(): // we modify this to call a js function which then returns the tank def ptr from a table
                return new Uint8Array([
                    OP_GET_LOCAL, 0,
                    OP_CALL, ...VarUint32ToArray(imports.getTankDefinition.i32()),
                    OP_RETURN,
                    OP_END
                ]);
            // overwrite find command function
            case originalFindCommand.i32():
                return new Uint8Array([
                    OP_GET_LOCAL, 0,
                    OP_CALL, ...VarUint32ToArray(imports.findCommand.i32()),
                    OP_RETURN,
                    OP_END
                ]);
            // delete tankdefs loading function
            case originalLoadTankDefs.i32(): // we dont want this to run anymore because it will call the original tank wrapper function
                return new Uint8Array([
                    OP_END
                ]);
            // no interesting index
            default:
                return bytes;
        }
    });

    // parse modded wasm
    parser.parse();
    // instantiate
    return [new Promise(resolve => WebAssembly.instantiate(parser.write(), Module.imports).then(res => resolve(res.instance), reason => Module.abort(reason)))];
}, true]);

Module.todo.push([instance => {
    Module.status = "INITIALIZE";
    // Exports
    Module.exports = Object.fromEntries(Object.entries(instance.exports).map(([key, func]) => [WASM_EXPORTS[key], func]));    
    Module.rawExports = instance.exports;
    // Memory
    Module.memBuf = wasmImports.wasmMemory.buffer,
    Module.HEAPU8 = new Uint8Array(Module.memBuf);
    Module.HEAP8 = new Int8Array(Module.memBuf);
    Module.HEAPU16 = new Uint16Array(Module.memBuf);
    Module.HEAP16 = new Int16Array(Module.memBuf);
    Module.HEAPU32 = new Uint32Array(Module.memBuf);
    Module.HEAP32 = new Int32Array(Module.memBuf);
    Module.HEAPF32 = new Float32Array(Module.memBuf);
    Module.HEAPF64 = new Float64Array(Module.memBuf);
    Module.HEAPU64 = new BigUint64Array(Module.memBuf);
    Module.HEAP64 = new BigInt64Array(Module.memBuf);
    // Cp5 Contexts
    Module.cp5 = {
        contexts: [],
        images: [],
        sockets: [],
        patterns: []
    };
    // window.input & misc, see input.js
    window.setupInput();
    return [];
}, false]);

Module.todo.push([() => {
    window.Game = {
        reloadColors: async () => {
            Module.colors = await fetch(`${API_URL}colors`).then(res => res.json());
            Module.loadColors();
        },
        // refetches servers & resets gamemode buttons
        reloadServers: async () => {
            Module.servers = await fetch(`${API_URL}servers`).then(res => res.json());
            Module.loadGamemodeButtons();
        },
        // refetches tankdefs & resets them
        reloadTanks: async () => {
            Module.tankDefinitions = await fetch(`${API_URL}tanks`).then(res => res.json());
            if(Module.tankDefinitionsTable) {
                for(const tankDef of Module.tankDefinitionsTable) {
                    if(tankDef) Module.exports.free(tankDef);
                }
            }
            Module.loadTankDefinitions();
        },
        reloadCommands: async () => {
            Module.commandDefinitions = await fetch(`${API_URL}commands`).then(res => res.json());
            Module.loadCommands(Module.commandDefinitions); // remote
            Module.loadCommands(); // local
        },
        // sets changelog (input: [...""])
        changeChangelog: (lines) => Module.loadChangelog(lines),
        // main socket, see also Module.cp5.sockets[0]
        get socket() {
            return Module.cp5.sockets[0];
        },
        // executes spawn command
        spawn: name => window.input.execute(`game_spawn ${name}`),
        // executes reconnect command
        reconnect: () => window.input.execute(`lb_reconnect`)
    };

    // custom commands
    Module.executeCommandFunctionIndex = Module.imports.a.table.grow(1);
    Module.imports.a.table.set(Module.executeCommandFunctionIndex, Module.rawExports.executeCommand);

    // custom addons
    for(const addonId of Object.keys(CUSTOM_ADDONS)) {
        ADDON_MAP[addonId] = Module.imports.a.table.grow(1);
        Module.imports.a.table.set(ADDON_MAP[addonId],  Module.rawExports["_addon_" + addonId]);
    }

    Module.status = "START";
    // emscripten requirements
    Module.HEAP32[DYNAMIC_TOP_PTR >> 2] = DYNAMIC_BASE;
    Module.isRunning = true;
    Module.exports.wasmCallCtors();
    Module.exports.main();


    const reloadServersInterval = () => setTimeout(() => {
        reloadServersInterval();
        if(Module.reloadServersInterval < 0) return;
        Game.reloadServers(); 
    }, Module.reloadServersInterval);
    reloadServersInterval();

    const reloadTanksInterval = () => setTimeout(() => {
        reloadTanksInterval();
        if(Module.reloadTanksInterval < 0) return;
        Game.reloadTanks();
    }, Module.reloadTanksInterval);
    reloadTanksInterval();

    const reloadCommandsInterval = () => setTimeout(() => {
        reloadCommandsInterval();
        if(Module.reloadCommandsInterval < 0) return;
        Game.reloadCommands();
    }, Module.reloadCommandsInterval);
    reloadCommandsInterval();
    
    const reloadColorsInterval = () => setTimeout(() => {
        reloadColorsInterval();
        if(Module.reloadColorsInterval < 0) return;
        Game.reloadColors();
    }, Module.reloadColorsInterval);
    reloadColorsInterval();
}, false]);


// Part of the original emscripten bootstrap
class ASMConsts {
    static createCanvasCtxWithAlpha(canvasId, alpha) {
        const canvas = document.getElementById(Module.UTF8ToString(canvasId));
        if(!canvas) return -1;
        const ctx = canvas.getContext("2d", {
            alpha: Boolean(alpha)
        });
        for (let i = 0; i < Module.cp5.contexts.length; ++i) {
            if (Module.cp5.contexts[i] !== null) continue;
            Module.cp5.contexts[i] = ctx;
            return i;
        }
        Module.cp5.contexts.push(ctx);
        return Module.cp5.contexts.length - 1;
    }

    static createImage(src) {
        const img = new Image;
        img.isLoaded = false;
        img.onload = () => img.isLoaded = true;
        img.src = `${CDN}${Module.UTF8ToString(src)}`;
        for (let i = 0; i < Module.cp5.images.length; ++i) {
            if (Module.cp5.images[i] !== null) continue;
            Module.cp5.images[i] = img;
            return i;
        }
        Module.cp5.images.push(img);
        return Module.cp5.images.length - 1;
    }

    static websocketSend(socketId, packetStart, packetLength) {
        const socket = Module.cp5.sockets[socketId];
        if(!socket || socket.readyState !== 1) return 0;
        try {
            socket.send(Module.HEAP8.subarray(packetStart, packetStart + packetLength));
        } catch(e) {}
        return 1;
    }

    static wipeContext(index) {
        Module.cp5.contexts[index] = null;
    }

    static modulo(a, b) {
        return a % b;
    }

    static wipeSocket(index) {
        const socket = Module.cp5.sockets[index];
        socket.onopen = socket.onclose = socket.onmessage = socket.onerror = function() {};
        for(let i = 0; i < socket.events.length; ++i) Module.exports.free(socket.events[i][1]);
        socket.events = null;
        try {
            socket.close();
        } catch(e) {}
        Module.cp5.sockets[index] = null;
    }

    static setTextInput(value) {
        Module.textInput.value = Module.UTF8ToString(value);
    }

    static wipeImage(index) {
        Module.cp5.images[index] = null;
    }

    static reloadWindowTimeout() {
        //setTimeout(() => window.location.reload(), 100);
    }

    static existsInWindowObject(key) {
        return Boolean(window[Module.UTF8ToString(key)]);
    }

    // 6 (ads)

    static getQueries() {
        const queryString = window.location.href.split("?")[0];
        return Module.allocateUTF8(queryString.slice(0, queryString.lastIndexOf("/")));
    }

    // 2 (ads)

    static getLocalStorage(key, length) {
        const keyStr = Module.UTF8ToString(key);
        let str = window.localStorage[keyStr] || "";
        if(
            keyStr === "gamemode"
            && Module.servers
            && Module.servers.length
            && !Module.servers.find(({ gamemode }) => window.localStorage[keyStr] === gamemode)
        ) str = Module.servers[0].gamemode;
        Module.HEAPU32[length >> 2] = str.length;
        return Module.allocateUTF8(str);
    }

    static deleteLocalStorage(key) {
        delete window.localStorage[Module.UTF8ToString(key)];
    }

    static removeChildNode(nodeId) {
        const node = document.getElementById(Module.UTF8ToString(nodeId));
        if(node && node.parentNode) node.parentNode.removeChild(node);
    }

    static checkElementProperty(elementId, propertyKey, propertyIndex, value) {
        const element = document.getElementById(Module.UTF8ToString(elementId));
        const key = Module.UTF8ToString(propertyKey);
        if(!element || !element[key]) return true;
        return element[key][Module.UTF8ToString(propertyIndex)] === Module.UTF8ToString(value);
    }

    static existsQueryOrIsBlank(query) {
        const elements = document.querySelectorAll(Module.UTF8ToString(query));
        for(let i = 0; i < elements.length; ++i)
            if(elements[i].src === "about:blank") return true;
        return elements.length === 0;
    }

    // 1 (ads)

    static setLocalStorage(key, valueStart, valueLength) {
        window.localStorage[Module.UTF8ToString(key)] = Decoder.decode(Module.HEAPU8.subarray(valueStart, valueStart + valueLength));
    }

    // 3 (ads)

    static getGamepad() {
        return window.navigator.getGamepads && window.navigator.getGamepads()[0]?.mapping === "standard";
    }

    static toggleFullscreen() {
        const requestMethod = document.body.requestFullScreen || document.body.webkitRequestFullScreen || document.body.mozRequestFullScreen || document.body.msRequestFullScreen;
        const cancelMethod = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen;
        if(!requestMethod && !cancelMethod) return;
        requestMethod ? requestMethod.call(document.body) : cancelMethod.call(document);
    }

    static getCanvasSize(ctxId, width, height) {
        const canvas = Module.cp5.contexts[ctxId].canvas;
        Module.HEAP32[width >> 2] = canvas.width;
        Module.HEAP32[height >> 2] = canvas.height;
    }

    static setCursorDefault() {
        document.getElementById("canvas").style.cursor = "default";
    }

    static setCursorPointer() {
        document.getElementById("canvas").style.cursor = "pointer";
    }

    static setCursorText() {
        document.getElementById("canvas").style.cursor = "text";
    }

    static getTextInput() {
        return Module.allocateUTF8(Module.textInput.value);
    }

    static enableTyping(left, top, width, height, enabled) {
        window.setTyping(true);
        Module.textInputContainer.style.display = "block";
        Module.textInputContainer.style.position = "absolute";
        Module.textInputContainer.style.left = window.unscale(left) + "px";
        Module.textInputContainer.style.top = window.unscale(top) + "px";
        Module.textInput.style.width = window.unscale(width * 0.96) + "px";
        Module.textInput.style.height = window.unscale(height) + "px";
        Module.textInput.style.lineHeight = window.unscale(height * 0.9) + "px";
        Module.textInput.style.fontSize = window.unscale(height * 0.9) + "px";
        Module.textInput.style.paddingLeft = "5px";
        Module.textInput.style.paddingRight = "5px";
        Module.textInput.disabled = !enabled;
        Module.textInput.focus();
    }

    static disableTyping() {
        window.setTyping(false);
        Module.textInput.blur();
        Module.textInput.value = "";
        Module.textInputContainer.style.display = "none";
    }

    static focusCanvas() {
        const canvas = document.getElementById("canvas");
        if(document.activeElement && document.activeElement !== canvas) document.activeElement.blur()
        canvas.focus();
    }

    static setCanvasSize(ctxId, width, height) {
        const canvas = Module.cp5.contexts[ctxId].canvas;
        canvas.width = width;
        canvas.height = height;
    }

    // 1 (ads)

    static copyUTF8(original) {
        return Module.allocateUTF8(Module.UTF8ToString(original));
    }

    static alert(text) {
        window.alert(Module.UTF8ToString(text));
    }

    static saveContext(ctxId) {
        Module.cp5.contexts[ctxId].save();
    }

    static restoreContext(ctxId) {
        Module.cp5.contexts[ctxId].restore();
    }

    static scaleContextAlpha(ctxId, alpha) {
        Module.cp5.contexts[ctxId].globalAlpha *= alpha;
    }

    // 5 (ads)

    static setContextFillStyle(ctxId, r, g, b) {
        Module.cp5.contexts[ctxId].fillStyle = "rgb(" + r + "," + g + "," + b + ")";
    }

    static setContextTransform(ctxId, a, b, c, d, e, f) {
        Module.cp5.contexts[ctxId].setTransform(a, b, c, d, e, f);
    }

    static contextFillRect(ctxId) {
        Module.cp5.contexts[ctxId].fillRect(0, 0, 1, 1);
    }

    static contextBeginPath(ctxId) {
        Module.cp5.contexts[ctxId].beginPath();
    }

    static contextClip(ctxId) {
        Module.cp5.contexts[ctxId].clip();
    }

    static contextFill(ctxId) {
        Module.cp5.contexts[ctxId].fill();
    }

    static setContextLineJoinRound(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "round";
    }

    static setContextLineJoinBevel(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "bevel";
    }

    static setContextLineJoinMiter(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "miter";
    }

    static setContextLineWidth(ctxId, width) {
        Module.cp5.contexts[ctxId].lineWidth = width;
    }

    static setContextStrokeStyle(ctxId, r, g, b) {
        Module.cp5.contexts[ctxId].strokeStyle = "rgb(" + r + "," + g + "," + b + ")";
    }

    static setContextTransformBounds(ctxId, a, b, c, d) {
        Module.cp5.contexts[ctxId].setTransform(a, b, c, d, 0, 0);
    }

    static contextStroke(ctxId) {
        Module.cp5.contexts[ctxId].stroke();
    }

    // draws one pixel
    static contextRect(ctxId) {
        Module.cp5.contexts[ctxId].rect(0, 0, 1, 1);
    }

    static getFontsLoaded() {
        return document.fonts.check("1px Ubuntu");
    }

    static setContextFont(ctxId, fontSize) {
        Module.cp5.contexts[ctxId].font = fontSize + "px Ubuntu";
    }

    static measureContextTextWidth(ctxId, text) {
        return Module.cp5.contexts[ctxId].measureText(Module.UTF8ToString(text)).width;
    }

    static setContextAlpha(ctxId, alpha) {
        Module.cp5.contexts[ctxId].globalAlpha = alpha;
    }

    static contextFillText(ctxId, text) {
        Module.cp5.contexts[ctxId].fillText(Module.UTF8ToString(text), 0, 0);
    }

    static contextStrokeText(ctxId, text) {
        Module.cp5.contexts[ctxId].strokeText(Module.UTF8ToString(text), 0, 0);
    }

    static setContextTextBaselineTop(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "top";
    }

    static setContextTextBaselineHanging(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "hanging";
    }

    static setContextTextBaselineMiddle(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "middle";
    }

    static setContextTextBaselineAlphabetic(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "alphabetic";
    }

    static setContextTextBaselineIdeographic(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "ideographic";
    }

    static setContextTextBaselineBottom(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "bottom";
    }

    static setContextTransformNormalize(ctxId) {
        Module.cp5.contexts[ctxId].setTransform(1, 0, 0, 1, 0, 0);
    }

    static contextMoveTo(ctxId, x, y) {
        Module.cp5.contexts[ctxId].moveTo(x, y);
    }

    static contextLineTo(ctxId, x, y) {
        Module.cp5.contexts[ctxId].lineTo(x, y);
    }

    static contextClosePath(ctxId) {
        Module.cp5.contexts[ctxId].closePath();
    }

    static contextArc(ctxId, startAngle, endAngle, counterclockwise) {
        Module.cp5.contexts[ctxId].arc(0, 0, 1, startAngle, endAngle, counterclockwise)
    } 

    static copyToKeyboard(text) {
        window?.navigator?.clipboard?.writeText(Module.UTF8ToString(text));
    }

    static setLocation(newLocation) {
        // open in new tab instead
        window.open(Module.UTF8ToString(newLocation));
    }

    static contextDrawImage(ctxId, imgId) {
        const img = Module.cp5.images[imgId];
        if(!img.isLoaded || img.width === 0 || img.height === 0) return;
        Module.cp5.contexts[ctxId].drawImage(img, 0, 0, img.width, img.height, 0, 0, 1, 1);
    }

    static getImage(imgId, isLoaded, width, height) {
        const img = Module.cp5.images[imgId];
        Module.HEAPU8[isLoaded >> 0] = img.isLoaded;
        Module.HEAP32[width >> 2] = img.width;
        Module.HEAP32[height >> 2] = img.height;
    }

    static contextDrawCanvas(ctxId, targetCtxId) {
        Module.cp5.contexts[ctxId].drawImage(Module.cp5.contexts[targetCtxId].canvas, 0, 0);
    }

    static setContextLineCapButt(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "butt";
    }

    static setContextLineCapRound(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "round";
    }

    static setContextLineCapSquare(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "square";
    }

    static contextStrokeRect(ctxId) {
        Module.cp5.contexts[ctxId].strokeRect(0, 0, 1, 1);
    }

    static contextDrawFullCanvas(ctxId, targetCtxId) {
        const canvas = Module.cp5.contexts[targetCtxId].canvas;
        Module.cp5.contexts[ctxId].drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, 1, 1);
    }
    
    static isContextPatternAvailable() {
        return Boolean(CanvasRenderingContext2D.prototype.createPattern);
    }

    static createContextPattern(ctxId, targetCtxId) {
        const pattern = Module.cp5.contexts[ctxId].createPattern(Module.cp5.contexts[targetCtxId].canvas, null);
        for (let i = 0; i < Module.cp5.patterns.length; ++i) {
            if (Module.cp5.patterns[i] !== null) continue;
            Module.cp5.patterns[i] = pattern;
            return i;
        }
        Module.cp5.patterns.push(pattern);
        return Module.cp5.patterns.length - 1;
    }

    static contextGetPixelColor(ctxId, x, y) {
        const data = Module.cp5.contexts[ctxId].getImageData(x, y, 1, 1);
        return data.data[0] << 16 | data.data[1] << 8 | data.data[2];
    }

    static contextDrawCanvasSourceToPixel(ctxId, targetCtxId, x, y, w, h) {
        Module.cp5.contexts[ctxId].drawImage(Module.cp5.contexts[targetCtxId].canvas, x, y, w, h, 0, 0, 1, 1);
    }

    static contextFillRectWithPattern(ctxId, patternId, width, height) {
        Module.cp5.contexts[ctxId].fillStyle = Module.cp5.patterns[patternId];
        Module.cp5.contexts[ctxId].fillRect(0, 0, width, height);
    }

    static wipePattern(patternId) {
        Module.cp5.patterns[patternId] = null;
    }

    // 2 (verifying bootstrap integrity ?)

    static existsQuery(query) {
        return document.querySelector(Module.UTF8ToString(query)) !== null;
    }

    // 1 (anticheat)

    // used for shadow root
    static canvasHasSamePropertyAsDocumentBody(property) {
        const propertyKey = Module.UTF8ToString(property);
        return document.getElementById("canvas")[propertyKey] !== document.body[propertyKey];
    }

    // used for shadow root
    static existsDocumentBodyProperty(property) {
        return document.body[Module.UTF8ToString(property)] !== undefined;
    }

    // used for shadow root
    static existsDocumentBodyProperty2(property) {
        return Boolean(document.body[Module.UTF8ToString(property)]);
    }

    // used for shadow root
    static existsDivPropertyAndEqualsPropertyOnDocumentBody(propertyDiv, propertyBody) {
        const propertyDivKey = Module.UTF8ToString(propertyDiv);
        const div = document.createElement("div");
        if(!div[propertyDivKey]) return;
        return div[propertyDivKey]() === document.body[Module.UTF8ToString(propertyBody)];
    }

    // 3 (anticheat)

    // anticheat but need to be kept
    static acCheckWindow(property) {
        if(Module.UTF8ToString(property) === "navigator") return true;
    }

    static getDocumentBody() {
        return Module.allocateUTF8(document.body.innerHTML);
    }

    // 2 (anticheat)

    static getUserAgent() {
        return Module.allocateUTF8(window.navigator.userAgent);
    }

    // 1 (anticheat)

    static getQuerySelectorToString() {
        return Module.allocateUTF8("function querySelector() { [native code] }");
    }
    
    static getFillTextToString() {
        return Module.allocateUTF8("function fillText() { [native code] }");
    }

    static getStrokeRectToString() {
        return Module.allocateUTF8("function strokeRect() { [native code] }");
    }

    static getStrokeTextToString() {
        return Module.allocateUTF8("function strokeText() { [native code] }");
    }

    static getScaleToString() {
        return Module.allocateUTF8("function scale() { [native code] }");
    }

    static getTranslateToString() {
        return Module.allocateUTF8("function translate() { [native code] }");
    }

    static getFillRectToString() {
        return Module.allocateUTF8("function fillRect() { [native code] }");
    }

    static getRotateToString() {
        return Module.allocateUTF8("function rotate() { [native code] }");
    }

    static getGetImageDataToString() {
        return Module.allocateUTF8("function getImageData() { [native code] }");
    }

    // 1 (ads)

    static contextClearRect(ctxId) {
        const ctx = Module.cp5.contexts[ctxId];
        const canvas = ctx.canvas;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    static createCanvasCtx() {
        const ctx = document.createElement("canvas").getContext("2d");
        for(let i = 0; i < Module.cp5.contexts.length; ++i) {
            if(Module.cp5.contexts[i]) continue;
            Module.cp5.contexts[i] = ctx;
            return i; 
        }
        Module.cp5.contexts.push(ctx);
        return Module.cp5.contexts.length - 1;
    }

    static setContextMiterLimit(ctxId, miterLimit) {
        Module.cp5.contexts[ctxId].miterLimit = miterLimit;
    }

    static getWindowLocation() {
        return Module.allocateUTF8(window.location.hash);
    }

    static setLoadingStatus(status) {
        if(window.setLoadingStatus) window.setLoadingStatus(Module.UTF8ToString(status));
    }

    static m28nReply(requestId, endpoint) {
        if(!Module.servers || !Module.servers.length) return console.warn("No Servers Found");
        const server = Module.servers.find(({ gamemode }) => gamemode === Module.UTF8ToString(endpoint).slice(7)) || Module.servers[0];
        const id = Module.allocateUTF8(server.gamemode);
        const ipv4 = Module.allocateUTF8(server.gamemode);
        const ipv6 = Module.allocateUTF8(server.gamemode);
        Module.exports.restReply(requestId, id, ipv4, ipv6);
        Module.exports.free(id);
        Module.exports.free(ipv4);
        Module.exports.free(ipv6);
    }

    static isSSL() {
        return false;
    }

    static createWebSocket(urlPtr) {
        const url = Module.UTF8ToString(urlPtr);
        const ws = new WebSocket(`ws${location.protocol.slice(4)}//${location.host}/${url.slice(5, url.length - 4)}`);
        ws.binaryType = "arraybuffer";
        ws.events = [];
        ws.onopen = function() {
            ws.events.push([2, 0, 0]);
            Module.exports.checkWS();
        };
        ws.onerror = function() {
            ws.events.push([3, 0, 0]);
            Module.exports.checkWS();
        };
        ws.onclose = function({ reason }) {
            ws.events.push([4, 0, 0]);
            Module.exports.checkWS();
            if(reason) console.log("WebSocket closed due to:", reason);
        };
        ws.onmessage = function(e) {
            const view = new Uint8Array(e.data);
            if(view[0] === 7) {
                let out = 0, i = 0, at = 1;
                while(view[at] & 0x80) {
                    out |= (view[at++] & 0x7f) << i;
                    i += 7;
                }
                out |= (view[at++] & 0x7f) << i;
                Module.permissionLevel = (0 - (out & 1)) ^ (out >>> 1);
                window.Game.reloadCommands();
            }
            const ptr = Module.exports.malloc(view.length);
            Module.HEAP8.set(view, ptr);
            ws.events.push([1, ptr, view.length]);
            Module.exports.checkWS();
        };
        for (let i = 0; i < Module.cp5.sockets.length; ++i) {
            if (Module.cp5.sockets[i] != null)
                continue;
            Module.cp5.sockets[i] = ws;
            return i;
        }

        if(Module.reloadServersInterval === -2) Game.reloadServers();
        if(Module.reloadTanksInterval === -2) Game.reloadTanks(); 
        if(Module.reloadCommandsInterval === -2) Game.reloadCommands();
        if(Module.reloadColorsInterval === -2) Game.reloadColors();

        Module.cp5.sockets.push(ws);
        return Module.cp5.sockets.length - 1;
    }

    static findServerById(requestId, endpoint) {
        Module.exports.restReply(requestId, 0, 0, 0);
    }

    static invalidPartyId() {
        alert("Invalid party ID");
    }

    static wipeLocation() {
        window.location.hash = "";
    }

    static getGamepadAxe(axeId) {
        const axes = window.navigator.getGamepads()[0].axes;
        if(axeId >= axes.length) return;
        return axes[axeId];
    }

    static getGamepadButtonPressed(buttonId) {
        const buttons = window.navigator.getGamepads()[0].buttons;
        if(buttonId >= buttons.length) return;
        return buttons[buttonId].pressed;
    }

    static pollWebSocketEvent(socketId, msg, length) {
        const ws = Module.cp5.sockets[socketId];
        if(ws.events.length === 0) return null;
        const event = ws.events.shift();
        Module.HEAPU32[msg >> 2] = event[1]; // packet ptr
        Module.HEAP32[length >> 2] = event[2]; // packet length
        return event[0]; // type
    }

    static updateToNewVersion(version) {
        console.log(Module.UTF8ToString(version));
        setTimeout(() => window.location.reload());
    }

    // 1 (pow)

    static reloadWindow() {
        setTimeout(() => window.location.reload());
    }

    static getWindowLocationSearch() {
        return Module.allocateUTF8(window.location.search);
    }

    static getWindowReferrer() {
        return Module.allocateUTF8(window.document.referrer);
    }

    // 7 (fingerprinting)

    static empty() {}
}

Module.run();
`