From 98ddfe0db71d1b9047880f4978d7ebe24be695b7 Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Sat, 14 Mar 2026 01:43:57 +0100 Subject: [PATCH] update: enhance bot functionality with security features and improved item handling --- allowed_players.txt | 5 +- index.js | 770 +++++++++++++++++++++++++++++++++----------- 2 files changed, 586 insertions(+), 189 deletions(-) diff --git a/allowed_players.txt b/allowed_players.txt index b0135f6..972bdbe 100644 --- a/allowed_players.txt +++ b/allowed_players.txt @@ -1 +1,4 @@ -ZareMate \ No newline at end of file +ZareMate +Bronkol +SkyBloczek +qawe7 \ No newline at end of file diff --git a/index.js b/index.js index 9792e1b..2c0ca4f 100644 --- a/index.js +++ b/index.js @@ -4,38 +4,39 @@ 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 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 BULB_VERIFY_RETRIES = 3; +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"; -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; +let securityArmed = false; +let securityArmTimer = null; +let loopStartPosition = null; +let pausedByMovementGuard = false; const allowedPlayers = loadAllowedPlayers(); @@ -58,112 +59,24 @@ function loadAllowedPlayers() { } } -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; + securityArmed = false; + loopStartPosition = null; + pausedByMovementGuard = false; + if (securityArmTimer) { + clearTimeout(securityArmTimer); + securityArmTimer = null; + } bot = mineflayer.createBot({ - host: "localhost", + host: "java.donutsmp.net", username: "Tayota4420", auth: "microsoft", - version: "1.21.4", + version: "1.21.1", }); bot.loadPlugin(gui); @@ -207,6 +120,24 @@ function pushLog(message) { 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"); @@ -214,7 +145,10 @@ async function sendDiscordWebhook(content) { } try { - const body = JSON.stringify({ content }); + const body = JSON.stringify({ + content: `@everyone ${content}`, + allowed_mentions: { parse: ["everyone"] }, + }); const url = new URL(DISCORD_WEBHOOK_URL); await new Promise((resolve, reject) => { @@ -246,7 +180,7 @@ async function sendDiscordWebhook(content) { function isSpawnerBlock(block) { if (!block) return false; - return ["chest", "trapped_chest"].includes(block.name); + return ["spawner", "mob_spawner", "trial_spawner"].includes(block.name); } function getUnauthorizedNearbyPlayers() { @@ -268,11 +202,16 @@ function getUnauthorizedNearbyPlayers() { } async function breakSpawnerUntilGone() { - bot.quickBarSlot = EMERGENCY_HOTBAR_SLOT; - bot.setControlState("sneak", true); + 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) { @@ -284,23 +223,34 @@ async function breakSpawnerUntilGone() { } if (!isSpawnerBlock(targetBlock)) { - pushLog("chest no longer present"); - return true; + 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 chest yet, retrying"); + pushLog("cannot dig spawner yet, retrying"); await sleep(300); continue; } try { await bot.dig(targetBlock, true); + await sleep(150); } catch (err) { - pushLog(`chest dig failed: ${err.message}`); + pushLog(`spawner dig failed: ${err.message}`); await sleep(300); } } @@ -317,7 +267,7 @@ async function triggerSecurityShutdown(reason) { stopSpawnerLoop("spawner loop stopped due to security trigger"); const spawnerDestroyed = await breakSpawnerUntilGone(); - const status = spawnerDestroyed ? "chest removed" : "chest not removed"; + const status = spawnerDestroyed ? "spawner removed" : "spawner not removed"; await sendDiscordWebhook(`MCBOT ALERT: ${reason} | ${status}`); @@ -332,7 +282,7 @@ function startSecurityMonitor() { if (securityMonitorInterval) clearInterval(securityMonitorInterval); securityMonitorInterval = setInterval(() => { - if (!hasJoined || !bot || emergencyTriggered) return; + if (!hasJoined || !bot || emergencyTriggered || !securityArmed) return; const intruders = getUnauthorizedNearbyPlayers(); if (intruders.length > 0) { @@ -349,6 +299,24 @@ function stopSecurityMonitor() { 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) => { @@ -431,12 +399,7 @@ function readCurrentWindowInventory() { ? window.items() : []; - const title = - typeof window.title === "string" - ? window.title - : window.title && typeof window.title === "object" - ? JSON.stringify(window.title) - : "custom gui"; + const title = getWindowTitle(window); pushLog(`window opened: ${title}`); sayItems(items); @@ -455,15 +418,18 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -function stopSpawnerLoop(reason) { +function stopSpawnerLoop(reason, options = {}) { if (!spawnerLoopRunning) return; spawnerLoopRunning = false; + if (!options.keepMovementPause) { + pausedByMovementGuard = false; + } pushLog(reason || "spawner loop stopped"); closeCurrentWindow(); } function findSpawnerBlock() { - const matching = ["chest", "trapped_chest"] + const matching = ["spawner", "mob_spawner", "trial_spawner"] .filter((name) => bot.registry.blocksByName[name] !== undefined) .map((name) => bot.registry.blocksByName[name].id); @@ -487,13 +453,13 @@ async function rotateToSpawner() { if (targetBlock) { spawnerPosition = targetBlock.position.clone(); pushLog( - `chest position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`, + `spawner position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`, ); } } if (!targetBlock) { - pushLog("no chest found nearby"); + pushLog("no spawner found nearby"); return null; } @@ -505,11 +471,15 @@ 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); }; @@ -521,11 +491,14 @@ 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"); + 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; @@ -535,7 +508,7 @@ async function openSpawnerGui(maxAttempts = 2) { } pushLog( - `no gui opened from chest '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`, + `no gui opened from block '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`, ); if (attempt < maxAttempts) { await sleep(700); @@ -591,41 +564,443 @@ async function dropAllBonesFromCurrentWindow() { pushLog(`dropped bones total: ${dropped}`); } -async function dropAllArrowsFromCurrentWindow() { - if (!bot.currentWindow) { - pushLog("no window open to drop arrows"); - return; +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 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; } - 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, - ); + return null; +} - if (arrowCount === 0) break; +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() - .Window(comparator) - .Drop("Arrow", 64) + .Inventory(comparator) + .Click("Bone", "shift", 1) .end() .run(); if (!ok) { - pushLog("failed to drop arrows from window"); - break; + 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; + } } - dropped += Math.min(64, arrowCount); - await sleep(150); + 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(`dropped arrows total: ${dropped}`); + 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() { @@ -639,30 +1014,19 @@ function closeCurrentWindow() { } } -async function runSpawnerPhase(mode) { - const toggled = await ensureBulbModeBeforeOpen(mode); - await sleep(toggled ? 1300 : 500); +async function runSpawnerPhase() { + await sleep(500); const opened = await openSpawnerGui(); if (!opened) return; - if (mode === "arrows") { - await dropAllArrowsFromCurrentWindow(); - } else { - await dropAllBonesFromCurrentWindow(); - } - + await moveAllWindowBonesToInventory(); closeCurrentWindow(); + await sleep(300); + await runOrderForBones(); } 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); + await runSpawnerPhase(); } function startSpawnerLoop() { @@ -677,7 +1041,18 @@ function startSpawnerLoop() { } spawnerLoopRunning = true; - bothModeStep = 0; + 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 () => { @@ -689,7 +1064,7 @@ function startSpawnerLoop() { } if (!spawnerLoopRunning) break; - pushLog("waiting 10s for next cycle"); + pushLog("waiting 60s for next cycle"); await sleep(LOOP_DELAY_MS); } })(); @@ -709,16 +1084,19 @@ function bindBotEvents() { 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)}`, ); + startSecurityMonitor(); + pushLog("security monitor started"); + armSecurityAfterJoinDelay(); }); bot.on("blockUpdate", (oldBlock, newBlock) => { if ( !hasJoined || emergencyTriggered || + !securityArmed || !bot.entity || !bot.entity.position ) { @@ -738,26 +1116,47 @@ function bindBotEvents() { }); bot.on("physicsTick", () => { - if ( - !spawnerLoopRunning || - !joinPosition || - !bot.entity || - !bot.entity.position - ) { + if (!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`, + 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()); + bot.on("end", () => { + stopSecurityMonitor(); + if (securityArmTimer) { + clearTimeout(securityArmTimer); + securityArmTimer = null; + } + securityArmed = false; + }); } function itemToString(item) { @@ -769,14 +1168,9 @@ function itemToString(item) { 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("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)`);