feat: enhance AFK bot with dynamic username configuration and logging improvements
This commit is contained in:
parent
ff255bea38
commit
d28a55cfcd
23
afk.js
23
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
|
||||
|
||||
183
afk_parent.js
Normal file
183
afk_parent.js
Normal file
@ -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);
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user