const http = require("http"); const fs = require("fs"); const path = require("path"); const mineflayer = require("mineflayer"); const gui = require("mineflayer-gui"); const logs = []; const MAX_LOGS = 200; const LOOP_DELAY_MS = 30_000; const MAX_ALLOWED_MOVE_BLOCKS = 10; const REJOIN_DELAY_MS = 60_000; let spawnerLoopRunning = false; let joinPosition = null; let hasJoined = false; let spawnerPosition = null; let reconnectTimer = null; let bot = null; function createBotInstance() { hasJoined = false; joinPosition = null; bot = mineflayer.createBot({ host: "java.donutsmp.net", username: "Tayota4420", auth: "microsoft", version: "1.21.1", }); bot.loadPlugin(gui); bindBotEvents(); } function stringifyReason(reason) { if (typeof reason === "string") return reason; try { return JSON.stringify(reason); } catch { return String(reason); } } function scheduleRejoinIfAlreadyOnline(reason) { const reasonText = stringifyReason(reason); pushLog(`kicked: ${reasonText}`); if (!reasonText.includes("You are already online")) return; if (reconnectTimer) { pushLog("rejoin already scheduled"); return; } stopSpawnerLoop("stopping loop due to kick"); pushLog("already online kick detected, rejoining in 60s"); reconnectTimer = setTimeout(() => { reconnectTimer = null; pushLog("rejoining now..."); createBotInstance(); }, REJOIN_DELAY_MS); } function pushLog(message) { logs.push({ time: new Date().toISOString(), message, }); if (logs.length > MAX_LOGS) logs.shift(); console.log(message); } const webPath = path.join(__dirname, "web", "index.html"); const server = http.createServer(async (req, res) => { if (req.method === "GET" && req.url === "/") { try { const html = fs.readFileSync(webPath, "utf8"); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(html); } catch { res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); res.end("UI file missing"); } return; } if (req.method === "GET" && req.url === "/api/logs") { res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify({ logs })); return; } if (req.method === "POST" && req.url === "/api/spawner") { try { startSpawnerLoop(); res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", }); res.end(JSON.stringify({ ok: true, running: spawnerLoopRunning })); } catch (err) { pushLog(`spawner error: ${err.message}`); res.writeHead(500, { "Content-Type": "application/json; charset=utf-8", }); res.end(JSON.stringify({ ok: false, error: err.message })); } return; } if (req.method === "POST" && req.url === "/api/stop") { try { stopSpawnerLoop("spawner loop stopped by user"); res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", }); res.end(JSON.stringify({ ok: true, running: spawnerLoopRunning })); } catch (err) { pushLog(`stop error: ${err.message}`); res.writeHead(500, { "Content-Type": "application/json; charset=utf-8", }); res.end(JSON.stringify({ ok: false, error: err.message })); } return; } res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); res.end("Not found"); }); server.listen(3000, () => { pushLog("web ui running at http://localhost:3000"); }); function sayItems(items = []) { const output = items.map(itemToString).join(", "); pushLog(output || "empty"); } function readCurrentWindowInventory() { const window = bot.currentWindow; if (!window) { pushLog("no window open"); return; } const items = typeof window.containerItems === "function" ? window.containerItems() : typeof window.items === "function" ? window.items() : []; const title = typeof window.title === "string" ? window.title : window.title && typeof window.title === "object" ? JSON.stringify(window.title) : "custom gui"; pushLog(`window opened: ${title}`); sayItems(items); } function comparator(item, pItem) { if (!pItem) return false; const target = String(item).toLowerCase(); return ( String(pItem.displayName || "").toLowerCase() === target || String(pItem.name || "").toLowerCase() === target ); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function stopSpawnerLoop(reason) { if (!spawnerLoopRunning) return; spawnerLoopRunning = false; pushLog(reason || "spawner loop stopped"); closeCurrentWindow(); } function findSpawnerBlock() { const matching = ["spawner", "mob_spawner", "trial_spawner"] .filter((name) => bot.registry.blocksByName[name] !== undefined) .map((name) => bot.registry.blocksByName[name].id); if (matching.length === 0) return null; return bot.findBlock({ matching, maxDistance: 6, }); } async function rotateToSpawner() { let targetBlock = null; if (spawnerPosition) { targetBlock = bot.blockAt(spawnerPosition); } if (!targetBlock) { targetBlock = findSpawnerBlock(); if (targetBlock) { spawnerPosition = targetBlock.position.clone(); pushLog( `spawner position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`, ); } } if (!targetBlock) { pushLog("no spawner found nearby"); return null; } await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true); return targetBlock; } function waitForWindowOpen(timeoutMs = 5000) { return new Promise((resolve) => { const timeout = setTimeout(() => { bot.removeListener("windowOpen", onOpen); resolve(false); }, timeoutMs); const onOpen = () => { clearTimeout(timeout); resolve(true); }; bot.once("windowOpen", onOpen); }); } async function openSpawnerGui() { const targetBlock = await rotateToSpawner(); if (!targetBlock || targetBlock.name === "air") { pushLog("unable to target spawner block"); return; } const openPromise = waitForWindowOpen(5000); await bot.activateBlock(targetBlock); const opened = await openPromise; if (!opened) { pushLog(`no gui opened from block '${targetBlock.name}'`); return false; } return true; } function getWindowItems() { const window = bot.currentWindow; if (!window) return []; if (typeof window.containerItems === "function") return window.containerItems(); if (typeof window.items === "function") return window.items(); return []; } async function dropAllBonesFromCurrentWindow() { if (!bot.currentWindow) { pushLog("no window open to drop bones"); return; } let dropped = 0; while (true) { const boneStacks = getWindowItems().filter( (item) => item && String(item.name || "").toLowerCase() === "bone", ); const boneCount = boneStacks.reduce( (sum, item) => sum + (item.count || 0), 0, ); if (boneCount === 0) break; const ok = await bot.gui .Query() .Window(comparator) .Drop("Bone", 64) .end() .run(); if (!ok) { pushLog("failed to drop bones from window"); break; } dropped += Math.min(64, boneCount); await sleep(150); } pushLog(`dropped bones total: ${dropped}`); } function closeCurrentWindow() { if (!bot.currentWindow) return; if (typeof bot.closeWindow === "function") { bot.closeWindow(bot.currentWindow); return; } if (typeof bot.currentWindow.close === "function") { bot.currentWindow.close(); } } async function runSpawnerCycle() { const opened = await openSpawnerGui(); if (!opened) return; await dropAllBonesFromCurrentWindow(); closeCurrentWindow(); } function startSpawnerLoop() { if (spawnerLoopRunning) { pushLog("spawner loop already running"); return; } if (!joinPosition) { pushLog("join position not set yet; wait for spawn"); return; } spawnerLoopRunning = true; pushLog("spawner loop started"); (async () => { while (spawnerLoopRunning) { try { await runSpawnerCycle(); } catch (err) { pushLog(`cycle error: ${err.message}`); } if (!spawnerLoopRunning) break; pushLog("waiting 30s for next cycle"); await sleep(LOOP_DELAY_MS); } })(); } function bindBotEvents() { bot.on("windowOpen", () => { readCurrentWindowInventory(); }); bot.on("windowClose", () => { if (!hasJoined) return; pushLog("window closed"); }); bot.once("spawn", () => { hasJoined = true; if (!bot.entity || !bot.entity.position) return; joinPosition = bot.entity.position.clone(); pushLog( `join position set: ${joinPosition.x.toFixed(1)}, ${joinPosition.y.toFixed(1)}, ${joinPosition.z.toFixed(1)}`, ); }); bot.on("physicsTick", () => { if ( !spawnerLoopRunning || !joinPosition || !bot.entity || !bot.entity.position ) { return; } const movedDistance = bot.entity.position.distanceTo(joinPosition); if (movedDistance > MAX_ALLOWED_MOVE_BLOCKS) { stopSpawnerLoop( `moved ${movedDistance.toFixed(2)} blocks from join position (> ${MAX_ALLOWED_MOVE_BLOCKS}), stopping loop`, ); } }); bot.on("kicked", (reason) => scheduleRejoinIfAlreadyOnline(reason)); bot.on("error", (err) => pushLog(`error: ${err.message}`)); } function itemToString(item) { if (item) { return `${item.name} x ${item.count}`; } return "(nothing)"; } createBotInstance();