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 fs = require("fs");
|
||||||
const path = require("path");
|
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
|
// Configuration
|
||||||
const BOT_CONFIG = {
|
const BOT_CONFIG = {
|
||||||
host: "java.donutsmp.net",
|
host: "java.donutsmp.net",
|
||||||
username: "AFKBot",
|
username: runtimeUsername,
|
||||||
auth: "microsoft",
|
auth: "microsoft",
|
||||||
version: "1.21.1",
|
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;
|
const TELEPORT_DETECT_REGEX = /you teleported to\b/i;
|
||||||
|
|
||||||
// Logging
|
// 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",
|
"type": "commonjs",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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": {
|
"dependencies": {
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user