From a1e08ca7924fec2e926e34d4833f094eeee94250 Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Wed, 11 Mar 2026 11:42:49 +0100 Subject: [PATCH] initial commit: add main bot functionality, web interface, and configuration files --- .gitignore | 2 + index.js | 400 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 18 +++ web/index.html | 184 +++++++++++++++++++++++ 4 files changed, 604 insertions(+) create mode 100644 .gitignore create mode 100644 index.js create mode 100644 package.json create mode 100644 web/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..572406b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +package-lock.json \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..0e10616 --- /dev/null +++ b/index.js @@ -0,0 +1,400 @@ +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(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb1722a --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "mcbot", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "mineflayer": "^4.35.0", + "mineflayer-gui": "^4.0.2", + "mineflayer-pathfinder": "^2.4.5", + "prismarine-viewer": "^1.33.0" + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0b43cd1 --- /dev/null +++ b/web/index.html @@ -0,0 +1,184 @@ + + +
+ + +