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(); // 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 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 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); }); 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("error", (err) => { log(`Error: ${err.message}`); }); bot.on("kicked", (reason) => { log(`Kicked: ${reason}`); }); bot.on("end", () => { clearAfkRetryTimer(); log("Bot disconnected"); 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"); clearAfkRetryTimer(); bot.quit(); }); log("Starting AFK bot...");