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:
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:
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 itsidinstead of your main player —!pahrc 3turns 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:
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:
| Command | Relay codes | Per BansheeFunctions.txt |
|---|---|---|
!ab / !!ab | eab / dab | "Auto rebuild your current base" |
!au / !!au | eau / dau | "Auto upgrade your current base to its current tiers" |
!atb / !!atb | eatb / datb | "Auto attack with the bow" |
!aa / !!aa | eaa / daa | "Aimbot for players" |
!apr / !!apr | eapr / dapr | "Auto revive your pet" |
!ape / !!ape | eape / dape | "Auto evolve your pet" |
!aph / !!aph | eaph / daph | "Auto heal your pet below 70% HP" |
!aaz / !!aaz | eaaz / daaz | "Aimbot for zombies" |
!aad / !!aad | eaad / 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:
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:
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:
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:
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:
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…!!antoggles it back to your default name" — rewritesthis.altName, which flows straight into thedecodeOpcode5message 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": whenargs[1]resolves through thesockets[args[1]]lookup to a live Alt, the flag is set on that Alt (alt.ahrc = true) instead of the globalthis.scripts.ahrc— the per-alt AHRC switch referenced on the multibox page.