DonutBot/afk.js
ZareMate 24c5debffc feat: implement AFK Bot Control UI and server functionality
- Updated index.html to reflect new title and layout for AFK Bot Control.
- Enhanced styling for better user experience with new color scheme and responsive design.
- Added WebSocket server in server.js to handle communication between browser clients and the parent process.
- Implemented bot management features including start, stop, and chat functionalities.
- Introduced logging mechanism to relay server and bot logs to the UI.
- Exported functions from hashed_profiles.js for better modularity.
- Added ws package to package.json for WebSocket support.
2026-04-02 15:28:41 +02:00

494 lines
12 KiB
JavaScript

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...");