|
|
const fs = require("fs"); |
|
|
const express = require("express"); |
|
|
const bodyParser = require("body-parser"); |
|
|
const mineflayer = require("mineflayer"); |
|
|
|
|
|
const skills = require("./lib/skillLoader"); |
|
|
const {initCounter, getNextTime} = require("./lib/utils"); |
|
|
const obs = require("./lib/observation/base"); |
|
|
const OnChat = require("./lib/observation/onChat"); |
|
|
const OnError = require("./lib/observation/onError"); |
|
|
const {Voxels, BlockRecords} = require("./lib/observation/voxels"); |
|
|
const Status = require("./lib/observation/status"); |
|
|
const Inventory = require("./lib/observation/inventory"); |
|
|
const OnSave = require("./lib/observation/onSave"); |
|
|
const Chests = require("./lib/observation/chests"); |
|
|
const {plugin: tool} = require("mineflayer-tool"); |
|
|
|
|
|
const {pathfinder, Movements, goals} = require('mineflayer-pathfinder') |
|
|
const {GoalXZ, GoalBlock} = goals |
|
|
|
|
|
|
|
|
const mineflayerViewer = require('prismarine-viewer').mineflayer |
|
|
|
|
|
let bot = null; |
|
|
|
|
|
const app = express(); |
|
|
|
|
|
app.use(bodyParser.json({limit: "50mb"})); |
|
|
app.use(bodyParser.urlencoded({limit: "50mb", extended: false})); |
|
|
|
|
|
app.post("/start", (req, res) => { |
|
|
if (bot) onDisconnect("Restarting bot"); |
|
|
bot = null; |
|
|
console.log(req.body); |
|
|
bot = mineflayer.createBot({ |
|
|
host: "localhost", |
|
|
port: req.body.port, |
|
|
username: `bot_${PORT}`, |
|
|
disableChatSigning: true, |
|
|
checkTimeoutInterval: 60 * 60 * 1000, |
|
|
}); |
|
|
bot.once("error", onConnectionFailed); |
|
|
|
|
|
|
|
|
bot.waitTicks = req.body.waitTicks; |
|
|
bot.globalTickCounter = 0; |
|
|
bot.stuckTickCounter = 0; |
|
|
bot.stuckPosList = []; |
|
|
bot.iron_pickaxe = false; |
|
|
|
|
|
bot.on("kicked", onDisconnect); |
|
|
|
|
|
|
|
|
bot.on("mount", () => { |
|
|
bot.dismount(); |
|
|
}); |
|
|
|
|
|
bot.once("spawn", async () => { |
|
|
if (VISUAL_SERVER_PORT !== "-1") { |
|
|
console.log("Initializing Mineflayer viewer..."); |
|
|
mineflayerViewer(bot, {firstPerson: true, port: Number(VISUAL_SERVER_PORT)}); |
|
|
console.log("Mineflayer viewer initialized."); |
|
|
} |
|
|
bot.removeListener("error", onConnectionFailed); |
|
|
let itemTicks = 1; |
|
|
if (req.body.reset === "hard") { |
|
|
bot.chat("/clear @s"); |
|
|
bot.chat("/kill @s"); |
|
|
const inventory = req.body.inventory ? req.body.inventory : {}; |
|
|
const equipment = req.body.equipment |
|
|
? req.body.equipment |
|
|
: [null, null, null, null, null, null]; |
|
|
for (let key in inventory) { |
|
|
bot.chat(`/give @s minecraft:${key} ${inventory[key]}`); |
|
|
itemTicks += 1; |
|
|
} |
|
|
const equipmentNames = [ |
|
|
"armor.head", |
|
|
"armor.chest", |
|
|
"armor.legs", |
|
|
"armor.feet", |
|
|
"weapon.mainhand", |
|
|
"weapon.offhand", |
|
|
]; |
|
|
for (let i = 0; i < 6; i++) { |
|
|
if (i === 4) continue; |
|
|
if (equipment[i]) { |
|
|
bot.chat( |
|
|
`/item replace entity @s ${equipmentNames[i]} with minecraft:${equipment[i]}` |
|
|
); |
|
|
itemTicks += 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (req.body.position) { |
|
|
bot.chat( |
|
|
`/tp @s ${req.body.position.x} ${req.body.position.y} ${req.body.position.z}` |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const {pathfinder} = require("mineflayer-pathfinder"); |
|
|
const tool = require("mineflayer-tool").plugin; |
|
|
const collectBlock = require("mineflayer-collectblock").plugin; |
|
|
const pvp = require("mineflayer-pvp").plugin; |
|
|
const minecraftHawkEye = require("minecrafthawkeye"); |
|
|
bot.loadPlugin(pathfinder); |
|
|
bot.loadPlugin(tool); |
|
|
bot.loadPlugin(collectBlock); |
|
|
bot.loadPlugin(pvp); |
|
|
bot.loadPlugin(minecraftHawkEye); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
obs.inject(bot, [ |
|
|
OnChat, |
|
|
OnError, |
|
|
Voxels, |
|
|
Status, |
|
|
Inventory, |
|
|
OnSave, |
|
|
Chests, |
|
|
BlockRecords, |
|
|
]); |
|
|
skills.inject(bot); |
|
|
|
|
|
if (req.body.spread) { |
|
|
bot.chat(`/spreadplayers ~ ~ 0 300 under 80 false @s`); |
|
|
await bot.waitForTicks(bot.waitTicks); |
|
|
} |
|
|
|
|
|
await bot.waitForTicks(bot.waitTicks * itemTicks); |
|
|
res.json(bot.observe()); |
|
|
|
|
|
initCounter(bot); |
|
|
bot.chat("/gamerule keepInventory true"); |
|
|
bot.chat("/gamerule doDaylightCycle false"); |
|
|
}); |
|
|
|
|
|
function onConnectionFailed(e) { |
|
|
console.log(e); |
|
|
bot = null; |
|
|
res.status(400).json({error: e}); |
|
|
} |
|
|
|
|
|
function onDisconnect(message) { |
|
|
if (bot.viewer) { |
|
|
bot.viewer.close(); |
|
|
} |
|
|
bot.end(); |
|
|
console.log(message); |
|
|
bot = null; |
|
|
} |
|
|
}); |
|
|
|
|
|
app.post("/step", async (req, res) => { |
|
|
|
|
|
let response_sent = false; |
|
|
|
|
|
function otherError(err) { |
|
|
console.log("Uncaught Error"); |
|
|
bot.emit("error", handleError(err)); |
|
|
bot.waitForTicks(bot.waitTicks).then(() => { |
|
|
if (!response_sent) { |
|
|
response_sent = true; |
|
|
res.json(bot.observe()); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
process.on("uncaughtException", otherError); |
|
|
|
|
|
const mcData = require("minecraft-data")(bot.version); |
|
|
mcData.itemsByName["leather_cap"] = mcData.itemsByName["leather_helmet"]; |
|
|
mcData.itemsByName["leather_tunic"] = |
|
|
mcData.itemsByName["leather_chestplate"]; |
|
|
mcData.itemsByName["leather_pants"] = |
|
|
mcData.itemsByName["leather_leggings"]; |
|
|
mcData.itemsByName["leather_boots"] = mcData.itemsByName["leather_boots"]; |
|
|
mcData.itemsByName["lapis_lazuli_ore"] = mcData.itemsByName["lapis_ore"]; |
|
|
mcData.blocksByName["lapis_lazuli_ore"] = mcData.blocksByName["lapis_ore"]; |
|
|
const { |
|
|
Movements, |
|
|
goals: { |
|
|
Goal, |
|
|
GoalBlock, |
|
|
GoalNear, |
|
|
GoalXZ, |
|
|
GoalNearXZ, |
|
|
GoalY, |
|
|
GoalGetToBlock, |
|
|
GoalLookAtBlock, |
|
|
GoalBreakBlock, |
|
|
GoalCompositeAny, |
|
|
GoalCompositeAll, |
|
|
GoalInvert, |
|
|
GoalFollow, |
|
|
GoalPlaceBlock, |
|
|
}, |
|
|
pathfinder, |
|
|
Move, |
|
|
ComputedPath, |
|
|
PartiallyComputedPath, |
|
|
XZCoordinates, |
|
|
XYZCoordinates, |
|
|
SafeBlock, |
|
|
GoalPlaceBlockOptions, |
|
|
} = require("mineflayer-pathfinder"); |
|
|
const {Vec3} = require("vec3"); |
|
|
|
|
|
|
|
|
const movements = new Movements(bot, mcData); |
|
|
bot.pathfinder.setMovements(movements); |
|
|
|
|
|
bot.globalTickCounter = 0; |
|
|
bot.stuckTickCounter = 0; |
|
|
bot.stuckPosList = []; |
|
|
|
|
|
function onTick() { |
|
|
bot.globalTickCounter++; |
|
|
if (bot.pathfinder.isMoving()) { |
|
|
bot.stuckTickCounter++; |
|
|
if (bot.stuckTickCounter >= 100) { |
|
|
onStuck(1.5); |
|
|
bot.stuckTickCounter = 0; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
bot.on("physicTick", onTick); |
|
|
|
|
|
|
|
|
let _craftItemFailCount = 0; |
|
|
let _killMobFailCount = 0; |
|
|
let _mineBlockFailCount = 0; |
|
|
let _placeItemFailCount = 0; |
|
|
let _smeltItemFailCount = 0; |
|
|
|
|
|
|
|
|
const code = req.body.code; |
|
|
const programs = req.body.programs; |
|
|
bot.cumulativeObs = []; |
|
|
await bot.waitForTicks(bot.waitTicks); |
|
|
const r = await evaluateCode(code, programs); |
|
|
process.off("uncaughtException", otherError); |
|
|
if (r !== "success") { |
|
|
bot.emit("error", handleError(r)); |
|
|
} |
|
|
await returnItems(); |
|
|
|
|
|
await bot.waitForTicks(bot.waitTicks); |
|
|
if (!response_sent) { |
|
|
response_sent = true; |
|
|
res.json(bot.observe()); |
|
|
} |
|
|
bot.removeListener("physicTick", onTick); |
|
|
|
|
|
async function evaluateCode(code, programs) { |
|
|
|
|
|
try { |
|
|
await eval("(async () => {" + programs + "\n" + code + "})()"); |
|
|
return "success"; |
|
|
} catch (err) { |
|
|
return err; |
|
|
} |
|
|
} |
|
|
|
|
|
function onStuck(posThreshold) { |
|
|
const currentPos = bot.entity.position; |
|
|
bot.stuckPosList.push(currentPos); |
|
|
|
|
|
|
|
|
if (bot.stuckPosList.length === 5) { |
|
|
const oldestPos = bot.stuckPosList[0]; |
|
|
const posDifference = currentPos.distanceTo(oldestPos); |
|
|
|
|
|
if (posDifference < posThreshold) { |
|
|
teleportBot(); |
|
|
} |
|
|
|
|
|
|
|
|
bot.stuckPosList.shift(); |
|
|
} |
|
|
} |
|
|
|
|
|
function teleportBot() { |
|
|
const blocks = bot.findBlocks({ |
|
|
matching: (block) => { |
|
|
return block.type === 0; |
|
|
}, |
|
|
maxDistance: 1, |
|
|
count: 27, |
|
|
}); |
|
|
|
|
|
if (blocks) { |
|
|
|
|
|
const randomIndex = Math.floor(Math.random() * blocks.length); |
|
|
const block = blocks[randomIndex]; |
|
|
bot.chat(`/tp @s ${block.x} ${block.y} ${block.z}`); |
|
|
} else { |
|
|
bot.chat("/tp @s ~ ~1.25 ~"); |
|
|
} |
|
|
} |
|
|
|
|
|
function returnItems() { |
|
|
bot.chat("/gamerule doTileDrops false"); |
|
|
const crafting_table = bot.findBlock({ |
|
|
matching: mcData.blocksByName.crafting_table.id, |
|
|
maxDistance: 128, |
|
|
}); |
|
|
if (crafting_table) { |
|
|
bot.chat( |
|
|
`/setblock ${crafting_table.position.x} ${crafting_table.position.y} ${crafting_table.position.z} air destroy` |
|
|
); |
|
|
bot.chat("/give @s crafting_table"); |
|
|
} |
|
|
const furnace = bot.findBlock({ |
|
|
matching: mcData.blocksByName.furnace.id, |
|
|
maxDistance: 128, |
|
|
}); |
|
|
if (furnace) { |
|
|
bot.chat( |
|
|
`/setblock ${furnace.position.x} ${furnace.position.y} ${furnace.position.z} air destroy` |
|
|
); |
|
|
bot.chat("/give @s furnace"); |
|
|
} |
|
|
if (bot.inventoryUsed() >= 32) { |
|
|
|
|
|
if (!bot.inventory.items().find((item) => item.name === "chest")) { |
|
|
bot.chat("/give @s chest"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bot.chat("/gamerule doTileDrops true"); |
|
|
} |
|
|
|
|
|
function handleError(err) { |
|
|
let stack = err.stack; |
|
|
if (!stack) { |
|
|
return err; |
|
|
} |
|
|
console.log(stack); |
|
|
const final_line = stack.split("\n")[1]; |
|
|
const regex = /<anonymous>:(\d+):\d+\)/; |
|
|
|
|
|
const programs_length = programs.split("\n").length; |
|
|
let match_line = null; |
|
|
for (const line of stack.split("\n")) { |
|
|
const match = regex.exec(line); |
|
|
if (match) { |
|
|
const line_num = parseInt(match[1]); |
|
|
if (line_num >= programs_length) { |
|
|
match_line = line_num - programs_length; |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
if (!match_line) { |
|
|
return err.message; |
|
|
} |
|
|
let f_line = final_line.match( |
|
|
/\((?<file>.*):(?<line>\d+):(?<pos>\d+)\)/ |
|
|
); |
|
|
if (f_line && f_line.groups && fs.existsSync(f_line.groups.file)) { |
|
|
const {file, line, pos} = f_line.groups; |
|
|
const f = fs.readFileSync(file, "utf8").split("\n"); |
|
|
|
|
|
let source = file + `:${line}\n${f[line - 1].trim()}\n `; |
|
|
|
|
|
const code_source = |
|
|
"at " + |
|
|
code.split("\n")[match_line - 1].trim() + |
|
|
" in your code"; |
|
|
return source + err.message + "\n" + code_source; |
|
|
} else if ( |
|
|
f_line && |
|
|
f_line.groups && |
|
|
f_line.groups.file.includes("<anonymous>") |
|
|
) { |
|
|
const {file, line, pos} = f_line.groups; |
|
|
let source = |
|
|
"Your code" + |
|
|
`:${match_line}\n${code.split("\n")[match_line - 1].trim()}\n `; |
|
|
let code_source = ""; |
|
|
if (line < programs_length) { |
|
|
source = |
|
|
"In your program code: " + |
|
|
programs.split("\n")[line - 1].trim() + |
|
|
"\n"; |
|
|
code_source = `at line ${match_line}:${code |
|
|
.split("\n") |
|
|
[match_line - 1].trim()} in your code`; |
|
|
} |
|
|
return source + err.message + "\n" + code_source; |
|
|
} |
|
|
return err.message; |
|
|
} |
|
|
}); |
|
|
|
|
|
app.post("/stop", (req, res) => { |
|
|
bot.end(); |
|
|
res.json({ |
|
|
message: "Bot stopped", |
|
|
}); |
|
|
}); |
|
|
|
|
|
app.post("/pause", (req, res) => { |
|
|
if (!bot) { |
|
|
res.status(400).json({error: "Bot not spawned"}); |
|
|
return; |
|
|
} |
|
|
bot.chat("/pause"); |
|
|
bot.waitForTicks(bot.waitTicks).then(() => { |
|
|
res.json({message: "Success"}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const PORT = process.argv[2]; |
|
|
const VISUAL_SERVER_PORT = process.argv[3]; |
|
|
app.listen(PORT, () => { |
|
|
console.log(`Server started on port ${PORT}`); |
|
|
}); |
|
|
|