Multibox (Sockets / Alts)

class Alt — extra characters spawned inside your own browser tab, each driving its own WebSocket connection and its own copy of the bot tick loop.

Banshee calls its in-browser multibox characters Alts (the code and chat commands also call them Sockets — hence window.sockets, socketsByUid, !send). Don't confuse them with Sessions: an Alt is a second (third, fourth, …) WebSocket opened from your own page to the same zombs.io server, wrapped in a purpose-built class that re-implements just enough of the game client to log in, join your party, and run a slimmed-down copy of the automations from the main script. Close the tab and every Alt disappears with it — for bots that survive a closed browser, see Session Saver.

class Alt — script.js:4461 spawned with new Alt() key bind L · command !send

Constructor — a second client in one object

Each Alt opens its own WebSocket straight to game.network.connectionOptions.host (the same server your main character is on), gets its own codec instance off game.networkType, and tracks its own entity map, harvester map, inventory, and AHRC tick table — a private copy of the same harvesterTicks schedule documented on the AHRC page. The random wid ("WASM id") is how the Socket Server keeps each Alt's anti-bot solver state separate:

script.js — Alt constructor & outbound packets
class Alt {
    constructor() {
        this.ws = new WebSocket(`wss://${game.network.connectionOptions.host}`);
        this.ws.binaryType = "arraybuffer";
        this.ws.onmessage = this.onMessage.bind(this);
        this.ws.onclose = this.onClose.bind(this);
        this.codec = new game.networkType().codec;
        this.codec.isAlt = true;
        this.entity = new Map();
        this.harvesters = new Map();
        this.mousePs = ban.mousePs;
        this.timeout1Ticks = 0;
        this.timeout2Ticks = 0;
        this.timeout3Ticks = 0;
        this.hitTicks = 0;
        this.inventory = {};
        this.harvesterTicks = [
            { tick: 0, resetTick: 31, deposit: 0.4, tier: 1 },
            { tick: 0, resetTick: 29, deposit: 0.6, tier: 2 },
            // … tiers 3-8, identical schedule to the main script's AHRC table
        ];
        this.id = ++ban.counts;
        this.wid = Math.round(Math.random() * 10 ** 16);
    }
    sendPacket(event, data) {
        if (this.ws && this.ws.readyState === 1) {
            this.ws.send(new Uint8Array(this.codec.encode(event, data)));
        }
    }
}

On creation, every live Alt is registered in two global maps that the rest of script.js reads from constantly: window.sockets (keyed by this.id, an incrementing counter) and, once it's actually in the world, window.socketsByUid (keyed by its in-game player uid, used so the main script's autoaim/autofollow logic can skip targeting your own alts).

Routing the WebSocket — onMessage

Just like the real client, every inbound frame is a binary packet whose first byte is an opcode. Alt decodes it with its own codec and switches on that byte — entity updates (0), world entry (4), the anti-bot blend-field challenge (5 and 10), and RPC responses (9):

script.js — Alt.onMessage
async onMessage(msg) {
    const opcode = new Uint8Array(msg.data)[0];
    const m = new Uint8Array(msg.data);
    let data;
    try {
        data = this.codec.decode(msg.data);
    } catch (e) { };
    switch (opcode) {
        case 0:
            this.onEntityUpdate(data);
            break;
        case 4:
            this.onEnterWorld(data);
            break;
        case 5:
            sockets[this.id] = this;
            for (let num = 0; num < Object.keys(socketServers).length; num++) {
                if (!this.isMade) {
                    this.isMade = true;
                    socketServers[num].alts[this.wid] = true;
                    this.serverNum = [ban.connect(num)];
                    const wasmmoduleMessage = `createModule ${this.wid}`;
                    if (socketServers[this.serverNum].socket.readyState === 1) {
                        socketServers[this.serverNum].socket.sendMessage(wasmmoduleMessage);
                    } else {
                        ban.messagesToSend.push(wasmmoduleMessage);
                    }
                }
            }
            if (this.serverNum) {
                const opcode5Message = `decodeOpcode5 ${this.wid} ${m} ${game.network.connectionOptions.hostname} ${ban.altName}`;
                if (socketServers[this.serverNum].socket.readyState === 1) {
                    socketServers[this.serverNum].socket.sendMessage(opcode5Message);
                } else {
                    ban.messagesToSend.push(opcode5Message);
                }
            } else {
                this.ws.send(0);
            }
            break;
        case 9:
            this.onRpc(data);
            break;
        case 10:
            const opcode10Message = `decodeOpcode10 ${this.wid} ${m}`;
            if (socketServers[this.serverNum].socket.readyState === 1) {
                socketServers[this.serverNum].socket.sendMessage(opcode10Message);
            } else {
                ban.messagesToSend.push(opcode10Message);
            }
            break;
    }
}

Opcode 5/10 — outsourcing the anti-bot puzzle. The zombs.io server sends every connecting client a "blend field" challenge (opcode 5) it must answer correctly to be allowed past the loading screen, and a follow-up (opcode 10) partway through the session. A real browser solves this with a WASM module baked into the official client; Alt doesn't embed that module itself — it forwards the raw challenge bytes (m), the connection hostname, and the alt's display name to the Socket Server over a control WebSocket as a plain text command (decodeOpcode5 <wid> <bytes> <hostname> <name>), which runs the same wasmmodule() shim documented on the architecture page and relays the answer back. This is exactly why a separate Socket Server process exists: solving the challenge per-alt in the page thread would be slow and would multiply the WASM memory footprint by the number of alts open.

Entering the world — onEnterWorld

Once the server admits the connection (data.allowed), the Alt marks itself as a real player, registers in socketsByUid, and immediately fires off a fixed bring-up sequence: face a direction, join your party by share key, then buy and equip HatHorns, PetCARL, and PetMiner — the same starter loadout PetCARL reasoning applies to here, since every alt benefits from the same passive resource trickle:

script.js — Alt.onEnterWorld (bring-up sequence)
onEnterWorld(data) {
    ban.players = data.players;
    if (data.allowed) {
        this.myPlayer = true;
        this.uid = data.uid;
        if (this.enterworld2) this.ws.send(this.enterworld2);
        socketsByUid[this.uid] = { id: this.id, x: 0, y: 0 };
        this.sendPacket(3, { mouseMoved: 15 });
        this.sendPacket(9, { name: "JoinPartyByShareKey", partyShareKey: game.ui.playerPartyShareKey });
        this.sendPacket(9, { name: "BuyItem", itemName: "HatHorns", tier: 1 });
        this.sendPacket(9, { name: "BuyItem", itemName: "PetCARL", tier: 1 });
        this.sendPacket(9, { name: "BuyItem", itemName: "PetMiner", tier: 1 });
        this.sendPacket(9, { name: "EquipItem", itemName: "PetCARL", tier: 1 });
        this.sendPacket(9, { name: "EquipItem", itemName: "PetMiner", tier: 1 });
        this.sendPacket(3, { up: 1 });
        if (typeof partyInfo !== "undefined") {
            game.ui.components.Map.onPartyMembersUpdate(partyInfo);
        }
        game.ui.components.Map.onPartyMembersUpdate(ban.partyInfoAlt);
    } else {
        this.ws.send(0);
    }
    game.ui.components.PopupOverlay.showHint(`${ban.players === 1 ? "1 player" : `${ban.players} players`} | ${Object.keys(socketsByUid).length === 1 ? "1 socket" : `${Object.keys(socketsByUid).length} sockets`}`);
}

That hint popup and the document.title rewrite (BAN_TAB - players, sockets / BAN_SESSION #id - players, sockets) are how Banshee gives you an at-a-glance headcount across every tab and session without opening the menu — useful when you're juggling a dozen alts across multiple browser windows.

The Alt's own tick loop — onEntityUpdate

Every entity-update packet (opcode 0) drives a miniature copy of the main script's automation tick — the same shape as the Bot tick loop on Session Saver, just scoped to the toggles that make sense for a follower character. After it finds and caches its own player entity, it recomputes its aim from the shared ban.mousePs cursor position (unless positionlock is on, in which case the alt keeps its own independent mousePs):

script.js — Alt.onEntityUpdate (aiming & movement)
this.myPlayer = this.entity.get(this.uid);
if (!this.myPlayer) return;
socketsByUid[this.uid] = { id: this.id, x: this.myPlayer.position.x, y: this.myPlayer.position.y };
if (!ban.scripts.positionlock && !this.isLocked) {
    this.mousePs = ban.mousePs;
}
this.aimingYaw = Math.floor((Math.atan2(this.mousePs.y - this.myPlayer.position.y, this.mousePs.x - this.myPlayer.position.x) * 180 / Math.PI + 450) % 360) || 0;
this.sendPacket(3, { mouseMoved: this.aimingYaw });
if (ban.scripts.mousemove && !this.isFrozen) {
    const x = (Math.round(((Math.atan2(this.mousePs.y - this.myPlayer.position.y, this.mousePs.x - this.myPlayer.position.x) * 180 / Math.PI + 450) % 360) / 45) * 45) % 360;
    let movementPacket;
    if (!ban.scatter && !this.scatter) {
        movementPacket = { up: (x === 0 || x === 45 || x === 315) ? 1 : 0, down: (x === 135 || x === 180 || x === 225) ? 1 : 0, right: (x === 45 || x === 90 || x === 135) ? 1 : 0, left: (x === 225 || x === 270 || x === 315) ? 1 : 0 };
    } else {
        movementPacket = this.justDied
            ? { up: 1, down: 0, right: 0, left: 1 }
            : { down: (x === 0 || x === 45 || x === 315) ? 1 : 0, up: (x === 135 || x === 180 || x === 225) ? 1 : 0, left: (x === 45 || x === 90 || x === 135) ? 1 : 0, right: (x === 225 || x === 270 || x === 315) ? 1 : 0 };
    }
    this.sendPacket(3, movementPacket);
}

mousemove is what makes a pile of alts actually walk toward your cursor instead of standing still — it snaps the angle to 8 compass directions and turns that into a WASD-style input packet. The "scatter" branch is a death-recovery behavior: when an alt dies next to a live gold stash (this.gs && !data.response.stashDied, set in onRpc's "Dead" handler below), it inverts its movement for a few ticks so respawning alts don't immediately re-cluster on the same spot and block each other from re-entering the base.

The rest of the tick is a flat list of toggle checks — each one a smaller, alt-scoped sibling of a feature documented in depth on the main script page:

Toggle (read from ban.scripts)What the Alt does
autoheal (and not xkey)Buys/equips HealthPotion once health drops to ≤20%, mirroring Auto Heal
autorevivepetsKeeps PetRevive bought and equipped while a pet is active
autobowPulses { space: 0/1 } to draw and release a Bow, or holds mouseDown for melee weapons — the alt-side Auto Bow
autoaltjoinJoins the party while a friendly gold stash (this.gs) exists nearby, leaves it otherwise — keeps "farmer" alts grouped only while there's something to farm
autospearLeaves the party once gold or Spear tier crosses a threshold (ban.requiredGold / ban.spearTier) — used to cycle alts in and out of a Spear-upgrade rotation
chatspamSends ban.chatSpamMessage to Local chat every tick
xkeyBuys then equips a configurable weapon (ban.xKeyWeapon) the moment gold allows
autoshield / autotimeout / autopetpotion / autopethealSame gold-gated buy/equip loops as the main script's AITO and pet-care toggles

And if this.ahrc is set on the individual alt, it runs the exact same deposit/collect driver documented in depth on the AHRC section of the main script page — proof that the harvester-farming logic was written once and reused verbatim across the player, the alt, and the headless Bot:

script.js — Alt AHRC driver (identical shape to the main script & Bot)
depositAhrc(tick) {
    this.harvesters.forEach((e) => {
        if (e.tier === tick.tier) {
            this.sendPacket(9, { name: "AddDepositToHarvester", uid: e.uid, deposit: tick.deposit });
        }
    });
}
collectAhrc(tick) {
    this.harvesters.forEach((e) => {
        if (e.tier === tick.tier) {
            this.sendPacket(9, { name: "CollectHarvester", uid: e.uid });
        }
    });
}

Reacting to RPCs — onRpc

The same way the main script keeps a live mirror of buildings and inventory from RPC pushes, each Alt tracks just enough state to drive its own automations: which buildings exist near it (LocalBuilding populates this.harvesters and flips this.gs for "is there a friendly gold stash"), its party share key, its inventory contents, and — notably — death:

script.js — Alt.onRpc ("Dead" → scatter)
case "Dead":
    if (ban.scripts.autorespawn) {
        this.sendPacket(3, { respawn: 1 });
    }
    if (this.gs && !data.response.stashDied) {
        this.justDied = true;
        this.scatter = 1;
    }
    break;

That's the other half of the scatter behavior seen above: dying near a stash that's still alive sets scatter = 1, which the tick loop counts up to 7 ticks while inverting the alt's movement, then clears justDied — giving a respawned character a few steps of breathing room before it resumes normal pathing back toward the cursor.

Spawning, closing, and reconnecting

An Alt's life is bookended by three entry points, all converging on the same constructor call:

TriggerWhereBehavior
Key bind LonKeyDown, case "KeyL"Spawns one Alt — but only while you're actually in the world (game.world.inWorld)
Chat command !send <n>onSendRpc interpreterSpawns up to 5 Alts in one shot (args[1] > 5 ? 5 : args[1] — a hard ceiling so a typo can't fork-bomb your tab)
autorefiller + dayFillerMain tick loopSpawns a fresh Alt automatically whenever it's daytime in-game and the refill cycle (arfTicks === 0) comes around — see the Auto Refiller writeup on the base automations page
autoreconnectAlt.onCloseIf the closing socket belonged to a real player (this.myPlayer) and auto-reconnect is on, immediately opens a replacement: new Alt()
script.js — Alt.onClose (cleanup & auto-reconnect)
onClose() {
    delete sockets[this.id];
    delete opcode5Ids[this.wid];
    if (this.uid) {
        delete socketsByUid[this.uid];
        // … decrements ban.players, refreshes the hint popup & document.title
    }
    const onCloseMessage = `altClosed ${this.wid}`;
    if (this.serverNum) {
        // … tells the Socket Server to free this alt's WASM module slot
        if (socketServers[this.serverNum].alts[this.wid]) {
            delete socketServers[this.serverNum].alts[this.wid];
        }
    }
    if (ban.scripts.autoreconnect && this.myPlayer) {
        new Alt();
    }
}

Notice the cleanup runs before the spawn check — every map (sockets, opcode5Ids, socketsByUid, the Socket Server's alts table) is unwound first, so a long chain of disconnect → reconnect cycles never leaks stale entries that would otherwise make ban.players or the on-screen counters drift from reality.

The Multibox settings tab

The settings menu's Multibox group exposes the toggles that matter once you actually have alts on screen — these gate the behaviors threaded through onEntityUpdate above:

script.js — tabs schema, "Multibox" group
{
    title: "Multibox",
    items: [
        ["mousemove", "Mouse Move"],
        ["positionlock", "Position Lock"],
        ["wasd", "WASD"],
        ["autofollow", "Auto Follow"]
    ]
}
  • Mouse Move (mousemove) — the master switch for the WASD-style movement packets shown above; without it, alts aim at your cursor but never walk.
  • Position Lock (positionlock) — freezes each alt's mousePs at whatever it was when the toggle flipped, so they stop following your live cursor and instead hold their current heading — handy for parking a wall of alts in a doorway.
  • Auto Follow (autofollow) — feeds into the main player's own targeting logic (alongside autoaim) to steer toward the nearest tracked player, which alts then inherit through the shared cursor position.

A neighboring Tools group adds alt-aware utilities like Player Info (showrss — overlays each alt's numeric id on its nameplate so you can tell them apart at a glance), Score Logger, and Auto Alt Join (autoaltjoin — the party-join/leave behavior documented above), all driven from the same ban.scripts flag object as every other toggle in the script.