From d28a55cfcda4372e31ba1c18d85842004b57d1de Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Fri, 27 Mar 2026 02:30:52 +0100 Subject: [PATCH] feat: enhance AFK bot with dynamic username configuration and logging improvements --- afk.js | 23 ++++++- afk_parent.js | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 afk_parent.js diff --git a/afk.js b/afk.js index 788f303..ba8c3e1 100644 --- a/afk.js +++ b/afk.js @@ -2,15 +2,34 @@ const mineflayer = require("mineflayer"); 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: "AFKBot", + username: runtimeUsername, auth: "microsoft", version: "1.21.1", }; -const LOG_FILE = path.join(__dirname, "afk.log"); +const LOG_FILE = path.join( + __dirname, + `afk-${toSafeFilePart(runtimeUsername)}.log`, +); const TELEPORT_DETECT_REGEX = /you teleported to\b/i; // Logging diff --git a/afk_parent.js b/afk_parent.js new file mode 100644 index 0000000..be367f5 --- /dev/null +++ b/afk_parent.js @@ -0,0 +1,183 @@ +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// Add all bot usernames here. +const USERNAMES = ["ZareMate", "Tomek"]; + +const AFK_SCRIPT = path.join(__dirname, "afk.js"); +const OUTPUT_MODE = ( + process.env.PARENT_OUTPUT_MODE || "prefixed" +).toLowerCase(); +const SPLIT_LOGS_ENABLED = process.env.PARENT_SPLIT_LOGS === "1"; +const RESTART_MIN_MS = 2 * 60 * 1000; +const RESTART_MAX_MS = 5 * 60 * 1000; + +const ALREADY_ONLINE_TEXT = "You are already online, try restarting your game."; +const TIMEOUT_REGEX = /timeout|timed out|ETIMEDOUT|ECONNRESET|socket hang up/i; + +let isShuttingDown = false; + +function toSafeFilePart(value) { + return String(value).replace(/[^a-zA-Z0-9_-]/g, "_"); +} + +function writeSplitLog(logFile, line) { + if (!logFile) return; + const timestamp = new Date().toISOString(); + fs.appendFileSync(logFile, `[${timestamp}] ${line}\n`); +} + +function withUsernamePrefix(username, line) { + const prefix = `[${username}] `; + if (line.startsWith(prefix)) return line; + return `${prefix}${line}`; +} + +function randomRestartDelayMs() { + return ( + RESTART_MIN_MS + + Math.floor(Math.random() * (RESTART_MAX_MS - RESTART_MIN_MS + 1)) + ); +} + +function shouldRestartFromState(state) { + if (state.sawKick) return true; + if (state.sawTimeout) return true; + if (state.sawAlreadyOnline) return true; + return false; +} + +function updateRestartSignalsFromLine(state, line) { + if (!line) return; + + if (line.includes("Kicked:")) { + state.sawKick = true; + } + if (TIMEOUT_REGEX.test(line)) { + state.sawTimeout = true; + } + if (line.includes(ALREADY_ONLINE_TEXT)) { + state.sawAlreadyOnline = true; + } +} + +function startBot(state) { + const { username } = state; + const shouldPrefix = OUTPUT_MODE !== "raw"; + const logFile = SPLIT_LOGS_ENABLED + ? path.join(__dirname, `afk-parent-${toSafeFilePart(username)}.log`) + : null; + + state.sawKick = false; + state.sawTimeout = false; + state.sawAlreadyOnline = false; + state.child = null; + + const child = spawn(process.execPath, [AFK_SCRIPT, username], { + cwd: __dirname, + stdio: shouldPrefix ? ["inherit", "pipe", "pipe"] : "inherit", + }); + state.child = child; + + console.log(`[parent] started ${username} (pid=${child.pid})`); + + child.on("exit", (code, signal) => { + const reason = signal ? `signal ${signal}` : `code ${code}`; + console.log(`[parent] ${username} exited (${reason})`); + + if (isShuttingDown) return; + if (!shouldRestartFromState(state)) return; + + const delay = randomRestartDelayMs(); + const seconds = Math.round(delay / 1000); + console.log(`[parent] scheduling restart for ${username} in ${seconds}s`); + + if (state.restartTimer) clearTimeout(state.restartTimer); + state.restartTimer = setTimeout(() => { + if (isShuttingDown) return; + startBot(state); + }, delay); + }); + + child.on("error", (err) => { + console.error(`[parent] failed to start ${username}: ${err.message}`); + }); + + if (shouldPrefix) { + let stdoutBuffer = ""; + let stderrBuffer = ""; + + child.stdout.on("data", (chunk) => { + const text = stdoutBuffer + chunk.toString("utf8"); + const lines = text.split(/\r?\n/); + stdoutBuffer = lines.pop() || ""; + for (const line of lines) { + if (!line) continue; + updateRestartSignalsFromLine(state, line); + const rendered = withUsernamePrefix(username, line); + process.stdout.write(`${rendered}\n`); + writeSplitLog(logFile, `stdout: ${line}`); + } + }); + + child.stderr.on("data", (chunk) => { + const text = stderrBuffer + chunk.toString("utf8"); + const lines = text.split(/\r?\n/); + stderrBuffer = lines.pop() || ""; + for (const line of lines) { + if (!line) continue; + updateRestartSignalsFromLine(state, line); + const rendered = withUsernamePrefix(username, line); + process.stderr.write(`${rendered}\n`); + writeSplitLog(logFile, `stderr: ${line}`); + } + }); + + child.on("close", () => { + if (stdoutBuffer) { + updateRestartSignalsFromLine(state, stdoutBuffer); + const rendered = withUsernamePrefix(username, stdoutBuffer); + process.stdout.write(`${rendered}\n`); + writeSplitLog(logFile, `stdout: ${stdoutBuffer}`); + } + if (stderrBuffer) { + updateRestartSignalsFromLine(state, stderrBuffer); + const rendered = withUsernamePrefix(username, stderrBuffer); + process.stderr.write(`${rendered}\n`); + writeSplitLog(logFile, `stderr: ${stderrBuffer}`); + } + }); + } + + return child; +} + +const botStates = USERNAMES.map((username) => ({ + username, + child: null, + restartTimer: null, + sawKick: false, + sawTimeout: false, + sawAlreadyOnline: false, +})); + +for (const state of botStates) { + startBot(state); +} + +function shutdownAll() { + isShuttingDown = true; + console.log("[parent] shutdown requested"); + for (const state of botStates) { + if (state.restartTimer) { + clearTimeout(state.restartTimer); + state.restartTimer = null; + } + const child = state.child; + if (child && !child.killed) child.kill("SIGINT"); + } +} + +process.on("SIGINT", shutdownAll); +process.on("SIGTERM", shutdownAll); diff --git a/package.json b/package.json index ec11cd8..46a7bf2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "type": "commonjs", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "afk": "node afk.js", + "afk:parent": "node afk_parent.js" }, "dependencies": { "dotenv": "^17.3.1",