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"); require("dotenv").config(); const logs = []; const MAX_LOGS = 200; const LOOP_DELAY_MS = 60_000; const MAX_ALLOWED_MOVE_BLOCKS = 10; const LOOP_RESUME_TOLERANCE_BLOCKS = 1.5; const REJOIN_DELAY_MS = 60_000; const SECURITY_ARM_DELAY_MS = 5_000; const SECURITY_RADIUS = 10; const EMERGENCY_HOTBAR_SLOT = 0; const EMERGENCY_MISSING_RETRIES = 8; const DEBUG_LOGS = String(process.env.DEBUG_LOGS || "") === "1" || String(process.env.DEBUG_LOGS || "").toLowerCase() === "true"; const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || ""; const ALLOWED_PLAYERS_FILE = process.env.ALLOWED_PLAYERS_FILE || "allowed_players.txt"; let spawnerLoopRunning = false; let joinPosition = null; let hasJoined = false; let spawnerPosition = null; let reconnectTimer = null; let bot = null; let emergencyTriggered = false; let securityMonitorInterval = null; let securityArmed = false; let securityArmTimer = null; let loopStartPosition = null; let pausedByMovementGuard = false; 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 createBotInstance() { hasJoined = false; joinPosition = null; spawnerPosition = null; emergencyTriggered = false; securityArmed = false; loopStartPosition = null; pausedByMovementGuard = false; if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = 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); } function debugLog(message) { if (!DEBUG_LOGS) return; pushLog(`[debug] ${message}`); } function getWindowTitle(window) { if (!window) return "no-window"; if (typeof window.title === "string") return window.title; if (window.title && typeof window.title === "object") { try { return JSON.stringify(window.title); } catch { return "[window-title-object]"; } } return "custom gui"; } async function sendDiscordWebhook(content) { if (!DISCORD_WEBHOOK_URL) { pushLog("DISCORD_WEBHOOK_URL not set; skipping webhook"); return; } try { const body = JSON.stringify({ content: `@everyone ${content}`, allowed_mentions: { parse: ["everyone"] }, }); 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 ["spawner", "mob_spawner", "trial_spawner"].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() { await bot.setQuickBarSlot(EMERGENCY_HOTBAR_SLOT); await bot.setControlState("sneak", true); await sleep(250); let missingChecks = 0; try { while (true) { await bot.setControlState("sneak", true); let targetBlock = null; if (spawnerPosition) { targetBlock = bot.blockAt(spawnerPosition); } if (!isSpawnerBlock(targetBlock)) { targetBlock = findSpawnerBlock(); } if (!isSpawnerBlock(targetBlock)) { missingChecks += 1; if (missingChecks >= EMERGENCY_MISSING_RETRIES) { pushLog("spawner no longer present"); return true; } await sleep(200); continue; } missingChecks = 0; spawnerPosition = targetBlock.position.clone(); // Keep sneak forced on every iteration so dig always happens while sneaking. await bot.setControlState("sneak", true); await sleep(120); await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true); if (!bot.canDigBlock(targetBlock)) { pushLog("cannot dig spawner yet, retrying"); await sleep(300); continue; } try { await bot.dig(targetBlock, true); await sleep(150); } catch (err) { pushLog(`spawner 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 ? "spawner removed" : "spawner not removed"; await sleep(600); const stashResult = await stashSpawnerDropsInEnderChest(); const stashStatus = stashResult.attempted ? stashResult.remaining === 0 ? `spawners stashed (moved=${stashResult.moved})` : `stash incomplete (moved=${stashResult.moved}, remaining=${stashResult.remaining})` : "no spawner items to stash"; await sendDiscordWebhook( `MCBOT ALERT: ${reason} | ${status} | ${stashStatus}`, ); try { bot.quit("security shutdown"); } catch { // ignore disconnect errors } } function startSecurityMonitor() { if (securityMonitorInterval) clearInterval(securityMonitorInterval); securityMonitorInterval = setInterval(() => { if (!hasJoined || !bot || emergencyTriggered || !securityArmed) return; const intruders = getUnauthorizedNearbyPlayers(); if (intruders.length > 0) { triggerSecurityShutdown( `unauthorized player nearby: ${intruders.join(", ")}`, ); } }, 1000); } function stopSecurityMonitor() { if (!securityMonitorInterval) return; clearInterval(securityMonitorInterval); securityMonitorInterval = null; } function armSecurityAfterJoinDelay() { if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = null; } securityArmed = false; pushLog(`security will auto-enable in ${SECURITY_ARM_DELAY_MS / 1000}s`); securityArmTimer = setTimeout(() => { securityArmTimer = null; if (!bot || !hasJoined || emergencyTriggered) return; securityArmed = true; pushLog("security auto-enabled"); }, SECURITY_ARM_DELAY_MS); } 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(3008, () => { pushLog("web ui running at http://localhost:3008"); }); 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 = getWindowTitle(window); 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, options = {}) { if (!spawnerLoopRunning) return; spawnerLoopRunning = false; if (!options.keepMovementPause) { pausedByMovementGuard = 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); debugLog(`waitForWindowOpen timeout after ${timeoutMs}ms`); resolve(false); }, timeoutMs); const onOpen = () => { clearTimeout(timeout); debugLog( `waitForWindowOpen resolved: ${getWindowTitle(bot.currentWindow)}`, ); 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 spawner block"); return false; } const openPromise = waitForWindowOpen(5000); debugLog( `openSpawnerGui attempt ${attempt}/${maxAttempts} block=${targetBlock.name}`, ); closeCurrentWindow(); await bot.activateBlock(targetBlock); const opened = await openPromise; if (opened) { return true; } pushLog( `no gui opened from block '${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}`); } function countItemByName(items, name) { const target = String(name).toLowerCase(); return items .filter((item) => item && String(item.name || "").toLowerCase() === target) .reduce((sum, item) => sum + (item.count || 0), 0); } function isSpawnerItem(item) { if (!item) return false; return ["spawner", "mob_spawner", "trial_spawner"].includes( String(item.name || "").toLowerCase(), ); } function countSpawnerItemsInInventory() { if (!bot || !bot.inventory) return 0; return bot.inventory .items() .filter((item) => isSpawnerItem(item)) .reduce((sum, item) => sum + (item.count || 0), 0); } function findNearbyEnderChest(maxDistance = 6) { const blockDef = bot?.registry?.blocksByName?.ender_chest; if (!blockDef) return null; return bot.findBlock({ matching: blockDef.id, maxDistance, }); } function getInventorySpawnerSlotsInCurrentWindow() { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return []; const slots = bot.currentWindow.slots; const inventoryStart = getInventoryStart(bot.currentWindow); const result = []; for (let i = inventoryStart; i < slots.length; i += 1) { const item = slots[i]; if (!isSpawnerItem(item)) continue; result.push(i); } return result; } async function moveAllInventorySpawnersToCurrentWindow() { if (!bot.currentWindow) return 0; let moved = 0; let safety = 0; while (safety < 120) { safety += 1; const before = countSpawnerItemsInInventory(); if (before === 0) break; const spawnerSlots = getInventorySpawnerSlotsInCurrentWindow(); if (spawnerSlots.length === 0) break; let clicked = false; for (const slot of spawnerSlots) { try { // mode=1 is shift-click, which moves the stack to the open container. await bot.clickWindow(slot, 0, 1); clicked = true; await sleep(120); } catch (err) { debugLog( `moveAllInventorySpawnersToCurrentWindow: raw shift slot=${slot} failed: ${err.message}`, ); } } const after = countSpawnerItemsInInventory(); if (after < before) { moved += before - after; continue; } if (!clicked) break; break; } pushLog(`moved spawners from inventory: ${moved}`); return moved; } async function stashSpawnerDropsInEnderChest() { const before = countSpawnerItemsInInventory(); if (before === 0) { pushLog("no spawner items in inventory to stash"); return { attempted: false, moved: 0, remaining: 0 }; } const enderChest = findNearbyEnderChest(6); if (!enderChest) { pushLog("no nearby ender chest found for stashing spawners"); return { attempted: true, moved: 0, remaining: before }; } try { await bot.lookAt(enderChest.position.offset(0.5, 0.5, 0.5), true); const openPromise = waitForWindowOpen(5000); closeCurrentWindow(); await bot.activateBlock(enderChest); const opened = await openPromise; if (!opened || !bot.currentWindow) { pushLog("ender chest did not open"); return { attempted: true, moved: 0, remaining: countSpawnerItemsInInventory(), }; } await sleep(150); const moved = await moveAllInventorySpawnersToCurrentWindow(); closeCurrentWindow(); const remaining = countSpawnerItemsInInventory(); if (remaining === 0) { pushLog("all spawner items stashed in ender chest"); } else { pushLog(`spawner stash incomplete, remaining in inventory: ${remaining}`); } return { attempted: true, moved, remaining }; } catch (err) { pushLog(`failed stashing spawners in ender chest: ${err.message}`); closeCurrentWindow(); return { attempted: true, moved: 0, remaining: countSpawnerItemsInInventory(), }; } } function normalizeLabel(value) { return String(value || "") .toLowerCase() .replace(/[_\s]+/g, "") .trim(); } function findWindowSlotByCandidates(candidates) { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return null; const normalizedCandidates = candidates.map(normalizeLabel); for (const item of bot.currentWindow.slots) { if (!item) continue; const itemName = normalizeLabel(item.name); const displayName = normalizeLabel(item.displayName); const nbtName = normalizeLabel(item.customName || ""); const matched = normalizedCandidates.some( (candidate) => itemName.includes(candidate) || displayName.includes(candidate) || nbtName.includes(candidate), ); if (matched) return item.slot; } return null; } function getInventoryStart(window = bot.currentWindow) { if (!window) return 9; if (Number.isFinite(window.inventoryStart)) return window.inventoryStart; // Fallback for custom windows that don't expose inventoryStart. // Most windows append 36 player inventory slots at the end. if (Array.isArray(window.slots) && window.slots.length >= 36) { return Math.max(0, window.slots.length - 36); } return 9; } function logWindowSlotSummary(label) { if ( !DEBUG_LOGS || !bot.currentWindow || !Array.isArray(bot.currentWindow.slots) ) return; const inventoryStart = getInventoryStart(bot.currentWindow); const slots = bot.currentWindow.slots; const nonEmpty = []; for (let i = 0; i < slots.length; i += 1) { const item = slots[i]; if (!item) continue; nonEmpty.push(`${i}:${item.name}x${item.count}`); } debugLog( `${label} slots=${slots.length} inventoryStart=${inventoryStart} nonEmpty=${nonEmpty.join(" | ") || "none"}`, ); } function getInventoryBoneSlotsInCurrentWindow() { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return []; const slots = bot.currentWindow.slots; const inventoryStart = getInventoryStart(bot.currentWindow); const result = []; for (let i = inventoryStart; i < slots.length; i += 1) { const item = slots[i]; if (!item) continue; if (String(item.name || "").toLowerCase() === "bone") { result.push(i); } } return result; } async function manualPlaceInventoryBonesToGui() { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return false; const inventoryStart = getInventoryStart(bot.currentWindow); const topSlots = []; for (let slot = 0; slot < inventoryStart; slot += 1) { topSlots.push(slot); } if (topSlots.length === 0) { debugLog("manualPlaceInventoryBonesToGui: no top slots in current window"); return false; } const boneSlots = getInventoryBoneSlotsInCurrentWindow(); if (boneSlots.length === 0) { debugLog( "manualPlaceInventoryBonesToGui: no bone slots available in inventory range", ); return false; } let placedAny = false; for (const sourceSlot of boneSlots) { const sourceItem = bot.currentWindow.slots[sourceSlot]; if (!sourceItem || String(sourceItem.name || "").toLowerCase() !== "bone") { continue; } try { // Pick up stack from inventory slot. await bot.clickWindow(sourceSlot, 0, 0); await sleep(90); } catch (err) { debugLog( `manualPlaceInventoryBonesToGui: pick failed slot=${sourceSlot} err=${err.message}`, ); continue; } let placedStack = false; for (const targetSlot of topSlots) { try { await bot.clickWindow(targetSlot, 0, 0); await sleep(90); const cursor = bot.currentWindow.selectedItem; if (!cursor || String(cursor.name || "").toLowerCase() !== "bone") { placedStack = true; placedAny = true; break; } } catch (err) { debugLog( `manualPlaceInventoryBonesToGui: place failed target=${targetSlot} err=${err.message}`, ); } } // If still holding bones, put them back to avoid cursor desync. const cursor = bot.currentWindow.selectedItem; if (cursor && String(cursor.name || "").toLowerCase() === "bone") { try { await bot.clickWindow(sourceSlot, 0, 0); await sleep(60); } catch (err) { debugLog( `manualPlaceInventoryBonesToGui: return-to-source failed slot=${sourceSlot} err=${err.message}`, ); } } if (placedStack) { debugLog( `manualPlaceInventoryBonesToGui: placed stack from source slot=${sourceSlot}`, ); } } return placedAny; } async function clickWindowItem(itemName, clickType = "left", count = 1) { if (!bot.currentWindow) return false; const ok = await bot.gui .Query() .Window(comparator) .Click(itemName, clickType, count) .end() .run(); return Boolean(ok); } async function clickWindowItemByCandidates(candidates) { debugLog(`clickWindowItemByCandidates candidates=${candidates.join(" | ")}`); for (const candidate of candidates) { const ok = await clickWindowItem(candidate, "left", 1); if (ok) { debugLog(`matched candidate directly: ${candidate}`); return true; } } const slot = findWindowSlotByCandidates(candidates); if (slot === null) { debugLog("no slot match found for candidates"); return false; } try { await bot.clickWindow(slot, 0, 0); debugLog(`clicked fallback slot=${slot}`); return true; } catch (err) { pushLog(`slot click failed: ${err.message}`); return false; } } async function moveAllWindowBonesToInventory() { if (!bot.currentWindow) return 0; let moved = 0; let safety = 0; while (safety < 120) { safety += 1; const before = countItemByName(getWindowItems(), "bone"); if (before === 0) { debugLog("moveAllWindowBonesToInventory: no bones in window"); break; } const ok = await clickWindowItem("Bone", "shift", 1); if (!ok) { debugLog("moveAllWindowBonesToInventory: shift click failed"); break; } await sleep(120); const after = countItemByName(getWindowItems(), "bone"); if (after >= before) { debugLog( `moveAllWindowBonesToInventory: no progress before=${before} after=${after}`, ); break; } moved += before - after; debugLog( `moveAllWindowBonesToInventory: moved this step=${before - after}`, ); } pushLog(`moved bones to inventory: ${moved}`); return moved; } async function moveAllInventoryBonesToCurrentWindow() { if (!bot.currentWindow) return 0; let moved = 0; let safety = 0; while (safety < 120) { safety += 1; const before = countItemByName(bot.inventory.items(), "bone"); if (before === 0) { debugLog("moveAllInventoryBonesToCurrentWindow: no bones in inventory"); break; } const ok = await bot.gui .Query() .Inventory(comparator) .Click("Bone", "shift", 1) .end() .run(); if (!ok) { debugLog( "moveAllInventoryBonesToCurrentWindow: inventory shift click failed, trying raw slot shift-click", ); const boneSlots = getInventoryBoneSlotsInCurrentWindow(); if (boneSlots.length === 0) { debugLog( "moveAllInventoryBonesToCurrentWindow: no bone slots found in window inventory range", ); break; } let rawAttemptWorked = false; for (const slot of boneSlots) { try { // mode=1 is shift-click in the vanilla click-window protocol. await bot.clickWindow(slot, 0, 1); rawAttemptWorked = true; await sleep(120); } catch (err) { debugLog( `moveAllInventoryBonesToCurrentWindow: raw shift slot=${slot} failed: ${err.message}`, ); } } if (!rawAttemptWorked) { break; } } await sleep(120); const after = countItemByName(bot.inventory.items(), "bone"); if (after >= before) { debugLog( `moveAllInventoryBonesToCurrentWindow: no progress before=${before} after=${after}, trying raw slot shift-click fallback`, ); const boneSlots = getInventoryBoneSlotsInCurrentWindow(); debugLog( `moveAllInventoryBonesToCurrentWindow: candidate inventory bone slots=${boneSlots.join(",") || "none"}`, ); let rawMovedSomething = false; for (const slot of boneSlots) { try { await bot.clickWindow(slot, 0, 1); rawMovedSomething = true; await sleep(120); } catch (err) { debugLog( `moveAllInventoryBonesToCurrentWindow: raw slot shift failed slot=${slot} err=${err.message}`, ); } } const afterRaw = countItemByName(bot.inventory.items(), "bone"); if (afterRaw < before) { moved += before - afterRaw; debugLog( `moveAllInventoryBonesToCurrentWindow: raw fallback moved=${before - afterRaw}`, ); continue; } debugLog( "moveAllInventoryBonesToCurrentWindow: trying manual place fallback", ); const manualPlaced = await manualPlaceInventoryBonesToGui(); if (manualPlaced) { await sleep(150); const afterManual = countItemByName(bot.inventory.items(), "bone"); if (afterManual < before) { moved += before - afterManual; debugLog( `moveAllInventoryBonesToCurrentWindow: manual fallback moved=${before - afterManual}`, ); continue; } } if (!rawMovedSomething) { debugLog( "moveAllInventoryBonesToCurrentWindow: raw fallback did not run", ); } break; } moved += before - after; debugLog( `moveAllInventoryBonesToCurrentWindow: moved this step=${before - after}`, ); } pushLog(`moved bones from inventory: ${moved}`); return moved; } async function runOrderForBones() { debugLog("runOrderForBones: sending /order command"); bot.chat("/order ZareMate"); const opened = await waitForWindowOpen(5000); if (!opened || !bot.currentWindow) { pushLog("order gui did not open"); return; } debugLog( `runOrderForBones: first window=${getWindowTitle(bot.currentWindow)}`, ); logWindowSlotSummary("runOrderForBones:first"); const clickedBone = await clickWindowItem("Bone", "left", 1); if (!clickedBone) { pushLog("unable to click Bone in order gui"); closeCurrentWindow(); return; } await sleep(300); debugLog( `runOrderForBones: after bone click window=${getWindowTitle(bot.currentWindow)}`, ); logWindowSlotSummary("runOrderForBones:deliver"); debugLog( `runOrderForBones: inventory bones before move=${countItemByName(bot.inventory.items(), "bone")}`, ); await moveAllInventoryBonesToCurrentWindow(); debugLog( `runOrderForBones: inventory bones after move=${countItemByName(bot.inventory.items(), "bone")}`, ); await sleep(250); // Required sequence: close once, then click glass, then close. closeCurrentWindow(); const reopened = await waitForWindowOpen(3000); if (!reopened || !bot.currentWindow) { pushLog("no gui after close-once; cannot click glass"); return; } const glassCandidates = [ "Lime Stained Glass Pane", "lime_stained_glass_pane", "Green Stained Glass Pane", "green_stained_glass_pane", "Glass Pane", "stained_glass_pane", "Confirm", ]; const clickedLime = await clickWindowItemByCandidates(glassCandidates); if (!clickedLime) { const available = getWindowItems() .map((item) => item?.displayName || item?.name || "unknown") .join(", "); pushLog(`unable to click glass pane (available: ${available || "none"})`); return; } await sleep(200); closeCurrentWindow(); } 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() { await sleep(500); const opened = await openSpawnerGui(); if (!opened) return; await moveAllWindowBonesToInventory(); closeCurrentWindow(); await sleep(300); await runOrderForBones(); } async function runSpawnerCycle() { await runSpawnerPhase(); } function startSpawnerLoop() { if (spawnerLoopRunning) { pushLog("spawner loop already running"); return; } if (!joinPosition) { pushLog("join position not set yet; wait for spawn"); return; } spawnerLoopRunning = true; pausedByMovementGuard = false; if (bot && bot.entity && bot.entity.position) { loopStartPosition = bot.entity.position.clone(); } else if (joinPosition) { loopStartPosition = joinPosition.clone(); } if (loopStartPosition) { pushLog( `loop start position: ${loopStartPosition.x.toFixed(1)}, ${loopStartPosition.y.toFixed(1)}, ${loopStartPosition.z.toFixed(1)}`, ); } pushLog("spawner loop started"); (async () => { while (spawnerLoopRunning) { try { await runSpawnerCycle(); } catch (err) { pushLog(`cycle error: ${err.message}`); } if (!spawnerLoopRunning) break; pushLog("waiting 60s 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)}`, ); startSecurityMonitor(); pushLog("security monitor started"); armSecurityAfterJoinDelay(); }); bot.on("blockUpdate", (oldBlock, newBlock) => { if ( !hasJoined || emergencyTriggered || !securityArmed || !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 (!bot.entity || !bot.entity.position) { return; } const guardAnchor = loopStartPosition || joinPosition; if (spawnerLoopRunning) { if (!guardAnchor) return; const movedDistance = bot.entity.position.distanceTo(guardAnchor); if (movedDistance > MAX_ALLOWED_MOVE_BLOCKS) { pausedByMovementGuard = true; stopSpawnerLoop( `moved ${movedDistance.toFixed(2)} blocks from loop start (> ${MAX_ALLOWED_MOVE_BLOCKS}), pausing loop`, { keepMovementPause: true }, ); } return; } if (!pausedByMovementGuard || !guardAnchor) return; const distanceFromAnchor = bot.entity.position.distanceTo(guardAnchor); if (distanceFromAnchor <= LOOP_RESUME_TOLERANCE_BLOCKS) { pushLog( `returned to loop start (<= ${LOOP_RESUME_TOLERANCE_BLOCKS} blocks), re-enabling loop`, ); startSpawnerLoop(); } }); bot.on("kicked", (reason) => scheduleRejoinIfAlreadyOnline(reason)); bot.on("error", (err) => pushLog(`error: ${err.message}`)); bot.on("end", () => { stopSecurityMonitor(); if (securityArmTimer) { clearTimeout(securityArmTimer); securityArmTimer = null; } securityArmed = false; }); } function itemToString(item) { if (item) { return `${item.name} x ${item.count}`; } return "(nothing)"; } createBotInstance(); pushLog("mode=bones (orders-only)"); pushLog( `security radius=${SECURITY_RADIUS}m, allowed players file=${ALLOWED_PLAYERS_FILE}`, ); pushLog(`debug logs=${DEBUG_LOGS ? "on" : "off"} (set DEBUG_LOGS=1 to enable)`);