Chat Commands

onSendRpc — Banshee hijacks the chat box itself, turning every !-prefixed message into a script command instead of a network packet.

zombs.io's network layer exposes a single outbound hook: game.network.sendRpc, called every time the client wants to ask the server to do something — including SendChatMessage. Banshee saves a reference to the original (sendRpc2) and replaces it with its own interceptor. That's the entire mechanism behind the command system — no new UI, no new event listeners, just a chat box that secretly doubles as a command line:

script.js — replacing the RPC hook
game.network.sendRpc2 = game.network.sendRpc;
game.network.sendRpc = (e) => {
    this.onSendRpc(e);
}

The interpreter — ! vs !!

onSendRpc only does anything special for SendChatMessage RPCs whose message starts with !. It lower-cases the message, splits on spaces, and switches on the first token. Everything else — and any chat message that doesn't start with ! — falls through to game.network.sendRpc2(e), the original handler, so normal chat keeps working untouched:

script.js — onSendRpc dispatch
onSendRpc(e) {
    if (!game.world.inWorld) return;
    if (e.name === "SendChatMessage") {
        if (e.message.startsWith("!")) {
            const msg = e.message.toLowerCase();
            const args = msg.split(" ");
            const alt = sockets[args[1]];
            switch (msg.split(" ")[0].replaceAll(" ", "")) {
                // … 100+ cases, see below
            }
            return;
        } else {
            game.network.sendRpc2(e);
        }
    }
    // … MakeBuilding interception happens after this block, see "Placement spam" below
}

Two conventions run through almost every command in the switch:

  • ! enables, !! disables. The convention is so consistent that the documentation's own manual (BansheeFunctions.txt) writes most entries as a single line — !ab / !!ab — AUTO BUILD — rather than describing the pair separately.
  • args[1] doubles as a Socket lookup. const alt = sockets[args[1]]; runs before the switch even starts, so any command that takes a numeric argument can transparently target a specific in-browser Alt by its id instead of your main player — !pahrc 3 turns AHRC on for socket #3 specifically, for example.

The Session relay — e/d prefixes

A whole family of commands — !ab, !au, !atb, !aa, !apr, !ape, !aph, !aaz, !aad, and more — don't run any logic in the browser at all. They only matter when you're attached to a Session (user.connectedToId is set — see the session-attach flow), and all they do is forward a short text command over the relay socket:

script.js — Session command relay (a representative slice)
case "!ab":
    if (user.connectedToId) {
        user.sendMessage("eab");
    }
    break;
case "!!ab":
    if (user.connectedToId) {
        user.sendMessage("dab");
    }
    break;
case "!au":
    if (user.connectedToId) {
        user.sendMessage("eau");
    }
    break;
case "!!au":
    if (user.connectedToId) {
        user.sendMessage("dau");
    }
    break;
// … !atb/!aa/!apr/!ape/!aph/!aaz/!aad follow the exact same e‑/d‑prefix pattern

The prefix is the whole protocol: e means "enable", d means "disable", followed by the same short command code used in the chat trigger. zombsSessions.js has a mirror-image switch on the other end that turns "eab" back into bot.scripts.autobuild = true on the headless Bot instance — documented with the server-side code on the Session Saver commands section. According to Banshee's own manual, this family covers:

CommandRelay codesPer BansheeFunctions.txt
!ab / !!abeab / dab"Auto rebuild your current base"
!au / !!aueau / dau"Auto upgrade your current base to its current tiers"
!atb / !!atbeatb / datb"Auto attack with the bow"
!aa / !!aaeaa / daa"Aimbot for players"
!apr / !!apreapr / dapr"Auto revive your pet"
!ape / !!apeeape / dape"Auto evolve your pet"
!aph / !!apheaph / daph"Auto heal your pet below 70% HP"
!aaz / !!aazeaaz / daaz"Aimbot for zombies"
!aad / !!aadeaad / daad"Aimbot for demons"

Base recording & auto-build — !record / !arb / !aru

This trio is Banshee's base-replication system, and it's a good example of a command that does real, non-trivial work entirely client-side using localStorage as a tiny database. !record [name] walks every building you currently own, converts each one's world coordinates into grid-relative offsets from your gold stash ((x - this.gs.x) / 24 — 24 being half a building tile), and serializes the whole layout — position, type, facing yaw, and tier — into a hand-built JSON string keyed by the name you gave it:

script.js — !record (serializing the base layout into localStorage)
case "!record":
    if (args[1] && Object.keys(game.ui.buildings).length > 1) {
        localStorage[`ban.${args[1]}`] = "";
        const rebuilder = new Map();
        for (let i in game.ui.buildings) {
            rebuilder.set(
                (game.ui.buildings[i].x - this.gs.x) / 24 + (game.ui.buildings[i].y - this.gs.y) / 24 * 1000,
                [(game.ui.buildings[i].x - this.gs.x) / 24, (game.ui.buildings[i].y - this.gs.y) / 24,
                 game.ui.buildings[i].type,
                 (game.world.entities.get(game.ui.buildings[i].uid) ? game.world.entities.get(game.ui.buildings[i].uid).targetTick.yaw : 0),
                 game.ui.buildings[i].tier]
            );
        }
        // … then walks `rebuilder` building a literal JSON string, entry by entry
    }
    break;

The composite map key (x + y * 1000) is a cheap trick to get buildings sorted in row-major order without writing a comparator — it folds a 2D grid coordinate into one sortable number, the same pattern used throughout the base-builder automations on the userscript page.

!arb [name] ("auto rebuild") loads that JSON back, rebuilds the same rebuilder/ reupgrader maps, and then diffs them against your current buildings — anything in the saved layout that doesn't already exist at the matching offset goes into inactiveRebuilder for the auto-build loop to place. !aru [name] ("auto re-upgrade") does the narrower job of just comparing tiers and queuing upgrades, without trying to place new buildings:

script.js — !arb (diffing the saved layout against reality)
case "!arb":
    if (args[1] && localStorage[`ban.${args[1]}`]) {
        this.targetBase = JSON.parse(localStorage[`ban.${args[1]}`]);
        this.inactiveRebuilder = new Map();
        this.inactiveReupgrader = new Map();
        this.rebuilder = new Map();
        this.reupgrader = new Map();
        for (let i in this.targetBase) {
            this.rebuilder.set(this.targetBase[i][0] + this.targetBase[i][1] * 1000, [this.targetBase[i][0], this.targetBase[i][1], this.targetBase[i][2], this.targetBase[i][3]]);
            this.reupgrader.set(this.targetBase[i][0] + this.targetBase[i][1] * 1000, [this.targetBase[i][0], this.targetBase[i][1], this.targetBase[i][4]]);
            if (!Object.values(game.ui.buildings).find((x) => x.x === this.targetBase[i][0] * 24 + this.gs.x && x.y === this.targetBase[i][1] * 24 + this.gs.y)) {
                if (this.targetBase[i][2] !== "GoldStash") {
                    this.inactiveRebuilder.set(this.targetBase[i][0] + this.targetBase[i][1] * 1000, this.targetBase[i]);
                }
            }
        }
        this.scripts.autobuild = true;
    }
    break;
case "!!arb":
    this.scripts.autobuild = false;
    break;

!delete [name] rounds out the trio by removing a saved layout from localStorage. Per Banshee's own manual: "!record [BASE] — Store your current base, by giving it a name in Local Storage", "!arb [BASE] / !!arb — Auto build your stored base, using its name in Local Storage", and "!aru [BASE] / !!aru — Auto upgrade your stored base, to its stored tiers, using its name in Local Storage".

Placement interception — wall & harvester spam

The Walls 3x3/5x5/7x7/9x9 and Harvs 4x4/8x8 toggles work completely differently from the chat-triggered automations — they hook the MakeBuilding RPC itself, so the single building placement you click becomes a whole pattern of placements around your cursor. The check runs right after the chat-command switch returns, on every outbound RPC named MakeBuilding:

script.js — wall-spam interception (snaps to a 48px grid, radius from the active toggle)
if (e.name === "MakeBuilding") {
    if (game.ui.components.PlacementOverlay.buildingId === "Wall" && e.type === "Wall"
        && (this.scripts.walls3x3 || this.scripts.walls5x5 || this.scripts.walls7x7 || this.scripts.walls9x9)
        && game.inputManager.mouseDown) {
        const worldPos = game.renderer.screenToWorld(game.ui.mousePosition.x, game.ui.mousePosition.y);
        const gridPos = { x: ((worldPos.x / 48 | 0) + 0.5) * 48, y: ((worldPos.y / 48 | 0) + 0.5) * 48 };
        const z = this.scripts.walls3x3 ? 1 : this.scripts.walls5x5 ? 2 : this.scripts.walls7x7 ? 3 : this.scripts.walls9x9 ? 4 : 0;
        if (z > 0) {
            for (let x = -z; x < z + 1; x++) {
                for (let y = -z; y < z + 1; y++) {
                    game.network.sendPacket(9, { name: e.name, type: e.type, x: gridPos.x + x * 48, y: gridPos.y + y * 48, yaw: e.yaw });
                }
            }
        }
    } else if (game.ui.components.PlacementOverlay.buildingId === "Harvester" && e.type === "Harvester" && game.inputManager.mouseDown) {
        const worldPos = game.renderer.screenToWorld(game.ui.mousePosition.x, game.ui.mousePosition.y);
        const gridPos = { x: Math.round(worldPos.x / 48) * 48, y: Math.round(worldPos.y / 48) * 48 };
        if (this.scripts.harvs4x4) {
            game.network.sendPacket(9, { name: e.name, type: e.type, x: gridPos.x + 96, y: gridPos.y, yaw: e.yaw });
            game.network.sendPacket(9, { name: e.name, type: e.type, x: gridPos.x, y: gridPos.y + 96, yaw: e.yaw });
            game.network.sendPacket(9, { name: e.name, type: e.type, x: gridPos.x - 96, y: gridPos.y, yaw: e.yaw });
            game.network.sendPacket(9, { name: e.name, type: e.type, x: gridPos.x, y: gridPos.y - 96, yaw: e.yaw });
        } else if (this.scripts.harvs8x8) {
            // … 8 more sendPacket calls, ringing the cursor at 48px/144px offsets in each direction
        }
    }
}

The intent, per the manual, is unambiguously offensive-PvP: "[Walls 3x3] — Spam walls in a 3 by 3 grid radius to trap enemy players" and "[Harvs 8x8] — Place 8 Harvesters around your mouse. Can trap multiple players." The size toggles are mutually exclusive within their own family (only one wall radius or one harvester pattern can be active), enforced the same way as every other toggle group via setScriptToggle — see the toggle system. The numeric shorthand commands !1x1!9x9 are just direct flag flips for the same toggles — !1x1/!2x2 turn everything off, the rest turn one pattern on and the others off:

script.js — numeric shortcuts (!3x3 example — note the explicit reset of siblings)
case "!1x1":
    this.scripts.walls3x3 = false;
    this.scripts.walls5x5 = false;
    this.scripts.walls7x7 = false;
    this.scripts.walls9x9 = false;
    break;
case "!3x3":
    this.scripts.walls3x3 = true;
    this.scripts.walls5x5 = false;
    this.scripts.walls7x7 = false;
    this.scripts.walls9x9 = false;
    break;

Recon — !ft, !fs, !stashes, !players

Banshee maintains a passive map of every entity it has ever seen in allEntities (populated from the entity-update stream — see onEntitiesUpdateHandler on the Session Saver tick-loop page for the server-side twin of this map). !ft ("find top players") and !fs ("find top stashes") are read-only commands that mine that cache and echo results straight into your own chat window via game.ui.components.Chat.onMessageReceived — no network packet leaves the client at all:

script.js — !fs (echoes every known T-tier gold stash into chat)
case "!fs":
    allEntities.forEach((e) => {
        if (e.tier) {
            const tier = e.tier === 1 ? "T1" : e.tier === 2 ? "T2" : e.tier === 3 ? "T3" : e.tier === 4 ? "T4"
                       : e.tier === 5 ? "T5" : e.tier === 6 ? "T6" : e.tier === 7 ? "T7" : e.tier === 8 ? "T8" : "";
            game.ui.components.Chat.onMessageReceived({
                displayName: e.name,
                message: `X ${this.counter(e.position.x)}, Y ${this.counter(e.position.y)}, PID ${e.partyId} (${game.ui.parties[e.partyId] ? game.ui.parties[e.partyId].memberCount : 1}) ${tier}`,
                uid: e.uid
            });
        }
    });
    break;

!ft runs the same scan but for entities without a tier (i.e. players, not stashes) and additionally cross-references the live leaderboard (game.ui.components.Leaderboard.leaderboardData) to append each player's numeric rank — and tags them with their party's gold-stash tier when one is known, so a single glance tells you both "who" and "how strong is their base". !stashes and !players are the live equivalents, calling straight into the scanner object (class ScannerAPI, documented on the architecture page) to pull fresh positional data for the current server rather than relying on the passive cache.

Multibox commands

A handful of commands manage the Alt fleet directly from chat rather than the key binds:

script.js — !send (bulk-spawns Alts, capped at 5)
case "!send":
    const amt = args[1] > 5 ? 5 : args[1];
    for (let i = 0; i < amt; i++) {
        new Alt();
    }
    break;
case "!servers":
    for (let i in socketServers) {
        const server = socketServers[i];
        const id = i;
        const alts = Object.keys(server.alts).length;
        game.ui.components.PopupOverlay.showHint(`Socket server ID: ${id} | Sockets: ${alts}`);
    }
    break;
  • !send [amount] — "Send a set amount of Sockets to the server, from 1 to 5" (manual). The hard ceiling of 5 is baked into the ternary, not just documentation — passing a larger number silently clamps.
  • !servers — "Socket server stats": pops a hint per configured Socket Server showing how many WASM-pool slots (alts) it currently has reserved.
  • !an [name] / !!an — "Set the player name that your Sockets join with… without a set name sets a Blank name… !!an toggles it back to your default name" — rewrites this.altName, which flows straight into the decodeOpcode5 message shown on the multibox page so the Socket Server can log/identify each alt by its display name.
  • !pahrc [alt] / !!pahrc [alt] — "AHRC used by your player and your Sockets… !pahrc [ALT] enables it for your Socket": when args[1] resolves through the sockets[args[1]] lookup to a live Alt, the flag is set on that Alt (alt.ahrc = true) instead of the global this.scripts.ahrc — the per-alt AHRC switch referenced on the multibox page.