Localhost Server

zombsLocalhost.js — the Express app that serves Banshee's modded client.

This is the smallest of Banshee's three Node processes (75 lines) and the one you actually point your browser at. It does four things: serve the modded client and its static assets, generate an asset manifest for texture preloading, proxy the official zombs.io leaderboard, and serve the WASM anti-bot module.

Express ^5.2.1 port 80 · HTTP no WebSocket

Static serving & entry point

Everything under public/ (the client bundle, app.js, script.js, pixi.js, CSS, ~1,000 textures) is served as-is via express.static; the bare / route returns client.html directly from the project root rather than from public/:

zombsLocalhost.js
const app = express();
app.use(express.static("public"));

app.get("/", (req, res) => {
    const options = { root: path.join(__dirname) };
    res.sendFile("client.html", options);
});

app.get("/zombs_wasm.wasm", (req, res) => {
    const options = { root: path.join(__dirname) };
    res.sendFile("zombs_wasm.wasm", options);
});

app.listen(80);

Texture manifest generation

client.html preloads roughly a thousand custom textures before letting you into the game (window.gameTexturePreloadPromise = fetch("/manifest.json")…). Rather than maintaining that list by hand, the server walks public/images recursively at request time and returns every image file it finds, with Cache-Control: no-store so the manifest always reflects what's actually on disk:

zombsLocalhost.js — recursive asset discovery
const imagesRoot = path.join(__dirname, "public", "images");
const getPictureAssets = (directory = imagesRoot) => {
    const assets = [];
    for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
        const fullPath = path.join(directory, entry.name);
        if (entry.isDirectory()) {
            assets.push(...getPictureAssets(fullPath));
        } else if (/\.(svg|png|ico)$/i.test(entry.name)) {
            const relativePath = path.relative(path.join(__dirname, "public"), fullPath).replace(/\\/g, "/");
            assets.push(`./${relativePath}`);
        }
    }
    return assets;
}

app.get("/manifest.json", (req, res) => {
    res.set("Cache-Control", "no-store");
    res.json(getPictureAssets());
});

Leaderboard proxy

Because the modded client runs on localhost, it can't fetch zombs.io/leaderboard/data directly without running into CORS — so the server fetches it server-side and hands the JSON back. It validates the category/time query params against an allow-list, caches each combination for 60 seconds in memory, and aborts the upstream request after 5 seconds:

zombsLocalhost.js — proxy with caching & timeout
const leaderboardCache = new Map();
const leaderboardCategories = new Set(["wave", "score"]);
const leaderboardTimes = new Set(["24h", "7d", "all"]);

app.get("/zombs-leaderboard", async (req, res) => {
    const category = leaderboardCategories.has(req.query.category) ? req.query.category : "wave";
    const time = leaderboardTimes.has(req.query.time) ? req.query.time : "24h";
    const cacheKey = `${category}:${time}`;
    const cached = leaderboardCache.get(cacheKey);
    res.set("Cache-Control", "no-store");
    if (cached && Date.now() - cached.createdAt < 60000) {
        res.json(cached.data);
        return;
    }
    try {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 5000);
        const response = await fetch(`https://zombs.io/leaderboard/data?category=${category}&time=${time}`, {
            headers: {
                "Accept": "application/json",
                "User-Agent": "<redacted — see note below>"
            },
            signal: controller.signal
        });
        clearTimeout(timeout);
        if (!response.ok) {
            res.status(response.status).json({ status: "error", parties: [] });
            return;
        }
        const data = await response.json();
        leaderboardCache.set(cacheKey, { createdAt: Date.now(), data });
        res.json(data);
    } catch (error) {
        res.status(502).json({ status: "error", parties: [] });
    }
});

Source note: the shipped file hard-codes a racial slur as the literal value of the "User-Agent" header in this handler. It has been replaced with <redacted — see note below> in the excerpt above so it isn't reproduced here. This is a property of Banshee's own source code (zombsLocalhost.js, around the /zombs-leaderboard route) — flagged for transparency, not something this documentation endorses or recommends carrying forward into any fork.

What's not here

Notice what this server doesn't do: no WebSocket server, no game-protocol handling, no bot logic. All of that lives in the other two processes — Session Saver (port 8090) and Socket Server (port 8100) — which the modded client connects to directly once it loads. This server's only job is to get the client bundle, its assets, and the WASM module into your browser.