const mineflayer = require("mineflayer"); const gui = require("mineflayer-gui"); const fs = require("fs"); const path = require("path"); function getRuntimeUsername() { const fromArg = process.argv[2]; if (typeof fromArg === "string" && fromArg.trim()) return fromArg.trim(); const fromEnv = process.env.BOT_USERNAME; if (typeof fromEnv === "string" && fromEnv.trim()) return fromEnv.trim(); return "AFKBot"; } function toSafeFilePart(value) { return String(value).replace(/[^a-zA-Z0-9_-]/g, "_"); } const runtimeUsername = getRuntimeUsername(); const IPC_ENABLED = typeof process.send === "function"; // Configuration const BOT_CONFIG = { host: "java.donutsmp.net", username: runtimeUsername, auth: "microsoft", version: "1.21.1", }; const LOG_FILE = path.join( __dirname, `afk-${toSafeFilePart(runtimeUsername)}.log`, ); const TELEPORT_DETECT_REGEX = /you teleported to\b/i; const AFK_INITIAL_CHECK_DELAY_MS = 5000; const AFK_POST_COMMAND_CHECK_DELAY_MS = 7000; let hasDetectedTeleport = false; let afkRetryTimer = null; // Logging function log(message) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}`; console.log(logMessage); fs.appendFileSync(LOG_FILE, logMessage + "\n"); } function sendToParent(type, payload = {}) { if (!IPC_ENABLED) return; try { process.send({ type, ...payload }); } catch { // Parent IPC may close during shutdown. } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function waitForWindowOpen(timeoutMs = 5000) { return new Promise((resolve) => { if (bot.currentWindow) { resolve(true); return; } const originalHandler = bot.listenerCount("windowOpen"); const onWindowOpen = () => { clearTimeout(timer); bot.removeListener("windowOpen", onWindowOpen); resolve(true); }; const timer = setTimeout(() => { bot.removeListener("windowOpen", onWindowOpen); resolve(false); }, timeoutMs); bot.on("windowOpen", onWindowOpen); }); } 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(); } } 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. if (Array.isArray(window.slots) && window.slots.length >= 36) { return Math.max(0, window.slots.length - 36); } return 9; } function serializeWindow(window = bot.currentWindow) { if (!window || !Array.isArray(window.slots)) return null; return { id: window.id, title: toPlainText(window.title || window.name || "Window"), type: window.type || "unknown", inventoryStart: getInventoryStart(window), slots: window.slots.map((item, slot) => { if (!item) return null; return { slot, name: item.name || "", displayName: item.displayName || item.name || "", count: Number(item.count || 0), }; }), }; } let attachedWindow = null; let attachedWindowUpdateHandler = null; function detachWindowUpdateHook() { if (!attachedWindow || !attachedWindowUpdateHandler) return; attachedWindow.removeListener("updateSlot", attachedWindowUpdateHandler); attachedWindow = null; attachedWindowUpdateHandler = null; } function attachWindowUpdateHook(window) { detachWindowUpdateHook(); if (!window || typeof window.on !== "function") return; attachedWindow = window; attachedWindowUpdateHandler = () => { sendToParent("windowSnapshot", { username: runtimeUsername, window: serializeWindow(window), }); }; window.on("updateSlot", attachedWindowUpdateHandler); } function findWindowSlotByCandidates(candidates) { if (!bot.currentWindow || !Array.isArray(bot.currentWindow.slots)) return null; const normalizedCandidates = candidates.map((c) => String(c || "") .toLowerCase() .replace(/[_\s]+/g, "") .trim(), ); for (const item of bot.currentWindow.slots) { if (!item) continue; const itemName = String(item.name || "") .toLowerCase() .replace(/[_\s]+/g, ""); const displayName = String(item.displayName || "") .toLowerCase() .replace(/[_\s]+/g, ""); const nbtName = String(item.customName || "") .toLowerCase() .replace(/[_\s]+/g, ""); const matched = normalizedCandidates.some( (candidate) => itemName.includes(candidate) || displayName.includes(candidate) || nbtName.includes(candidate), ); if (matched) return item.slot; } return null; } function clearAfkRetryTimer() { if (afkRetryTimer) { clearTimeout(afkRetryTimer); afkRetryTimer = null; } } function scheduleAfkRetryCheck(delayMs) { clearAfkRetryTimer(); afkRetryTimer = setTimeout(async () => { if (hasDetectedTeleport) return; log(`No teleport detected after ${delayMs}ms; sending /afk command`); bot.chat("/afk"); await sleep(500); const opened = await waitForWindowOpen(5000); if (!opened || !bot.currentWindow) { log("AFk GUI did not open"); scheduleAfkRetryCheck(AFK_POST_COMMAND_CHECK_DELAY_MS); return; } log("AFk GUI opened, clicking amethyst block"); const amethystCandidates = ["Amethyst Block", "amethyst_block", "Amethyst"]; const slot = findWindowSlotByCandidates(amethystCandidates); if (slot === null) { log("Amethyst block not found in AFk GUI"); closeCurrentWindow(); scheduleAfkRetryCheck(AFK_POST_COMMAND_CHECK_DELAY_MS); return; } try { await bot.clickWindow(slot, 0, 0); log("Clicked amethyst block"); } catch (err) { log(`Failed to click amethyst block: ${err.message}`); } closeCurrentWindow(); // Teleport feedback can lag a few seconds after issuing /afk. scheduleAfkRetryCheck(AFK_POST_COMMAND_CHECK_DELAY_MS); }, delayMs); } function toPlainText(value) { if (value == null) return ""; if (typeof value === "string") return value; if ( typeof value.toString === "function" && value.toString !== Object.prototype.toString ) { const asString = value.toString(); if (asString && asString !== "[object Object]") return asString; } if (Array.isArray(value)) { return value.map(toPlainText).join(""); } if (typeof value === "object") { let text = ""; if (typeof value.text === "string") text += value.text; if (typeof value.translate === "string") text += value.translate; if (Array.isArray(value.with)) text += value.with.map(toPlainText).join(""); if (Array.isArray(value.extra)) text += value.extra.map(toPlainText).join(""); return text; } return String(value); } function logIfTeleport(source, text) { const normalized = String(text || "").trim(); if (!normalized) return; if (!TELEPORT_DETECT_REGEX.test(normalized)) return; log(`Teleport detected from ${source}: ${normalized}`); if (!hasDetectedTeleport) { hasDetectedTeleport = true; clearAfkRetryTimer(); log("Teleport confirmed; stopped AFK retry loop"); } } // Create bot const bot = mineflayer.createBot({ host: BOT_CONFIG.host, username: BOT_CONFIG.username, auth: BOT_CONFIG.auth, version: BOT_CONFIG.version, }); // Event handlers bot.on("login", () => { log("Bot logged in"); hasDetectedTeleport = false; clearAfkRetryTimer(); bot.setControlState("forward", false); sendToParent("botStatus", { username: runtimeUsername, status: "online", pid: process.pid, }); }); bot.on("spawn", () => { log("Bot spawned"); log(`Bot username: ${bot.username}`); if (hasDetectedTeleport) { log("Teleport already confirmed before spawn; skipping AFK retry schedule"); return; } scheduleAfkRetryCheck(AFK_INITIAL_CHECK_DELAY_MS); }); bot.on("windowOpen", (window) => { attachWindowUpdateHook(window); sendToParent("windowSnapshot", { username: runtimeUsername, window: serializeWindow(window), }); }); bot.on("windowClose", () => { detachWindowUpdateHook(); sendToParent("windowSnapshot", { username: runtimeUsername, window: null, }); }); bot.on("error", (err) => { log(`Error: ${err.message}`); sendToParent("botError", { username: runtimeUsername, error: err.message, }); }); bot.on("kicked", (reason) => { log(`Kicked: ${reason}`); sendToParent("botEvent", { username: runtimeUsername, event: "kicked", reason: toPlainText(reason), }); }); bot.on("end", () => { detachWindowUpdateHook(); clearAfkRetryTimer(); log("Bot disconnected"); sendToParent("botStatus", { username: runtimeUsername, status: "offline", pid: process.pid, }); process.exit(0); }); bot.on("chat", (username, message) => { if (username === bot.username) return; log(`Chat [${username}]: ${message}`); logIfTeleport("chat", message); }); bot.on("messagestr", (message, position) => { if (!message) return; if (position === "game_info") { log(`Hotbar: ${message}`); } logIfTeleport(`messagestr${position ? `:${position}` : ""}`, message); }); bot.on("message", (jsonMsg, position) => { const text = toPlainText(jsonMsg); if (!text) return; if (position === "game_info") { log(`Hotbar(json): ${text}`); } logIfTeleport(`message${position ? `:${position}` : ""}`, text); }); // Fallback packet hooks for title/actionbar channels on servers that do not // emit teleport text through regular chat events. bot._client.on("set_action_bar_text", (packet) => { const text = toPlainText(packet?.text); if (!text) return; log(`ActionBar: ${text}`); logIfTeleport("actionbar", text); }); bot._client.on("set_title_text", (packet) => { const text = toPlainText(packet?.text); if (!text) return; log(`Title: ${text}`); logIfTeleport("title", text); }); bot._client.on("set_subtitle_text", (packet) => { const text = toPlainText(packet?.text); if (!text) return; log(`Subtitle: ${text}`); logIfTeleport("subtitle", text); }); // Graceful shutdown process.on("SIGINT", () => { log("Shutdown signal received"); detachWindowUpdateHook(); clearAfkRetryTimer(); bot.quit(); }); process.on("message", async (message) => { if (!message || typeof message !== "object") return; const { type, requestId } = message; const respond = (ok, error) => { sendToParent("actionResult", { username: runtimeUsername, requestId, ok, error: error || null, }); }; try { if (type === "chat") { const text = String(message.message || "").trim(); if (!text) throw new Error("Chat message is empty"); bot.chat(text); respond(true); return; } if (type === "window.click") { if (!bot.currentWindow) throw new Error("No window open"); const slot = Number(message.slot); if (!Number.isInteger(slot) || slot < 0) { throw new Error("Invalid slot index"); } const mouseButton = Number.isInteger(Number(message.mouseButton)) ? Number(message.mouseButton) : 0; const mode = Number.isInteger(Number(message.mode)) ? Number(message.mode) : 0; await bot.clickWindow(slot, mouseButton, mode); await sleep(75); sendToParent("windowSnapshot", { username: runtimeUsername, window: serializeWindow(), }); respond(true); return; } if (type === "window.close") { closeCurrentWindow(); respond(true); return; } if (type === "quit") { respond(true); bot.quit(); return; } if (type === "state.request") { sendToParent("botStatus", { username: runtimeUsername, status: bot.player ? "online" : "connecting", pid: process.pid, }); sendToParent("windowSnapshot", { username: runtimeUsername, window: serializeWindow(), }); respond(true); return; } } catch (err) { respond(false, err.message || "Command failed"); } }); log("Starting AFK bot...");