const http = require("http"); const https = require("https"); const fs = require("fs"); const path = require("path"); const mineflayer = require("mineflayer"); const gui = require("mineflayer-gui"); const Vec3 = require("vec3"); require("dotenv").config(); const logs = []; const MAX_LOGS = 200; const LOOP_DELAY_MS = 10_000; const MAX_ALLOWED_MOVE_BLOCKS = 10; const REJOIN_DELAY_MS = 60_000; const BULB_VERIFY_RETRIES = 3; const SECURITY_RADIUS = 10; const EMERGENCY_HOTBAR_SLOT = 0; const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || ""; const ALLOWED_PLAYERS_FILE = process.env.ALLOWED_PLAYERS_FILE || "allowed_players.txt"; const ITEM_MODE = normalizeItemMode( process.env.ITEM_MODE || process.env.SPAWNER_MODE || "bones", ); const BUTTON_POSITION = parseEnvPosition("BUTTON"); const BULB_POSITION = parseEnvPosition("BULB"); let spawnerLoopRunning = false; let joinPosition = null; let hasJoined = false; let spawnerPosition = null; let reconnectTimer = null; let bot = null; let bothModeStep = 0; let emergencyTriggered = false; let securityMonitorInterval = null; const allowedPlayers = loadAllowedPlayers(); function loadAllowedPlayers() { const filePath = path.isAbsolute(ALLOWED_PLAYERS_FILE) ? ALLOWED_PLAYERS_FILE : path.join(__dirname, ALLOWED_PLAYERS_FILE); try { const data = fs.readFileSync(filePath, "utf8"); return new Set( data .split(/\r?\n/) .map((line) => line.trim().toLowerCase()) .filter((line) => line && !line.startsWith("#")), ); } catch { pushLog(`allowed players file not found: ${filePath}`); return new Set(); } } function normalizeItemMode(value) { const input = String(value || "") .trim() .toLowerCase(); if (input === "both") return "both"; if (input === "arrow" || input === "arrows") return "arrows"; return "bones"; } function parseEnvPosition(prefix) { const compact = process.env[`${prefix}_POS`]; if (compact) { const parsed = compact .split(/[\s,]+/) .filter(Boolean) .map((v) => Number(v)); if (parsed.length === 3 && parsed.every(Number.isFinite)) { return new Vec3(parsed[0], parsed[1], parsed[2]); } } const x = Number(process.env[`${prefix}_X`]); const y = Number(process.env[`${prefix}_Y`]); const z = Number(process.env[`${prefix}_Z`]); if ([x, y, z].every(Number.isFinite)) { return new Vec3(x, y, z); } return null; } function isBulbOn(block) { if (!block) return null; const props = typeof block.getProperties === "function" ? block.getProperties() : null; if (props && typeof props.lit === "boolean") return props.lit; if (props && typeof props.powered === "boolean") return props.powered; const name = String(block.name || "").toLowerCase(); if (name.includes("lit")) return true; if (name.includes("unlit")) return false; if (name.includes("redstone_lamp")) return false; return null; } async function ensureBulbModeBeforeOpen(mode) { if (!BULB_POSITION || !BUTTON_POSITION) { pushLog("missing BULB_POS/BUTTON_POS in .env; skipping mode check"); return false; } const desiredBulbOn = mode === "arrows"; let bulbBlock = bot.blockAt(BULB_POSITION); if (!bulbBlock || bulbBlock.name === "air") { pushLog("bulb block not found at BULB_POS"); return false; } let currentBulbOn = isBulbOn(bulbBlock); if (currentBulbOn === desiredBulbOn) { return false; } const buttonBlock = bot.blockAt(BUTTON_POSITION); if (!buttonBlock || buttonBlock.name === "air") { pushLog("button block not found at BUTTON_POS"); return false; } for (let attempt = 1; attempt <= BULB_VERIFY_RETRIES; attempt += 1) { try { await bot.lookAt(buttonBlock.position.offset(0.5, 0.5, 0.5), true); await bot.activateBlock(buttonBlock); pushLog( `clicked button to set bulb ${desiredBulbOn ? "on" : "off"} for ${mode} (attempt ${attempt}/${BULB_VERIFY_RETRIES})`, ); await sleep(900); bulbBlock = bot.blockAt(BULB_POSITION); currentBulbOn = isBulbOn(bulbBlock); if (currentBulbOn === desiredBulbOn) { return true; } } catch (err) { pushLog(`failed to click button: ${err.message}`); } } pushLog( `bulb state still incorrect after ${BULB_VERIFY_RETRIES} attempts (wanted ${desiredBulbOn ? "on" : "off"})`, ); return false; } function createBotInstance() { hasJoined = false; joinPosition = null; spawnerPosition = null; emergencyTriggered = false; bot = mineflayer.createBot({ host: "localhost", username: "Tayota4420", auth: "microsoft", version: "1.21.4", }); 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); } async function sendDiscordWebhook(content) { if (!DISCORD_WEBHOOK_URL) { pushLog("DISCORD_WEBHOOK_URL not set; skipping webhook"); return; } try { const body = JSON.stringify({ content }); const url = new URL(DISCORD_WEBHOOK_URL); await new Promise((resolve, reject) => { const req = https.request( { method: "POST", hostname: url.hostname, path: `${url.pathname}${url.search}`, port: url.port || 443, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }, }, (res) => { res.on("data", () => {}); res.on("end", () => resolve()); }, ); req.on("error", reject); req.write(body); req.end(); }); } catch (err) { pushLog(`failed webhook send: ${err.message}`); } } function isSpawnerBlock(block) { if (!block) return false; return ["chest", "trapped_chest"].includes(block.name); } function getUnauthorizedNearbyPlayers() { if (!bot || !bot.entity || !bot.entity.position) return []; const intruders = []; for (const [username, player] of Object.entries(bot.players)) { if (!username || username === bot.username) continue; if (allowedPlayers.has(username.toLowerCase())) continue; if (!player || !player.entity || !player.entity.position) continue; const distance = player.entity.position.distanceTo(bot.entity.position); if (distance <= SECURITY_RADIUS) { intruders.push(`${username} (${distance.toFixed(1)}m)`); } } return intruders; } async function breakSpawnerUntilGone() { bot.quickBarSlot = EMERGENCY_HOTBAR_SLOT; bot.setControlState("sneak", true); try { while (true) { let targetBlock = null; if (spawnerPosition) { targetBlock = bot.blockAt(spawnerPosition); } if (!isSpawnerBlock(targetBlock)) { targetBlock = findSpawnerBlock(); } if (!isSpawnerBlock(targetBlock)) { pushLog("chest no longer present"); return true; } spawnerPosition = targetBlock.position.clone(); await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true); if (!bot.canDigBlock(targetBlock)) { pushLog("cannot dig chest yet, retrying"); await sleep(300); continue; } try { await bot.dig(targetBlock, true); } catch (err) { pushLog(`chest dig failed: ${err.message}`); await sleep(300); } } } finally { bot.setControlState("sneak", false); } } async function triggerSecurityShutdown(reason) { if (emergencyTriggered) return; emergencyTriggered = true; pushLog(`security trigger: ${reason}`); stopSpawnerLoop("spawner loop stopped due to security trigger"); const spawnerDestroyed = await breakSpawnerUntilGone(); const status = spawnerDestroyed ? "chest removed" : "chest not removed"; await sendDiscordWebhook(`MCBOT ALERT: ${reason} | ${status}`); try { bot.quit("security shutdown"); } catch { // ignore disconnect errors } } function startSecurityMonitor() { if (securityMonitorInterval) clearInterval(securityMonitorInterval); securityMonitorInterval = setInterval(() => { if (!hasJoined || !bot || emergencyTriggered) return; const intruders = getUnauthorizedNearbyPlayers(); if (intruders.length > 0) { triggerSecurityShutdown( `unauthorized player nearby: ${intruders.join(", ")}`, ); } }, 1000); } function stopSecurityMonitor() { if (!securityMonitorInterval) return; clearInterval(securityMonitorInterval); securityMonitorInterval = null; } 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 = ["chest", "trapped_chest"] .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( `chest position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`, ); } } if (!targetBlock) { pushLog("no chest 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(maxAttempts = 2) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const targetBlock = await rotateToSpawner(); if (!targetBlock || targetBlock.name === "air") { pushLog("unable to target chest block"); return false; } const openPromise = waitForWindowOpen(5000); closeCurrentWindow(); await bot.activateBlock(targetBlock); const opened = await openPromise; if (opened) { return true; } pushLog( `no gui opened from chest '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`, ); if (attempt < maxAttempts) { await sleep(700); } } return false; } 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}`); } async function dropAllArrowsFromCurrentWindow() { if (!bot.currentWindow) { pushLog("no window open to drop arrows"); return; } let dropped = 0; while (true) { const arrowStacks = getWindowItems().filter( (item) => item && String(item.name || "").toLowerCase() === "arrow", ); const arrowCount = arrowStacks.reduce( (sum, item) => sum + (item.count || 0), 0, ); if (arrowCount === 0) break; const ok = await bot.gui .Query() .Window(comparator) .Drop("Arrow", 64) .end() .run(); if (!ok) { pushLog("failed to drop arrows from window"); break; } dropped += Math.min(64, arrowCount); await sleep(150); } pushLog(`dropped arrows 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 runSpawnerPhase(mode) { const toggled = await ensureBulbModeBeforeOpen(mode); await sleep(toggled ? 1300 : 500); const opened = await openSpawnerGui(); if (!opened) return; if (mode === "arrows") { await dropAllArrowsFromCurrentWindow(); } else { await dropAllBonesFromCurrentWindow(); } closeCurrentWindow(); } async function runSpawnerCycle() { if (ITEM_MODE === "both") { const phase = bothModeStep < 2 ? "bones" : "arrows"; await runSpawnerPhase(phase); bothModeStep = (bothModeStep + 1) % 4; return; } await runSpawnerPhase(ITEM_MODE); } function startSpawnerLoop() { if (spawnerLoopRunning) { pushLog("spawner loop already running"); return; } if (!joinPosition) { pushLog("join position not set yet; wait for spawn"); return; } spawnerLoopRunning = true; bothModeStep = 0; pushLog("spawner loop started"); (async () => { while (spawnerLoopRunning) { try { await runSpawnerCycle(); } catch (err) { pushLog(`cycle error: ${err.message}`); } if (!spawnerLoopRunning) break; pushLog("waiting 10s 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(); startSecurityMonitor(); pushLog( `join position set: ${joinPosition.x.toFixed(1)}, ${joinPosition.y.toFixed(1)}, ${joinPosition.z.toFixed(1)}`, ); }); bot.on("blockUpdate", (oldBlock, newBlock) => { if ( !hasJoined || emergencyTriggered || !bot.entity || !bot.entity.position ) { return; } if (!oldBlock || !newBlock) return; if (oldBlock.name === "air") return; if (newBlock.name !== "air") return; const distance = oldBlock.position.distanceTo(bot.entity.position); if (distance <= SECURITY_RADIUS) { triggerSecurityShutdown( `block broken within ${SECURITY_RADIUS}m at ${oldBlock.position.x},${oldBlock.position.y},${oldBlock.position.z}`, ); } }); 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}`)); bot.on("end", () => stopSecurityMonitor()); } function itemToString(item) { if (item) { return `${item.name} x ${item.count}`; } return "(nothing)"; } createBotInstance(); if (BULB_POSITION && BUTTON_POSITION) { pushLog( `mode=${ITEM_MODE}, bulb=${BULB_POSITION.x},${BULB_POSITION.y},${BULB_POSITION.z}, button=${BUTTON_POSITION.x},${BUTTON_POSITION.y},${BUTTON_POSITION.z}`, ); } else { pushLog("set BULB_POS and BUTTON_POS in .env to enable mode checking"); } pushLog( `security radius=${SECURITY_RADIUS}m, allowed players file=${ALLOWED_PLAYERS_FILE}`, );