working emergency leave

This commit is contained in:
ZareMate 2026-03-13 17:55:57 +01:00
parent 2241108f97
commit ad70adb0f1
2 changed files with 210 additions and 7 deletions

1
allowed_players.txt Normal file
View File

@ -0,0 +1 @@
ZareMate

216
index.js
View File

@ -1,4 +1,5 @@
const http = require("http");
const https = require("https");
const fs = require("fs");
const path = require("path");
const mineflayer = require("mineflayer");
@ -13,6 +14,12 @@ const LOOP_DELAY_MS = 10_000;
const MAX_ALLOWED_MOVE_BLOCKS = 10;
const REJOIN_DELAY_MS = 60_000;
const BULB_VERIFY_RETRIES = 3;
const SECURITY_RADIUS = 10;
const EMERGENCY_HOTBAR_SLOT = 0;
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || "";
const ALLOWED_PLAYERS_FILE =
process.env.ALLOWED_PLAYERS_FILE || "allowed_players.txt";
const ITEM_MODE = normalizeItemMode(
process.env.ITEM_MODE || process.env.SPAWNER_MODE || "bones",
@ -27,6 +34,29 @@ let spawnerPosition = null;
let reconnectTimer = null;
let bot = null;
let bothModeStep = 0;
let emergencyTriggered = false;
let securityMonitorInterval = null;
const allowedPlayers = loadAllowedPlayers();
function loadAllowedPlayers() {
const filePath = path.isAbsolute(ALLOWED_PLAYERS_FILE)
? ALLOWED_PLAYERS_FILE
: path.join(__dirname, ALLOWED_PLAYERS_FILE);
try {
const data = fs.readFileSync(filePath, "utf8");
return new Set(
data
.split(/\r?\n/)
.map((line) => line.trim().toLowerCase())
.filter((line) => line && !line.startsWith("#")),
);
} catch {
pushLog(`allowed players file not found: ${filePath}`);
return new Set();
}
}
function normalizeItemMode(value) {
const input = String(value || "")
@ -126,12 +156,14 @@ async function ensureBulbModeBeforeOpen(mode) {
function createBotInstance() {
hasJoined = false;
joinPosition = null;
spawnerPosition = null;
emergencyTriggered = false;
bot = mineflayer.createBot({
host: "java.donutsmp.net",
host: "localhost",
username: "Tayota4420",
auth: "microsoft",
version: "1.21.1",
version: "1.21.4",
});
bot.loadPlugin(gui);
@ -175,6 +207,148 @@ function pushLog(message) {
console.log(message);
}
async function sendDiscordWebhook(content) {
if (!DISCORD_WEBHOOK_URL) {
pushLog("DISCORD_WEBHOOK_URL not set; skipping webhook");
return;
}
try {
const body = JSON.stringify({ content });
const url = new URL(DISCORD_WEBHOOK_URL);
await new Promise((resolve, reject) => {
const req = https.request(
{
method: "POST",
hostname: url.hostname,
path: `${url.pathname}${url.search}`,
port: url.port || 443,
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
},
},
(res) => {
res.on("data", () => {});
res.on("end", () => resolve());
},
);
req.on("error", reject);
req.write(body);
req.end();
});
} catch (err) {
pushLog(`failed webhook send: ${err.message}`);
}
}
function isSpawnerBlock(block) {
if (!block) return false;
return ["chest", "trapped_chest"].includes(block.name);
}
function getUnauthorizedNearbyPlayers() {
if (!bot || !bot.entity || !bot.entity.position) return [];
const intruders = [];
for (const [username, player] of Object.entries(bot.players)) {
if (!username || username === bot.username) continue;
if (allowedPlayers.has(username.toLowerCase())) continue;
if (!player || !player.entity || !player.entity.position) continue;
const distance = player.entity.position.distanceTo(bot.entity.position);
if (distance <= SECURITY_RADIUS) {
intruders.push(`${username} (${distance.toFixed(1)}m)`);
}
}
return intruders;
}
async function breakSpawnerUntilGone() {
bot.quickBarSlot = EMERGENCY_HOTBAR_SLOT;
bot.setControlState("sneak", true);
try {
while (true) {
let targetBlock = null;
if (spawnerPosition) {
targetBlock = bot.blockAt(spawnerPosition);
}
if (!isSpawnerBlock(targetBlock)) {
targetBlock = findSpawnerBlock();
}
if (!isSpawnerBlock(targetBlock)) {
pushLog("chest no longer present");
return true;
}
spawnerPosition = targetBlock.position.clone();
await bot.lookAt(targetBlock.position.offset(0.5, 0.5, 0.5), true);
if (!bot.canDigBlock(targetBlock)) {
pushLog("cannot dig chest yet, retrying");
await sleep(300);
continue;
}
try {
await bot.dig(targetBlock, true);
} catch (err) {
pushLog(`chest dig failed: ${err.message}`);
await sleep(300);
}
}
} finally {
bot.setControlState("sneak", false);
}
}
async function triggerSecurityShutdown(reason) {
if (emergencyTriggered) return;
emergencyTriggered = true;
pushLog(`security trigger: ${reason}`);
stopSpawnerLoop("spawner loop stopped due to security trigger");
const spawnerDestroyed = await breakSpawnerUntilGone();
const status = spawnerDestroyed ? "chest removed" : "chest not removed";
await sendDiscordWebhook(`MCBOT ALERT: ${reason} | ${status}`);
try {
bot.quit("security shutdown");
} catch {
// ignore disconnect errors
}
}
function startSecurityMonitor() {
if (securityMonitorInterval) clearInterval(securityMonitorInterval);
securityMonitorInterval = setInterval(() => {
if (!hasJoined || !bot || emergencyTriggered) return;
const intruders = getUnauthorizedNearbyPlayers();
if (intruders.length > 0) {
triggerSecurityShutdown(
`unauthorized player nearby: ${intruders.join(", ")}`,
);
}
}, 1000);
}
function stopSecurityMonitor() {
if (!securityMonitorInterval) return;
clearInterval(securityMonitorInterval);
securityMonitorInterval = null;
}
const webPath = path.join(__dirname, "web", "index.html");
const server = http.createServer(async (req, res) => {
@ -289,7 +463,7 @@ function stopSpawnerLoop(reason) {
}
function findSpawnerBlock() {
const matching = ["spawner", "mob_spawner", "trial_spawner"]
const matching = ["chest", "trapped_chest"]
.filter((name) => bot.registry.blocksByName[name] !== undefined)
.map((name) => bot.registry.blocksByName[name].id);
@ -313,13 +487,13 @@ async function rotateToSpawner() {
if (targetBlock) {
spawnerPosition = targetBlock.position.clone();
pushLog(
`spawner position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`,
`chest position set: ${spawnerPosition.x}, ${spawnerPosition.y}, ${spawnerPosition.z}`,
);
}
}
if (!targetBlock) {
pushLog("no spawner found nearby");
pushLog("no chest found nearby");
return null;
}
@ -347,7 +521,7 @@ async function openSpawnerGui(maxAttempts = 2) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const targetBlock = await rotateToSpawner();
if (!targetBlock || targetBlock.name === "air") {
pushLog("unable to target spawner block");
pushLog("unable to target chest block");
return false;
}
@ -361,7 +535,7 @@ async function openSpawnerGui(maxAttempts = 2) {
}
pushLog(
`no gui opened from block '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`,
`no gui opened from chest '${targetBlock.name}' (attempt ${attempt}/${maxAttempts})`,
);
if (attempt < maxAttempts) {
await sleep(700);
@ -535,11 +709,34 @@ function bindBotEvents() {
hasJoined = true;
if (!bot.entity || !bot.entity.position) return;
joinPosition = bot.entity.position.clone();
startSecurityMonitor();
pushLog(
`join position set: ${joinPosition.x.toFixed(1)}, ${joinPosition.y.toFixed(1)}, ${joinPosition.z.toFixed(1)}`,
);
});
bot.on("blockUpdate", (oldBlock, newBlock) => {
if (
!hasJoined ||
emergencyTriggered ||
!bot.entity ||
!bot.entity.position
) {
return;
}
if (!oldBlock || !newBlock) return;
if (oldBlock.name === "air") return;
if (newBlock.name !== "air") return;
const distance = oldBlock.position.distanceTo(bot.entity.position);
if (distance <= SECURITY_RADIUS) {
triggerSecurityShutdown(
`block broken within ${SECURITY_RADIUS}m at ${oldBlock.position.x},${oldBlock.position.y},${oldBlock.position.z}`,
);
}
});
bot.on("physicsTick", () => {
if (
!spawnerLoopRunning ||
@ -560,6 +757,7 @@ function bindBotEvents() {
bot.on("kicked", (reason) => scheduleRejoinIfAlreadyOnline(reason));
bot.on("error", (err) => pushLog(`error: ${err.message}`));
bot.on("end", () => stopSecurityMonitor());
}
function itemToString(item) {
@ -578,3 +776,7 @@ if (BULB_POSITION && BUTTON_POSITION) {
} else {
pushLog("set BULB_POS and BUTTON_POS in .env to enable mode checking");
}
pushLog(
`security radius=${SECURITY_RADIUS}m, allowed players file=${ALLOWED_PLAYERS_FILE}`,
);