initial commit: add main bot functionality, web interface, and configuration files
This commit is contained in:
commit
a1e08ca792
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
package-lock.json
|
||||
400
index.js
Normal file
400
index.js
Normal file
@ -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();
|
||||
18
package.json
Normal file
18
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
184
web/index.html
Normal file
184
web/index.html
Normal file
@ -0,0 +1,184 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spawner Control</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
|
||||
Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: radial-gradient(1200px 600px at 20% -10%, #1f2a44 0%, #0d1018 45%, #090b12 100%);
|
||||
color: #e6eaf2;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: min(900px, calc(100vw - 2rem));
|
||||
background: rgba(18, 22, 34, 0.9);
|
||||
border: 1px solid rgba(154, 167, 199, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(154, 167, 199, 0.35);
|
||||
border-radius: 10px;
|
||||
padding: 0.58rem 0.95rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(180deg, #212a43 0%, #171f34 100%);
|
||||
color: #edf2ff;
|
||||
transition: transform 120ms ease, filter 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.08);
|
||||
border-color: rgba(154, 167, 199, 0.6);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
#status {
|
||||
color: #aeb8cf;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
#logs {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid rgba(154, 167, 199, 0.22);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem;
|
||||
min-height: 280px;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
background: rgba(7, 10, 17, 0.7);
|
||||
}
|
||||
|
||||
#logs::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
#logs::-webkit-scrollbar-thumb {
|
||||
background: rgba(154, 167, 199, 0.35);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin: 0.4rem 0;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #96a2c2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app">
|
||||
<h1>Spawner Control</h1>
|
||||
<div class="toolbar">
|
||||
<button id="openSpawner">Open Spawner</button>
|
||||
<button id="stopSpawner">Stop Loop</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
|
||||
<div id="logs" aria-live="polite"></div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const statusEl = document.getElementById("status");
|
||||
const logsEl = document.getElementById("logs");
|
||||
const openBtn = document.getElementById("openSpawner");
|
||||
const stopBtn = document.getElementById("stopSpawner");
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
async function openSpawner() {
|
||||
setStatus("opening...");
|
||||
try {
|
||||
const res = await fetch("/api/spawner", {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "Request failed");
|
||||
}
|
||||
setStatus("done");
|
||||
} catch (err) {
|
||||
setStatus(`error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopSpawner() {
|
||||
setStatus("stopping...");
|
||||
try {
|
||||
const res = await fetch("/api/stop", {
|
||||
method: "POST",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "Request failed");
|
||||
}
|
||||
setStatus("stopped");
|
||||
} catch (err) {
|
||||
setStatus(`error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLogs() {
|
||||
try {
|
||||
const res = await fetch("/api/logs");
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
logsEl.innerHTML = "";
|
||||
for (const item of data.logs) {
|
||||
const line = document.createElement("div");
|
||||
line.className = "line";
|
||||
line.innerHTML = `<span class="time">[${new Date(item.time).toLocaleTimeString()}]</span> ${item.message}`;
|
||||
logsEl.appendChild(line);
|
||||
}
|
||||
logsEl.scrollTop = logsEl.scrollHeight;
|
||||
} catch {
|
||||
setStatus("could not fetch logs");
|
||||
}
|
||||
}
|
||||
|
||||
openBtn.addEventListener("click", openSpawner);
|
||||
stopBtn.addEventListener("click", stopSpawner);
|
||||
refreshLogs();
|
||||
setInterval(refreshLogs, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user