DonutBot/index.js

401 lines
9.4 KiB
JavaScript

const http = require("http");
const fs = require("fs");
const path = require("path");
const mineflayer = require("mineflayer");
const gui = require("mineflayer-gui");
const logs = [];
const MAX_LOGS = 200;
const LOOP_DELAY_MS = 30_000;
const MAX_ALLOWED_MOVE_BLOCKS = 10;
const REJOIN_DELAY_MS = 60_000;
let spawnerLoopRunning = false;
let joinPosition = null;
let hasJoined = false;
let spawnerPosition = null;
let reconnectTimer = null;
let bot = null;
function createBotInstance() {
hasJoined = false;
joinPosition = null;
bot = mineflayer.createBot({
host: "java.donutsmp.net",
username: "Tayota4420",
auth: "microsoft",
version: "1.21.1",
});
bot.loadPlugin(gui);
bindBotEvents();
}
function stringifyReason(reason) {
if (typeof reason === "string") return reason;
try {
return JSON.stringify(reason);
} catch {
return String(reason);
}
}
function scheduleRejoinIfAlreadyOnline(reason) {
const reasonText = stringifyReason(reason);
pushLog(`kicked: ${reasonText}`);
if (!reasonText.includes("You are already online")) return;
if (reconnectTimer) {
pushLog("rejoin already scheduled");
return;
}
stopSpawnerLoop("stopping loop due to kick");
pushLog("already online kick detected, rejoining in 60s");
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
pushLog("rejoining now...");
createBotInstance();
}, REJOIN_DELAY_MS);
}
function pushLog(message) {
logs.push({
time: new Date().toISOString(),
message,
});
if (logs.length > MAX_LOGS) logs.shift();
console.log(message);
}
const webPath = path.join(__dirname, "web", "index.html");
const server = http.createServer(async (req, res) => {
if (req.method === "GET" && req.url === "/") {
try {
const html = fs.readFileSync(webPath, "utf8");
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(html);
} catch {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end("UI file missing");
}
return;
}
if (req.method === "GET" && req.url === "/api/logs") {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ logs }));
return;
}
if (req.method === "POST" && req.url === "/api/spawner") {
try {
startSpawnerLoop();
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ ok: true, running: spawnerLoopRunning }));
} catch (err) {
pushLog(`spawner error: ${err.message}`);
res.writeHead(500, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ ok: false, error: err.message }));
}
return;
}
if (req.method === "POST" && req.url === "/api/stop") {
try {
stopSpawnerLoop("spawner loop stopped by user");
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ ok: true, running: spawnerLoopRunning }));
} catch (err) {
pushLog(`stop error: ${err.message}`);
res.writeHead(500, {
"Content-Type": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ ok: false, error: err.message }));
}
return;
}
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not found");
});
server.listen(3000, () => {
pushLog("web ui running at http://localhost:3000");
});
function sayItems(items = []) {
const output = items.map(itemToString).join(", ");
pushLog(output || "empty");
}
function readCurrentWindowInventory() {
const window = bot.currentWindow;
if (!window) {
pushLog("no window open");
return;
}
const items =
typeof window.containerItems === "function"
? window.containerItems()
: typeof window.items === "function"
? window.items()
: [];
const title =
typeof window.title === "string"
? window.title
: window.title && typeof window.title === "object"
? JSON.stringify(window.title)
: "custom gui";
pushLog(`window opened: ${title}`);
sayItems(items);
}
function comparator(item, pItem) {
if (!pItem) return false;
const target = String(item).toLowerCase();
return (
String(pItem.displayName || "").toLowerCase() === target ||
String(pItem.name || "").toLowerCase() === target
);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function stopSpawnerLoop(reason) {
if (!spawnerLoopRunning) return;
spawnerLoopRunning = false;
pushLog(reason || "spawner loop stopped");
closeCurrentWindow();
}
function findSpawnerBlock() {
const matching = ["spawner", "mob_spawner", "trial_spawner"]
.filter((name) => bot.registry.blocksByName[name] !== undefined)
.map((name) => bot.registry.blocksByName[name].id);
if (matching.length === 0) return null;
return bot.findBlock({
matching,
maxDistance: 6,
});
}
async function rotateToSpawner() {
let targetBlock = null;
if (spawnerPosition) {
targetBlock = bot.blockAt(spawnerPosition);
}
if (!targetBlock) {
targetBlock = findSpawnerBlock();
if (targetBlock) {
spawnerPosition = targetBlock.position.clone();
pushLog(
`spawner position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`,
);
}
}
if (!targetBlock) {
pushLog("no spawner found nearby");
return null;
}
await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true);
return targetBlock;
}
function waitForWindowOpen(timeoutMs = 5000) {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
bot.removeListener("windowOpen", onOpen);
resolve(false);
}, timeoutMs);
const onOpen = () => {
clearTimeout(timeout);
resolve(true);
};
bot.once("windowOpen", onOpen);
});
}
async function openSpawnerGui() {
const targetBlock = await rotateToSpawner();
if (!targetBlock || targetBlock.name === "air") {
pushLog("unable to target spawner block");
return;
}
const openPromise = waitForWindowOpen(5000);
await bot.activateBlock(targetBlock);
const opened = await openPromise;
if (!opened) {
pushLog(`no gui opened from block '${targetBlock.name}'`);
return false;
}
return true;
}
function getWindowItems() {
const window = bot.currentWindow;
if (!window) return [];
if (typeof window.containerItems === "function")
return window.containerItems();
if (typeof window.items === "function") return window.items();
return [];
}
async function dropAllBonesFromCurrentWindow() {
if (!bot.currentWindow) {
pushLog("no window open to drop bones");
return;
}
let dropped = 0;
while (true) {
const boneStacks = getWindowItems().filter(
(item) => item && String(item.name || "").toLowerCase() === "bone",
);
const boneCount = boneStacks.reduce(
(sum, item) => sum + (item.count || 0),
0,
);
if (boneCount === 0) break;
const ok = await bot.gui
.Query()
.Window(comparator)
.Drop("Bone", 64)
.end()
.run();
if (!ok) {
pushLog("failed to drop bones from window");
break;
}
dropped += Math.min(64, boneCount);
await sleep(150);
}
pushLog(`dropped bones total: ${dropped}`);
}
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();
}
}
async function runSpawnerCycle() {
const opened = await openSpawnerGui();
if (!opened) return;
await dropAllBonesFromCurrentWindow();
closeCurrentWindow();
}
function startSpawnerLoop() {
if (spawnerLoopRunning) {
pushLog("spawner loop already running");
return;
}
if (!joinPosition) {
pushLog("join position not set yet; wait for spawn");
return;
}
spawnerLoopRunning = true;
pushLog("spawner loop started");
(async () => {
while (spawnerLoopRunning) {
try {
await runSpawnerCycle();
} catch (err) {
pushLog(`cycle error: ${err.message}`);
}
if (!spawnerLoopRunning) break;
pushLog("waiting 30s for next cycle");
await sleep(LOOP_DELAY_MS);
}
})();
}
function bindBotEvents() {
bot.on("windowOpen", () => {
readCurrentWindowInventory();
});
bot.on("windowClose", () => {
if (!hasJoined) return;
pushLog("window closed");
});
bot.once("spawn", () => {
hasJoined = true;
if (!bot.entity || !bot.entity.position) return;
joinPosition = bot.entity.position.clone();
pushLog(
`join position set: ${joinPosition.x.toFixed(1)}, ${joinPosition.y.toFixed(1)}, ${joinPosition.z.toFixed(1)}`,
);
});
bot.on("physicsTick", () => {
if (
!spawnerLoopRunning ||
!joinPosition ||
!bot.entity ||
!bot.entity.position
) {
return;
}
const movedDistance = bot.entity.position.distanceTo(joinPosition);
if (movedDistance > MAX_ALLOWED_MOVE_BLOCKS) {
stopSpawnerLoop(
`moved ${movedDistance.toFixed(2)} blocks from join position (> ${MAX_ALLOWED_MOVE_BLOCKS}), stopping loop`,
);
}
});
bot.on("kicked", (reason) => scheduleRejoinIfAlreadyOnline(reason));
bot.on("error", (err) => pushLog(`error: ${err.message}`));
}
function itemToString(item) {
if (item) {
return `${item.name} x ${item.count}`;
}
return "(nothing)";
}
createBotInstance();