313 lines
7.6 KiB
JavaScript
313 lines
7.6 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();
|
|
|
|
// 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...");
|