Socket Server

zombsSockets.js — a 305-line WebSocket server (port 8100) that pools WASM anti-bot solver instances so a browser full of Alts doesn't have to load a copy each.

Every connection a Banshee client opens to zombs.io — your main player, every Alt, every headless Session — has to pass the same anti-bot "blend field" challenge (opcodes 5 and 10) using the same zombs_wasm.wasm module the official client embeds. Loading and running that module once per character would be expensive; loading it a dozen times in one browser tab for a wall of alts would be worse. The Socket Server's entire reason to exist is to be the place that loads it once per logical bot, in a separate process, and answers challenges on request over a tiny text protocol.

WebSocket.Server — port 8100 maxPayload 65536 password-gated, 15s grace windows

Connecting — clientId, password, and reattachment

A new socket is provisionally registered the instant it connects (ws.clientId = ++clientId), and immediately told its id. It has 15 seconds to send a matching clientId <id> <password> reply or the server closes it — a short authentication window, not an open relay:

zombsSockets.js — connection bootstrap & the verification window
wss.on("connection", (ws) => {
    ws.clientId = ++clientId;
    ws.actualId = ws.clientId;
    if (!clients[ws.clientId]) {
        clients[ws.clientId] = {};
        setTimeout(() => {
            if (!ws.verified) {
                ws.close();
            }
        }, 15000);
    }
    if (!clients[ws.clientId].modules) {
        clients[ws.clientId].modules = {};
    }
    ws.sendMessage(`clientId ${ws.clientId}`);
    ws.on("message", (m) => {
        const msg = decode(m);
        switch (msg.split(" ")[0].replaceAll(" ", "")) {
            case "clientId":
                const clientId = msg.split(" ")[1];
                const password = msg.split(" ")[2];
                if (password === socketServerPassword) {
                    if (ws.clientId != clientId && clients[clientId]) {
                        ws.actualId = clientId;
                        if (!clients[ws.actualId].isActive) {
                            clients[ws.actualId].isActive = true;
                        }
                    }
                    ws.verified = true;
                }
                break;
            // … createModule / decodeOpcode5 / decodeOpcode10 / altClosed / changePassword
        }
    });
});

The ws.actualId branch is the interesting bit: a verified connection can claim a different, already-known clientId as its real identity — letting a page reload or reconnect "rejoin" its previous module pool instead of starting from scratch with an empty one. This is the same identity-recovery pattern ban.connect relies on when a tab refreshes mid-session: the WASM modules already loaded for that tab's alts don't have to be rebuilt from nothing.

The control protocol — five commands

Past the handshake, the entire surface area of the server is five short text commands — each one a single line, each mapping onto the lifecycle of a pooled wasmmodule() instance keyed by the Alt's random wid:

zombsSockets.js — createModule / decodeOpcode5 / decodeOpcode10 / altClosed
case "createModule":
    if (!ws.verified) return;
    const moduleId = msg.split(" ")[1];
    clients[ws.actualId].modules[moduleId] = wasmmodule();
    break;
case "decodeOpcode5":
    const opcode5id = msg.split(" ")[1];
    const opcode5data = `[${msg.split(" ")[2]}]`;
    const hostname = msg.split(" ")[3];
    if (clients[ws.actualId].modules[opcode5id]) {
        clients[ws.actualId].modules[opcode5id].onDecodeOpcode5(new Uint8Array(JSON.parse(opcode5data)), hostname, decodedopcode5 => {
            ws.sendMessage(`opcode4 ${opcode5id} ${new Uint8Array(decodedopcode5[5])} ${decodedopcode5[6]}`);
        });
    }
    break;
case "decodeOpcode10":
    const opcode10id = msg.split(" ")[1];
    const opcode10data = `[${msg.split(" ")[2]}]`;
    if (clients[ws.actualId].modules[opcode10id]) {
        const module = clients[ws.actualId].modules[opcode10id].finalizeOpcode10(new Uint8Array(JSON.parse(opcode10data)));
        ws.sendMessage(`opcode10 ${opcode10id} ${module}`);
    }
    break;
case "altClosed":
    const altId = msg.split(" ")[1];
    if (clients[ws.actualId].modules[altId]) {
        delete clients[ws.actualId].modules[altId];
    }
    gc();
    break;

This is the exact other half of the exchange seen in Alt.onMessage on the multibox page — the client sends decodeOpcode5 <wid> <bytes> <hostname> <name>, and gets back opcode4 <wid> <bytes> <bytes>, which the Alt forwards straight into the game socket as its world-entry response. Notice the wire format itself: rather than a binary frame, the bytes are serialized by template-literal-stringifying a Uint8Array (${new Uint8Array(...)} → a comma-separated decimal string like "5,12,200,7"), then re-parsed on the other end with JSON.parse(`[${...}]`) — a slightly unusual choice, but one that lets the whole protocol stay plain text and trivially loggable, which matters when the entire point of this server is debugging a finicky, version-sensitive anti-bot handshake.

Cleanup — grace windows on both ends

altClosed is the clean-shutdown path (an Alt tells the server it's done, the module is freed, gc() runs immediately — recall setFlagsFromString("--expose_gc") at the top of the file exists specifically to make that call available). But browser tabs close ungracefully too, so onclose implements a second, coarser cleanup with its own 15-second grace window before it tears down an entire client's module pool:

zombsSockets.js — onclose (delayed teardown, in case of a fast reconnect)
ws.onclose = () => {
    clients[ws.actualId].isActive = false;
    setTimeout(() => {
        if (!clients[ws.clientId].isActive) {
            for (let i in clients[ws.clientId].modules) {
                delete clients[ws.clientId].modules[i];
            }
            delete clients[ws.clientId];
            gc();
        }
        if (clients[ws.actualId] && !clients[ws.actualId].isActive) {
            for (let i in clients[ws.actualId].modules) {
                delete clients[ws.actualId].modules[i];
            }
            delete clients[ws.actualId];
            gc();
        }
    }, 15000);
}

The isActive flag is what makes this a grace window rather than an immediate teardown: closing flips it to false, but the actual deletion is deferred 15 seconds and re-checks the flag first. If the same logical client (by actualId) reconnects and re-verifies within that window, isActive flips back to true and the scheduled cleanup becomes a no-op — so a quick page refresh doesn't force every alt to redo the expensive WASM instantiation and challenge-solving dance from zero. Both clientId and actualId pools get checked and swept independently, since a reattached connection can leave the original numeric id's pool orphaned.

Why pool at all — what wasmmodule() actually costs

Each pooled instance is a full WebAssembly.instantiate of zombs_wasm.wasm (read once into wasmbuffers at startup and reused as the instantiation source for every module), wired up with the same browser-faking import shim documented in depth on the architecture pagecstr() answering the handful of fixed strings the WASM module probes for (Game.currentGame.network.connected, document.getElementById("hud").children.length, and so on) with hand-picked constants that satisfy the module without a real window or document ever existing:

zombsSockets.js — cstr (the same browser-faking shim as the Session Saver's copy)
const cstr = (str) => {
    if (str.startsWith('typeof window === "undefined" ? 1 : 0')) return 0;
    if (str.startsWith("typeof process !== 'undefined' ? 1 : 0")) return 0;
    if (str.startsWith("Game.currentGame.network.connected ? 1 : 0")) return 1;
    if (str.startsWith("Game.currentGame.network.connectionOptions.ipAddress")) return Module.hostname;
    if (str.startsWith("Game.currentGame.world.myUid === null ? 0 : Game.currentGame.world.myUid")) return ((uid++) ? 1 : 0);
    if (str.startsWith('document.getElementById("hud").children.length')) return 24;
}

Standing this up — reading the 1-2MB .wasm binary, instantiating it, running its init exports, and warming its internal heap — isn't free, and a serious multibox session might run a dozen or more alts simultaneously. clients[id].modules is therefore a small per-connection registry mapping each alt's wid to its own live Module, instantiated once on createModule and reused for every subsequent decodeOpcode5/decodeOpcode10 call that alt needs — which is the entire performance argument for running this as a separate Node process rather than inline in the page: one process, one loaded .wasm buffer, many cheap module instances, and a a clean place to gc() them away the moment an alt disconnects.

Changing the password

The last command, changePassword, mirrors the same pattern seen on Session Saver's changehasaccess — a verified client can rotate the shared secret at runtime (capped at 50 characters), with the new value logged server-side rather than persisted anywhere a casual reader of the source would find it:

zombsSockets.js — changePassword
case "changePassword":
    if (!ws.verified) return;
    const pswd = msg.split(" ")[1];
    if (pswd && pswd.length <= 50 && socketServerPassword !== pswd) {
        socketServerPassword = pswd;
        console.log(`New socket server password: ${socketServerPassword}`);
    }
    break;