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.
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:
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):
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:
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):
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 |
autorevivepets | Keeps PetRevive bought and equipped while a pet is active |
autobow | Pulses { space: 0/1 } to draw and release a Bow, or holds mouseDown for melee weapons — the alt-side Auto Bow |
autoaltjoin | Joins 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 |
autospear | Leaves 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 |
chatspam | Sends ban.chatSpamMessage to Local chat every tick |
xkey | Buys then equips a configurable weapon (ban.xKeyWeapon) the moment gold allows |
autoshield / autotimeout / autopetpotion / autopetheal | Same 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:
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:
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:
| Trigger | Where | Behavior |
|---|---|---|
| Key bind L | onKeyDown, case "KeyL" | Spawns one Alt — but only while you're actually in the world (game.world.inWorld) |
Chat command !send <n> | onSendRpc interpreter | Spawns 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 + dayFiller | Main tick loop | Spawns 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 |
autoreconnect | Alt.onClose | If the closing socket belonged to a real player (this.myPlayer) and auto-reconnect is on, immediately opens a replacement: new Alt() |
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:
{
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'smousePsat 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 (alongsideautoaim) 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.