Communicate with mineflayer & update on_event()

This commit is contained in:
yuymf 2023-09-25 20:11:00 +08:00
parent 1bb2450949
commit 7225b70e25
32 changed files with 3274 additions and 20 deletions

33
Temp.md Normal file
View file

@ -0,0 +1,33 @@
## MG-MC记录文档
### 0926 环境信息获取和更新 on_event()实际内容
1. Nodejs + Mineflayer配置
A.自行安装[Node.js (nodejs.org)](https://nodejs.org/en)
B.Mineflayer配置
```bash
cd metagpt/mineflayer_env/mineflayer
npm install -g npx
npm install
cd mineflayer-collectblock
npm install
npx tsc
cd ..
npm install
```
2.配置完游戏后,在 minecraft_run.py 下修改
```python
mc_player.set_port(2465) # Modify this to your LAN port
```
python minecraft_run.py
<img src="docs/resources/workspace/minecraft_tests/on_event.jpeg" style="zoom:67%;" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

View file

@ -4,6 +4,10 @@
# @Desc :
from typing import Iterable, Dict, Any
from pydantic import BaseModel, Field
import requests
import json
import time
import os
from metagpt.logs import logger
from metagpt.roles import Role
@ -13,9 +17,125 @@ from metagpt.software_company import SoftwareCompany
from metagpt.actions.minecraft.player_action import PlayerActions
from metagpt.roles.minecraft.minecraft_base import Minecraft
from metagpt.environment import Environment
import metagpt.utils.minecraft as U
from metagpt.utils.minecraft.process_monitor import SubprocessMonitor
class MineflayerEnv:
def __init__(
self,
mc_port=None,
server_host="http://127.0.0.1",
server_port=3000,
request_timeout=600,
):
self.mc_port = mc_port
self.server = f"{server_host}:{server_port}"
self.server_port = server_port
self.request_timeout = request_timeout
self.mineflayer = self.get_mineflayer_process(server_port)
self.has_reset = False
self.reset_options = None
self.connected = False
self.server_paused = False
class GameEnvironment(BaseModel):
def set_mc_port(self, mc_port):
self.mc_port = mc_port
def get_mineflayer_process(self, server_port):
U.f_mkdir("./logs", "mineflayer")
file_path = os.path.abspath(os.path.dirname(__file__))
return SubprocessMonitor(
commands=[
"node",
U.f_join(file_path, "mineflayer_env/mineflayer/index.js"),
str(server_port),
],
name="mineflayer",
ready_match=r"Server started on port (\d+)",
log_path=U.f_join("./logs", "mineflayer"),
)
def check_process(self):
retry = 0
while not self.mineflayer.is_running:
logger.info("Mineflayer process has exited, restarting")
self.mineflayer.run()
if not self.mineflayer.is_running:
if retry > 3:
logger.error("Mineflayer process failed to start")
raise {}
else:
retry += 1
continue
logger.info(self.mineflayer.ready_line)
res = requests.post(
f"{self.server}/start",
json=self.reset_options,
timeout=self.request_timeout,
)
if res.status_code != 200:
self.mineflayer.stop()
logger.error(
f"Minecraft server reply with code {res.status_code}"
)
raise {}
return res.json()
def reset(self, *, seed=None, options=None, ):
if options is None:
options = {}
if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
logger.error("inventory can only be set when options is hard")
raise{}
self.reset_options = {
"port": self.mc_port,
"reset": options.get("mode", "hard"),
"inventory": options.get("inventory", {}),
"equipment": options.get("equipment", []),
"spread": options.get("spread", False),
"waitTicks": options.get("wait_ticks", 5),
"position": options.get("position", None),
}
self.unpause()
self.mineflayer.stop()
time.sleep(1) # wait for mineflayer to exit
returned_data = self.check_process()
self.has_reset = True
self.connected = True
# All the reset in step will be soft
self.reset_options["reset"] = "soft"
self.pause()
return json.loads(returned_data)
def close(self):
self.unpause()
if self.connected:
res = requests.post(f"{self.server}/stop")
if res.status_code == 200:
self.connected = False
self.mineflayer.stop()
return not self.connected
def pause(self):
if self.mineflayer.is_running and not self.server_paused:
res = requests.post(f"{self.server}/pause")
if res.status_code == 200:
self.server_paused = True
return self.server_paused
def unpause(self):
if self.mineflayer.is_running and self.server_paused:
res = requests.post(f"{self.server}/pause")
if res.status_code == 200:
self.server_paused = False
else:
print(res.json())
return self.server_paused
class GameEnvironment(BaseModel, arbitrary_types_allowed=True):
"""
游戏环境的记忆用于多个agent进行信息的共享和缓存而不需要重复在自己的角色内维护缓存
"""
@ -23,6 +143,14 @@ class GameEnvironment(BaseModel):
current_task: str = Field(default="Craft 4 wooden planks")
task_execution_time: float = Field(default=float)
context: str = Field(default="")
code: str = Field(default="")
programs: str = Field(default="")
mf_instance : MineflayerEnv = Field(default_factory=MineflayerEnv)
def set_mc_port(self, mc_port):
self.mf_instance.set_mc_port(mc_port)
def register_roles(self, roles: Iterable[Minecraft]):
for role in roles:
@ -36,7 +164,13 @@ class GameEnvironment(BaseModel):
def update_context(self, context: str):
self.context = context
def update_code(self, code: str):
self.code = code
def update_program(self, programs: str):
self.programs = programs
async def on_event(self, *args):
"""
Retrieve Minecraft events.
@ -52,26 +186,36 @@ class GameEnvironment(BaseModel):
"""
try:
# Implement the logic to retrieve Minecraft events here.
events = {
"Biome": "river",
"Time": "night",
"Nearby blocks": "water, dirt, stone, coal_ore, sandstone, grass_block, sand, grass, oak_leaves, fern, seagrass, tall_seagrass",
"Nearby entities(nearest to farthest)": "turtle, salmon",
"Health": "20.0 / 20",
"Hunger": "20.0 / 20",
"Position": "x = -47.5, y = 63.0, z = -283.5",
"Equipment": [],
"Inventory(0 / 36)": "Empty",
"Chests": ""
}
# Example: events = minecraft_api.get_events()
if not self.mf_instance.has_reset:
# TODO Modify
logger.info("Environment has not been reset yet, is resetting")
self.mf_instance.reset(options={
"mode": "soft",
"wait_ticks": 20,
})
# raise {}
self.mf_instance.check_process()
self.mf_instance.unpause()
data = {
"code": self.code,
"programs": self.programs,
}
res = requests.post(
f"{self.mf_instance.server}/step", json=data, timeout=self.mf_instance.request_timeout
)
if res.status_code != 200:
logger.error("Failed to step Minecraft server")
raise {}
returned_data = res.json()
self.mf_instance.pause()
events = json.loads(returned_data)
logger.info(f"Get Current Event: {events}")
return events
except Exception as e:
logger.error(f"Failed to retrieve Minecraft events: {str(e)}")
raise {}
class MinecraftPlayer(SoftwareCompany):
"""
Software Company: Possesses a team, SOP (Standard Operating Procedures), and a platform for instant messaging,
@ -83,6 +227,9 @@ class MinecraftPlayer(SoftwareCompany):
task: str = Field(default="")
game_info: dict = Field(default={})
def set_port(self, mc_port):
self.game_memory.set_mc_port(mc_port)
def hire(self, roles: list[Role]):
self.environment.add_roles(roles)
self.game_memory.register_roles(roles)
@ -107,4 +254,15 @@ class MinecraftPlayer(SoftwareCompany):
return self.environment.history
if "__name__" == "__main__":
test_code = "bot.chat(`/time set ${getNextTime()}`);"
mc_port = 1960
# env_wait_ticks = 20
ge = GameEnvironment()
ge.set_mc_port(mc_port)
ge.update_code(test_code)
# ge.mf_instance.reset(options={
# "mode": "soft",
# "wait_ticks": env_wait_ticks,
# })
logger.info(ge.on_event())

294
metagpt/mineflayer_env/.gitignore vendored Normal file
View file

@ -0,0 +1,294 @@
# MCP-Reborn
MCP-Reborn/
run/
*.jar
config.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Logs
logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
package-lock.json

View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

View file

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View file

@ -0,0 +1,425 @@
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");
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", // minecraft server ip
port: req.body.port, // minecraft server port
username: "bot",
disableChatSigning: true,
checkTimeoutInterval: 60 * 60 * 1000,
});
bot.once("error", onConnectionFailed);
// Event subscriptions
bot.waitTicks = req.body.waitTicks;
bot.globalTickCounter = 0;
bot.stuckTickCounter = 0;
bot.stuckPosList = [];
bot.iron_pickaxe = false;
bot.on("kicked", onDisconnect);
// mounting will cause physicsTick to stop
bot.on("mount", () => {
bot.dismount();
});
bot.once("spawn", async () => {
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}`
);
}
// if iron_pickaxe is in bot's inventory
if (
bot.inventory.items().find((item) => item.name === "iron_pickaxe")
) {
bot.iron_pickaxe = true;
}
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);
// bot.collectBlock.movements.digCost = 0;
// bot.collectBlock.movements.placeCost = 0;
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) => {
// import useful package
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");
// Set up pathfinder
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);
// initialize fail count
let _craftItemFailCount = 0;
let _killMobFailCount = 0;
let _mineBlockFailCount = 0;
let _placeItemFailCount = 0;
let _smeltItemFailCount = 0;
// Retrieve array form post bod
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();
// wait for last message
await bot.waitForTicks(bot.waitTicks);
if (!response_sent) {
response_sent = true;
res.json(bot.observe());
}
bot.removeListener("physicTick", onTick);
async function evaluateCode(code, programs) {
// Echo the code produced for players to see it. Don't echo when the bot code is already producing dialog or it will double echo
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);
// Check if the list is full
if (bot.stuckPosList.length === 5) {
const oldestPos = bot.stuckPosList[0];
const posDifference = currentPos.distanceTo(oldestPos);
if (posDifference < posThreshold) {
teleportBot(); // execute the function
}
// Remove the oldest time from the list
bot.stuckPosList.shift();
}
}
function teleportBot() {
const blocks = bot.findBlocks({
matching: (block) => {
return block.type === 0;
},
maxDistance: 1,
count: 27,
});
if (blocks) {
// console.log(blocks.length);
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 chest is not in bot's inventory
if (!bot.inventory.items().find((item) => item.name === "chest")) {
bot.chat("/give @s chest");
}
}
// if iron_pickaxe not in bot's inventory and bot.iron_pickaxe
if (
bot.iron_pickaxe &&
!bot.inventory.items().find((item) => item.name === "iron_pickaxe")
) {
bot.chat("/give @s iron_pickaxe");
}
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 filename = file.match(/(?<=node_modules\\)(.*)/)[1];
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" });
});
});
// Server listening to PORT 3000
const DEFAULT_PORT = 3000;
const PORT = process.argv[2] || DEFAULT_PORT;
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});

View file

@ -0,0 +1,107 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
lib/
package-lock.json

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 TheDudeFromCI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,89 @@
<h1 align="center">mineflayer-collectblock</h1>
<p align="center"><i>A small utility plugin for allowing users to collect blocks using a higher level API.</i></p>
<p align="center">
<img src="https://github.com/TheDudeFromCI/mineflayer-collectblock/workflows/Build/badge.svg" />
<a href="https://www.npmjs.com/package/mineflayer-collectblock"><img src="https://img.shields.io/npm/v/mineflayer-collectblock" /></a>
<img src="https://img.shields.io/github/repo-size/TheDudeFromCI/mineflayer-collectblock" />
<img src="https://img.shields.io/npm/dm/mineflayer-collectblock" />
<img src="https://img.shields.io/github/contributors/TheDudeFromCI/mineflayer-collectblock" />
<img src="https://img.shields.io/github/license/TheDudeFromCI/mineflayer-collectblock" />
</p>
---
## This is a modified version to better support Voyager
## Showcase
You can see a video of the plugin in action, [here.](https://youtu.be/5T_rcCnNnf4)
The source code of the bot in the video can be seen in the examples folder, [here.](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/examples/collector.js)
### Description
This plugin is a wrapper for mineflayer that allows for easier API usage when collecting blocks or item drops. This plugin is designed to reduce some of the boilerplate code based around the act of pathfinding to a block _(handled by_ ***mineflayer-pathfinder***_)_, selecting the best tool to mine that block _(handled by_ ***mineflayer-tool***_)_, actually mining it, then moving to collect the item drops from that block. This plugin allows for all of that basic concept to be wrapped up into a single API function.
In addition to the usage above, some additional quality of life features are available in this plugin. These include the ability to automatically deposit items into a chest when the bot's inventory is full, collecting new tools from a chest if the bot doesn't currently have a required tool _(also handled by_ ***mineflayer-tool***_)_, and allowing for queueing of multiple blocks or item drops to the collection task, so they can be processed later.
### Getting Started
This plugin is built using Node and can be installed using:
```bash
npm install --save mineflayer-collectblock
```
### Simple Bot
The brief description goes here.
```js
// Create your bot
const mineflayer = require("mineflayer")
const bot = mineflayer.createBot({
host: 'localhost',
username: 'Player',
})
let mcData
// Load collect block
bot.loadPlugin(require('mineflayer-collectblock').plugin)
async function collectGrass() {
// Find a nearby grass block
const grass = bot.findBlock({
matching: mcData.blocksByName.grass_block.id,
maxDistance: 64
})
if (grass) {
// If we found one, collect it.
try {
await bot.collectBlock.collect(grass)
collectGrass() // Collect another grass block
} catch (err) {
console.log(err) // Handle errors, if any
}
}
}
// On spawn, start collecting all nearby grass
bot.once('spawn', () => {
mcData = require('minecraft-data')(bot.version)
collectGrass()
})
```
### Documentation
[API](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/docs/api.md)
[Examples](https://github.com/TheDudeFromCI/mineflayer-collectblock/tree/master/examples)
### License
This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/LICENSE) license.
### Contributions
This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated!
For larger changes, it is recommended to discuss these changes in the issues tab before writing any code. It's also preferred to make many smaller PRs than one large one, where applicable.

View file

@ -0,0 +1 @@
theme: jekyll-theme-cayman

View file

@ -0,0 +1,52 @@
# API <!-- omit in toc -->
Welcome to the *mineflayer-collectblock* API documentation page.
## Table of Contents <!-- omit in toc -->
- [1. Summary](#1-summary)
- [Properties](#properties)
- [`bot.collectblock.movements: Movements`](#botcollectblockmovements-movements)
- [Functions](#functions)
- [collect](#collect)
- [Options:](#options)
## 1. Summary
The collect block plugin is a utility plugin that can be used to help make collecting blocks and item drops very easy, using only a single API call. No need to worry about pathfinding to the block, selecting the right tool, or moving to pick up the item drop after mining.
## Properties
### `bot.collectblock.movements: Movements`
The movements object used by the pathfinder plugin to define the movement configuration. This object is passed to the pathfinder plugin when any API from this plugin is called in order to control how pathfinding should work when collecting the given blocks or item.
If set to null, the pathfinder plugin movements is not updated.
Defaults to a new movements object instance.
## Functions
### collect
Usage: `bot.collectblock.collect(target: Collectable | Collectable[], options?: CollectOptions, cb: (err?: Error) => void): void`
Causes the bot to collect the given block, item drop, or list of those. If the target is a block, the bot will move to the block, mine it, and pick up the item drop. If the target is an item drop, the bot will move to the item drop and pick it up. If the target is a list of collectables, the bot will move from target to target in order of closest to furthest and collect each target in turn.
#### Options:
* `append: boolean`
If true, the target(s) will be appended to the existing target list instead of starting a new task. Defaults to false.
* `ignoreNoPath: boolean`
If true, errors will not be thrown when a path to the target block cannot be found. The bot will attempt to choose the best available position it can find, instead. Errors are still thrown if the bot cannot interact with the block from it's final location. Defaults to false.
* `chestLocations: Vec3[]`
Gets the list of chest locations to use when storing items after the bot's inventory becomes full. If undefined, it defaults to the chest location list on the bot.collectBlock plugin.
* `itemFilter: ItemFilter`
When transferring items to a chest, this filter is used to determine what items are allowed to be moved, and what items aren't allowed to be moved. Defaults to the item filter specified on the bot.collectBlock plugin.

View file

@ -0,0 +1,70 @@
/**
* This bot example show how to direct a bot to collect a specific block type
* or a group of nearby blocks of that type.
*/
const mineflayer = require('mineflayer')
const collectBlock = require('mineflayer-collectblock').plugin
if (process.argv.length < 4 || process.argv.length > 6) {
console.log('Usage : node collector.js <host> <port> [<name>] [<password>]')
process.exit(1)
}
const bot = mineflayer.createBot({
host: process.argv[2],
port: process.argv[3],
username: process.argv[4] || 'collector',
password: process.argv[5]
})
bot.loadPlugin(collectBlock)
let mcData
bot.once('spawn', () => {
mcData = require('minecraft-data')(bot.version)
})
bot.on('chat', async (username, message) => {
const args = message.split(' ')
if (args[0] !== 'collect') return
let count = 1
if (args.length === 3) count = parseInt(args[1])
let type = args[1]
if (args.length === 3) type = args[2]
const blockType = mcData.blocksByName[type]
if (!blockType) {
return
}
const blocks = bot.findBlocks({
matching: blockType.id,
maxDistance: 64,
count: count
})
if (blocks.length === 0) {
bot.chat("I don't see that block nearby.")
return
}
const targets = []
for (let i = 0; i < Math.min(blocks.length, count); i++) {
targets.push(bot.blockAt(blocks[i]))
}
bot.chat(`Found ${targets.length} ${type}(s)`)
try {
await bot.collectBlock.collect(targets)
// All blocks have been collected.
bot.chat('Done')
} catch (err) {
// An error occurred, report it.
bot.chat(err.message)
console.log(err)
}
})

View file

@ -0,0 +1,59 @@
/**
* This bot example shows how to collect a vein of ores quickly after only finding a single block.
* This makes it easy to collect a vein of ores or mine a tree without looking for every block in the
* area.
*/
const mineflayer = require('mineflayer')
const collectBlock = require('mineflayer-collectblock').plugin
if (process.argv.length < 4 || process.argv.length > 6) {
console.log('Usage : node oreMiner.js <host> <port> [<name>] [<password>]')
process.exit(1)
}
const bot = mineflayer.createBot({
host: process.argv[2],
port: process.argv[3],
username: process.argv[4] || 'oreMiner',
password: process.argv[5]
})
bot.loadPlugin(collectBlock)
let mcData
bot.once('spawn', () => {
mcData = require('minecraft-data')(bot.version)
})
bot.on('chat', async (username, message) => {
const args = message.split(' ')
if (args[0] !== 'collect') return
const blockType = mcData.blocksByName[args[1]]
if (!blockType) {
bot.chat(`I don't know any blocks named ${args[1]}.`)
return
}
const block = bot.findBlock({
matching: blockType.id,
maxDistance: 64
})
if (!block) {
bot.chat("I don't see that block nearby.")
return
}
const targets = bot.collectBlock.findFromVein(block)
try {
await bot.collectBlock.collect(targets)
// All blocks have been collected.
bot.chat('Done')
} catch (err) {
// An error occurred, report it.
bot.chat(err.message)
console.log(err)
}
})

View file

@ -0,0 +1,107 @@
/**
* This bot example shows how to use the chest filling mechanic of the plugin.
* Simply provide a given storage chest, and the bot will automatically try and
* store it's inventory in that chest when the bot's inventory becomes full.
*/
if (process.argv.length < 4 || process.argv.length > 6) {
console.log('Usage : node storageBot.js <host> <port> [<name>] [<password>]')
process.exit(1)
}
// Load your libraries
const mineflayer = require('mineflayer')
const collectBlock = require('mineflayer-collectblock').plugin
// Create your bot
const bot = mineflayer.createBot({
host: process.argv[2],
port: parseInt(process.argv[3]),
username: process.argv[4] ? process.argv[4] : 'storageBot',
password: process.argv[5]
})
// Load the collect block plugin
bot.loadPlugin(collectBlock)
// Load mcData on login
let mcData
bot.once('login', () => {
mcData = require('minecraft-data')(bot.version)
})
// On spawn, try to find any nearby chests and save those as storage locations.
// When the bot's inventory becomes too full, it will empty it's inventory into
// these chests before collecting more resources. If a chest gets full, it moves
// to the next one in order until it's inventory is empty or it runs out of chests.
bot.once('spawn', () => {
bot.collectBlock.chestLocations = bot.findBlocks({
matching: mcData.blocksByName.chest.id,
maxDistance: 16,
count: 999999 // Get as many chests as we can
})
if (bot.collectBlock.chestLocations.length === 0) {
bot.chat("I don't see any chests nearby.")
} else {
for (const chestPos of bot.collectBlock.chestLocations) {
bot.chat(`I found a chest at ${chestPos}`)
}
}
})
// Wait for someone to say something
bot.on('chat', async (username, message) => {
// If the player says something start starts with "collect"
// Otherwise, do nothing
const args = message.split(' ')
if (args[0] !== 'collect') return
// If the player specifies a number, collect that many. Otherwise, default to 1.
let count = 1
if (args.length === 3) count = parseInt(args[1])
// If a number was given the item number is the 3rd arg, not the 2nd.
let type = args[1]
if (args.length === 3) type = args[2]
// Get the id of that block type for this version of Minecraft.
const blockType = mcData.blocksByName[type]
if (!blockType) {
bot.chat(`I don't know any blocks named ${type}.`)
return
}
// Find all nearby blocks of that type, up to the given count, within 64 blocks.
const blocks = bot.findBlocks({
matching: blockType.id,
maxDistance: 64,
count: count
})
// Complain if we can't find any nearby blocks of that type.
if (blocks.length === 0) {
bot.chat("I don't see that block nearby.")
return
}
// Convert the block position array into a block array to pass to collect block.
const targets = []
for (let i = 0; i < Math.min(blocks.length, count); i++) {
targets.push(bot.blockAt(blocks[i]))
}
// Announce what we found.
bot.chat(`Found ${targets.length} ${type}(s)`)
// Tell the bot to collect all of the given blocks in the block list.
try {
await bot.collectBlock.collect(targets)
// All blocks have been collected.
bot.chat('Done')
} catch (err) {
// An error occurred, report it.
bot.chat(err.message)
console.log(err)
}
})

View file

@ -0,0 +1,44 @@
{
"name": "mineflayer-collectblock",
"version": "1.4.1",
"description": "A simple utility plugin for Mineflayer that add a higher level API for collecting blocks.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "ts-standard && tsc && require-self",
"clean": "rm -rf lib",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/TheDudeFromCI/mineflayer-collectblock.git"
},
"keywords": [
"mineflayer",
"plugin",
"api",
"utility",
"helper",
"collect"
],
"author": "TheDudeFromCI",
"license": "MIT",
"bugs": {
"url": "https://github.com/TheDudeFromCI/mineflayer-collectblock/issues"
},
"homepage": "https://github.com/TheDudeFromCI/mineflayer-collectblock#readme",
"dependencies": {
"mineflayer": "^4.0.0",
"mineflayer-pathfinder": "^2.1.1",
"mineflayer-tool": "^1.1.0"
},
"devDependencies": {
"@types/node": "^18.6.4",
"require-self": "^0.2.3",
"ts-standard": "^11.0.0",
"typescript": "^4.1.3"
},
"files": [
"lib/**/*"
]
}

View file

@ -0,0 +1,35 @@
import { Bot } from 'mineflayer'
import { Block } from 'prismarine-block'
export function findFromVein (bot: Bot, block: Block, maxBlocks: number, maxDistance: number, floodRadius: number): Block[] {
const targets: Block[] = []
const open: Block[] = [block]
const type = block.type
const center = block.position
for (let i = 0; i < maxBlocks; i++) {
const next = open.pop()
if (next == null) break
targets.push(next)
for (let x = -floodRadius; x <= floodRadius; x++) {
for (let y = -floodRadius; y <= floodRadius; y++) {
for (let z = -floodRadius; z <= floodRadius; z++) {
const neighborPos = next.position.offset(x, y, z)
if (neighborPos.manhattanDistanceTo(center) > maxDistance) continue
const neighbor = bot.blockAt(neighborPos)
if (neighbor == null || neighbor.type !== type) continue
if (targets.includes(neighbor)) continue
if (open.includes(neighbor)) continue
open.push(neighbor)
}
}
}
}
return targets
}

View file

@ -0,0 +1,451 @@
import { Bot } from "mineflayer";
import { Block } from "prismarine-block";
import { Movements, goals } from "mineflayer-pathfinder";
import { TemporarySubscriber } from "./TemporarySubscriber";
import { Entity } from "prismarine-entity";
import { error } from "./Util";
import { Vec3 } from "vec3";
import { emptyInventoryIfFull, ItemFilter } from "./Inventory";
import { findFromVein } from "./BlockVeins";
import { Collectable, Targets } from "./Targets";
import { Item } from "prismarine-item";
import mcDataLoader from "minecraft-data";
import { once } from "events";
import { callbackify } from "util";
export type Callback = (err?: Error) => void;
async function collectAll(
bot: Bot,
options: CollectOptionsFull
): Promise<void> {
let success_count = 0;
while (!options.targets.empty) {
await emptyInventoryIfFull(
bot,
options.chestLocations,
options.itemFilter
);
const closest = options.targets.getClosest();
if (closest == null) break;
switch (closest.constructor.name) {
case "Block": {
try {
if (success_count >= options.count) {
break;
}
await bot.tool.equipForBlock(
closest as Block,
equipToolOptions
);
const goal = new goals.GoalLookAtBlock(
closest.position,
bot.world
);
await bot.pathfinder.goto(goal);
await mineBlock(bot, closest as Block, options);
success_count++;
// TODO: options.ignoreNoPath
} catch (err) {
// @ts-ignore
// console.log(err.stack)
// bot.pathfinder.stop()
// bot.waitForTicks(10)
try {
bot.pathfinder.setGoal(null);
} catch (err) {}
if (options.ignoreNoPath) {
// @ts-ignore
if (err.name === "Invalid block") {
console.log(
`Block ${closest.name} at ${closest.position} is not valid! Skip it!`
);
} // @ts-ignore
else if (err.name === "Unsafe block") {
console.log(
`${closest.name} at ${closest.position} is not safe to break! Skip it!`
);
// @ts-ignore
} else if (err.name === "NoItem") {
const properties =
bot.registry.blocksByName[closest.name];
const leastTool = Object.keys(
properties.harvestTools
)[0];
const item = bot.registry.items[leastTool];
bot.chat(
`I need at least a ${item.name} to mine ${closest.name}! Skip it!`
);
return;
} else if (
// @ts-ignore
err.name === "NoPath" ||
// @ts-ignore
err.name === "Timeout"
) {
if (
bot.entity.position.distanceTo(
closest.position
) < 0.5
) {
await mineBlock(bot, closest as Block, options);
break;
}
console.log(
`No path to ${closest.name} at ${closest.position}! Skip it!`
);
// @ts-ignore
} else if (err.message === "Digging aborted") {
console.log(`Digging aborted! Skip it!`);
} else {
// @ts-ignore
bot.chat(`Error: ${err.message}`);
}
break;
}
throw err;
}
break;
}
case "Entity": {
// Don't collect any entities that are marked as 'invalid'
if (!(closest as Entity).isValid) break;
try {
const tempEvents = new TemporarySubscriber(bot);
const waitForPickup = new Promise<void>(
(resolve, reject) => {
const timeout = setTimeout(() => {
// After 10 seconds, reject the promise
clearTimeout(timeout);
tempEvents.cleanup();
reject(new Error("Failed to pickup item"));
}, 10000);
tempEvents.subscribeTo(
"entityGone",
(entity: Entity) => {
if (entity === closest) {
clearTimeout(timeout);
tempEvents.cleanup();
resolve();
}
}
);
}
);
bot.pathfinder.setGoal(
new goals.GoalFollow(closest as Entity, 0)
);
// await bot.pathfinder.goto(new goals.GoalBlock(closest.position.x, closest.position.y, closest.position.z))
await waitForPickup;
} catch (err) {
// @ts-ignore
console.log(err.stack);
try {
bot.pathfinder.setGoal(null);
} catch (err) {}
if (options.ignoreNoPath) {
// @ts-ignore
if (err.message === "Failed to pickup item") {
bot.chat(`Failed to pickup item! Skip it!`);
}
break;
}
throw err;
}
break;
}
default: {
throw error(
"UnknownType",
`Target ${closest.constructor.name} is not a Block or Entity!`
);
}
}
options.targets.removeTarget(closest);
}
bot.chat(`Collect finish!`);
}
const equipToolOptions = {
requireHarvest: true,
getFromChest: false,
maxTools: 2,
};
async function mineBlock(
bot: Bot,
block: Block,
options: CollectOptionsFull
): Promise<void> {
if (
bot.blockAt(block.position)?.type !== block.type ||
bot.blockAt(block.position)?.type === 0
) {
options.targets.removeTarget(block);
throw error("Invalid block", "Block is not valid!");
// @ts-expect-error
} else if (!bot.pathfinder.movements.safeToBreak(block)) {
options.targets.removeTarget(block);
throw error("Unsafe block", "Block is not safe to break!");
}
await bot.tool.equipForBlock(block, equipToolOptions);
if (!block.canHarvest(bot.heldItem ? bot.heldItem.type : bot.heldItem)) {
options.targets.removeTarget(block);
throw error("NoItem", "Bot does not have a harvestable tool!");
}
const tempEvents = new TemporarySubscriber(bot);
tempEvents.subscribeTo("itemDrop", (entity: Entity) => {
if (
entity.position.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <=
0.5
) {
options.targets.appendTarget(entity);
}
});
try {
await bot.dig(block);
// Waiting for items to drop
await new Promise<void>((resolve) => {
let remainingTicks = 10;
tempEvents.subscribeTo("physicTick", () => {
remainingTicks--;
if (remainingTicks <= 0) {
tempEvents.cleanup();
resolve();
}
});
});
} finally {
tempEvents.cleanup();
}
}
/**
* A set of options to apply when collecting the given targets.
*/
export interface CollectOptions {
/**
* If true, the target(s) will be appended to the existing target list instead of
* starting a new task. Defaults to false.
*/
append?: boolean;
/**
* If true, errors will not be thrown when a path to the target block cannot
* be found. The bot will attempt to choose the best available position it
* can find, instead. Errors are still thrown if the bot cannot interact with
* the block from it's final location. Defaults to false.
*/
ignoreNoPath?: boolean;
/**
* Gets the list of chest locations to use when storing items after the bot's
* inventory becomes full. If undefined, it defaults to the chest location
* list on the bot.collectBlock plugin.
*/
chestLocations?: Vec3[];
/**
* When transferring items to a chest, this filter is used to determine what
* items are allowed to be moved, and what items aren't allowed to be moved.
* Defaults to the item filter specified on the bot.collectBlock plugin.
*/
itemFilter?: ItemFilter;
/**
* The total number of items to collect
*/
count?: number;
}
/**
* A version of collect options where all values are assigned.
*/
interface CollectOptionsFull {
append: boolean;
ignoreNoPath: boolean;
chestLocations: Vec3[];
itemFilter: ItemFilter;
targets: Targets;
count: number;
}
/**
* The collect block plugin.
*/
export class CollectBlock {
/**
* The bot.
*/
private readonly bot: Bot;
/**
* The list of active targets being collected.
*/
private readonly targets: Targets;
/**
* The movements configuration to be sent to the pathfinder plugin.
*/
movements?: Movements;
/**
* A list of chest locations which the bot is allowed to empty their inventory into
* if it becomes full while the bot is collecting resources.
*/
chestLocations: Vec3[] = [];
/**
* When collecting items, this filter is used to determine what items should be placed
* into a chest if the bot's inventory becomes full. By default, returns true for all
* items except for tools, weapons, and armor.
*
* @param item - The item stack in the bot's inventory to check.
*
* @returns True if the item should be moved into the chest. False otherwise.
*/
itemFilter: ItemFilter = (item: Item) => {
if (item.name.includes("helmet")) return false;
if (item.name.includes("chestplate")) return false;
if (item.name.includes("leggings")) return false;
if (item.name.includes("boots")) return false;
if (item.name.includes("shield")) return false;
if (item.name.includes("sword")) return false;
if (item.name.includes("pickaxe")) return false;
if (item.name.includes("axe")) return false;
if (item.name.includes("shovel")) return false;
if (item.name.includes("hoe")) return false;
return true;
};
/**
* Creates a new instance of the create block plugin.
*
* @param bot - The bot this plugin is acting on.
*/
constructor(bot: Bot) {
this.bot = bot;
this.targets = new Targets(bot);
// @ts-ignore
this.movements = new Movements(bot, mcDataLoader(bot.version));
}
/**
* If target is a block:
* Causes the bot to break and collect the target block.
*
* If target is an item drop:
* Causes the bot to collect the item drop.
*
* If target is an array containing items or blocks, preforms the correct action for
* all targets in that array sorting dynamically by distance.
*
* @param target - The block(s) or item(s) to collect.
* @param options - The set of options to use when handling these targets
* @param cb - The callback that is called finished.
*/
async collect(
target: Collectable | Collectable[],
options: CollectOptions | Callback = {},
cb?: Callback
): Promise<void> {
if (typeof options === "function") {
cb = options;
options = {};
}
// @ts-expect-error
if (cb != null) return callbackify(this.collect)(target, options, cb);
const optionsFull: CollectOptionsFull = {
append: options.append ?? false,
ignoreNoPath: options.ignoreNoPath ?? false,
chestLocations: options.chestLocations ?? this.chestLocations,
itemFilter: options.itemFilter ?? this.itemFilter,
targets: this.targets,
count: options.count ?? Infinity,
};
if (this.bot.pathfinder == null) {
throw error(
"UnresolvedDependency",
"The mineflayer-collectblock plugin relies on the mineflayer-pathfinder plugin to run!"
);
}
if (this.bot.tool == null) {
throw error(
"UnresolvedDependency",
"The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!"
);
}
if (this.movements != null) {
this.bot.pathfinder.setMovements(this.movements);
}
if (!optionsFull.append) await this.cancelTask();
if (Array.isArray(target)) {
this.targets.appendTargets(target);
} else {
this.targets.appendTarget(target);
}
try {
await collectAll(this.bot, optionsFull);
this.targets.clear();
} catch (err) {
this.targets.clear();
// Ignore path stopped error for cancelTask to work properly (imo we shouldn't throw any pathing errors)
// @ts-expect-error
if (err.name !== "PathStopped") throw err;
} finally {
// @ts-expect-error
this.bot.emit("collectBlock_finished");
}
}
/**
* Loads all touching blocks of the same type to the given block and returns them as an array.
* This effectively acts as a flood fill algorithm to retrieve blocks in the same ore vein and similar.
*
* @param block - The starting block.
* @param maxBlocks - The maximum number of blocks to look for before stopping.
* @param maxDistance - The max distance from the starting block to look.
* @param floodRadius - The max distance distance from block A to block B to be considered "touching"
*/
findFromVein(
block: Block,
maxBlocks = 100,
maxDistance = 16,
floodRadius = 1
): Block[] {
return findFromVein(
this.bot,
block,
maxBlocks,
maxDistance,
floodRadius
);
}
/**
* Cancels the current collection task, if still active.
*
* @param cb - The callback to use when the task is stopped.
*/
async cancelTask(cb?: Callback): Promise<void> {
if (this.targets.empty) {
if (cb != null) cb();
return await Promise.resolve();
}
this.bot.pathfinder.stop();
if (cb != null) {
// @ts-expect-error
this.bot.once("collectBlock_finished", cb);
}
await once(this.bot, "collectBlock_finished");
}
}

View file

@ -0,0 +1,87 @@
import { Bot } from 'mineflayer'
import { Callback } from './CollectBlock'
import { Vec3 } from 'vec3'
import { error } from './Util'
import { Item } from 'prismarine-item'
import { goals } from 'mineflayer-pathfinder'
import { callbackify } from 'util'
export type ItemFilter = (item: Item) => boolean
function getClosestChest (bot: Bot, chestLocations: Vec3[]): Vec3 | null {
let chest = null
let distance = 0
for (const c of chestLocations) {
const dist = c.distanceTo(bot.entity.position)
if (chest == null || dist < distance) {
chest = c
distance = dist
}
}
if (chest != null) {
chestLocations.splice(chestLocations.indexOf(chest), 1)
}
return chest
}
export async function emptyInventoryIfFull (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise<void> {
// @ts-expect-error
if (cb != null) return callbackify(emptyInventoryIfFull)(bot, chestLocations, cb)
if (bot.inventory.emptySlotCount() > 0) return
return await emptyInventory(bot, chestLocations, itemFilter)
}
export async function emptyInventory (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise<void> {
// @ts-expect-error
if (cb != null) return callbackify(emptyInventory)(bot, chestLocations, cb)
if (chestLocations.length === 0) {
throw error('NoChests', 'There are no defined chest locations!')
}
// Shallow clone so we can safely remove chests from the list that are full.
chestLocations = [...chestLocations]
while (true) {
const chest = getClosestChest(bot, chestLocations)
if (chest == null) {
throw error('NoChests', 'All chests are full.')
}
const hasRemaining = await tryEmptyInventory(bot, chest, itemFilter)
if (!hasRemaining) return
}
}
async function tryEmptyInventory (bot: Bot, chestLocation: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise<boolean> {
// @ts-expect-error
if (cb != null) return callbackify(tryEmptyInventory)(bot, chestLocation, itemFilter, cb)
await gotoChest(bot, chestLocation)
return await placeItems(bot, chestLocation, itemFilter)
}
async function gotoChest (bot: Bot, location: Vec3, cb?: Callback): Promise<void> {
// @ts-expect-error
if (cb != null) return callbackify(gotoChest)(bot, location)
await bot.pathfinder.goto(new goals.GoalGetToBlock(location.x, location.y, location.z))
}
async function placeItems (bot: Bot, chestPos: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise<boolean> {
// @ts-expect-error
if (cb != null) return callbackify(placeItems)(bot, chestPos, itemFilter, cb)
const chestBlock = bot.blockAt(chestPos)
if (chestBlock == null) {
throw error('UnloadedChunk', 'Chest is in an unloaded chunk!')
}
const chest = await bot.openChest(chestBlock)
for (const item of bot.inventory.items()) {
if (!itemFilter(item)) continue
if (chest.firstEmptyContainerSlot() === null) {
// We have items that didn't fit.
return true
}
await chest.deposit(item.type, item.metadata, item.count)
}
return false
}

View file

@ -0,0 +1,60 @@
import { Bot } from 'mineflayer'
import { Block } from 'prismarine-block'
import { Entity } from 'prismarine-entity'
export type Collectable = Block | Entity
export class Targets {
private readonly bot: Bot
private targets: Collectable[] = []
constructor (bot: Bot) {
this.bot = bot
}
appendTargets (targets: Collectable[]): void {
for (const target of targets) {
this.appendTarget(target)
}
}
appendTarget (target: Collectable): void {
if (this.targets.includes(target)) return
this.targets.push(target)
}
/**
* Gets the closest target to the bot in this list.
*
* @returns The closest target, or null if there are no targets.
*/
getClosest (): Collectable | null {
let closest: Collectable | null = null
let distance: number = 0
for (const target of this.targets) {
const dist = target.position.distanceTo(this.bot.entity.position)
if (closest == null || dist < distance) {
closest = target
distance = dist
}
}
return closest
}
get empty (): boolean {
return this.targets.length === 0
}
clear (): void {
this.targets.length = 0
}
removeTarget (target: Collectable): void {
const index = this.targets.indexOf(target)
if (index < 0) return
this.targets.splice(index, 1)
}
}

View file

@ -0,0 +1,77 @@
import type { Callback } from './index'
export type Task = (cb: Callback) => void
export type SyncTask = () => void
/**
* A simple utility class for queuing up a series of async tasks to execute.
*/
export class TaskQueue {
private tasks: Task[] = []
/**
* If true, the task list will stop executing if one of the tasks throws an error.
*/
readonly stopOnError: boolean = true
/**
* Adds a new async task to this queue. The provided callback should be executed when
* the async task is complete.
*
* @param task - The async task to add.
*/
add (task: Task): void {
this.tasks.push(task)
}
/**
* Adds a synchronous task toi this queue.
*
* @param task - The sync task to add.
*/
addSync (task: SyncTask): void {
this.add((cb) => {
try {
task()
cb()
} catch (err: any) {
cb(err)
}
})
}
/**
* Runs all tasks currently in this queue and empties the queue.
*
* @param cb - The optional callback to be executed when all tasks in this queue have
* finished executing.
*/
runAll (cb?: Callback): void {
const taskList = this.tasks
this.tasks = []
let index = -1
const runNext: () => void = () => {
index++
if (index >= taskList.length) {
if (cb !== undefined) cb()
return
}
try {
taskList[index]((err) => {
if (err !== undefined) {
if (cb !== undefined) cb(err)
if (this.stopOnError) return
}
runNext()
})
} catch (err: any) {
if (cb !== undefined) cb(err)
}
}
runNext()
}
}

View file

@ -0,0 +1,34 @@
import { Bot } from 'mineflayer'
class Subscription {
constructor (readonly eventName: string, readonly callback: Function) {}
}
export class TemporarySubscriber {
private readonly subscriptions: Subscription[] = []
constructor (readonly bot: Bot) {}
/**
* Adds a new temporary event listener to the bot.
*
* @param event - The event to subscribe to.
* @param callback - The function to execute.
*/
subscribeTo (event: string, callback: Function): void {
this.subscriptions.push(new Subscription(event, callback))
// @ts-expect-error
this.bot.on(event, callback)
}
/**
* Removes all attached event listeners from the bot.
*/
cleanup (): void {
for (const sub of this.subscriptions) {
// @ts-expect-error
this.bot.removeListener(sub.eventName, sub.callback)
}
}
}

View file

@ -0,0 +1,13 @@
/**
* Creates a new error object with the given type and message.
*
* @param type - The error type.
* @param message - The error message.
*
* @returns The error object.
*/
export function error (type: string, message: string): Error {
const e = new Error(message)
e.name = type
return e
}

View file

@ -0,0 +1,25 @@
import { Bot } from 'mineflayer'
import { CollectBlock } from './CollectBlock'
import { pathfinder as pathfinderPlugin } from 'mineflayer-pathfinder'
import { plugin as toolPlugin } from 'mineflayer-tool'
export function plugin (bot: Bot): void {
// @ts-expect-error
bot.collectBlock = new CollectBlock(bot)
// Load plugins if not loaded manually.
setTimeout(() => loadPathfinderPlugin(bot), 0)
setTimeout(() => loadToolPlugin(bot), 0)
}
function loadPathfinderPlugin (bot: Bot): void {
if (bot.pathfinder != null) return
bot.loadPlugin(pathfinderPlugin)
}
function loadToolPlugin (bot: Bot): void {
if (bot.tool != null) return
bot.loadPlugin(toolPlugin)
}
export { CollectBlock, Callback, CollectOptions } from './CollectBlock'

View file

@ -0,0 +1,69 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
"checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib",
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
],
"exclude": [
"node_modules",
"**/__tests__/*"
]
}

View file

@ -0,0 +1,38 @@
{
"name": "voyager",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"express": "^4.18.2",
"magic-string": "^0.30.0",
"minecraft-data": "^3.31.0",
"minecrafthawkeye": "^1.3.6",
"mineflayer": "^4.8.1",
"mineflayer-collectblock": "file:mineflayer-collectblock",
"mineflayer-pathfinder": "^2.4.2",
"mineflayer-pvp": "^1.3.2",
"mineflayer-tool": "^1.2.0",
"mocha": "^10.2.0",
"prismarine-biome": "^1.3.0",
"prismarine-block": "=1.16.3",
"prismarine-entity": "^2.2.0",
"prismarine-item": "^1.12.1",
"prismarine-nbt": "^2.2.1",
"prismarine-recipe": "^1.3.1",
"prismarine-viewer": "^1.24.0",
"typescript": "^4.9.5",
"vec3": "^0.1.8",
"graceful-fs": "^4.2.11"
},
"devDependencies": {
"prettier": "2.8.5"
}
}

View file

@ -2,3 +2,6 @@
# @Date : 2023/9/24 0:32
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
from .load_prompts import load_prompt
from .json_utils import *
from .file_utils import *

View file

@ -0,0 +1,569 @@
# -*- coding: utf-8 -*-
# @Date : 2023/09/25 16:13
# @Author : yuymf
# @Desc : Temp Using :File system utils.@ https://github.com/MineDojo/Voyager/blob/main/voyager/utils/file_utils.py
import collections
import os
import pickle
import sys
import errno
import shutil
import glob
# import pwd
import codecs
import hashlib
import tarfile
import fnmatch
import tempfile
from datetime import datetime
from socket import gethostname
import logging
f_ext = os.path.splitext
f_size = os.path.getsize
is_file = os.path.isfile
is_dir = os.path.isdir
get_dir = os.path.dirname
def host_name():
"Get host name, alias with ``socket.gethostname()``"
return gethostname()
def host_id():
"""
Returns: first part of hostname up to '.'
"""
return host_name().split(".")[0]
def utf_open(fname, mode):
"""
Wrapper for codecs.open
"""
return codecs.open(fname, mode=mode, encoding="utf-8")
def is_sequence(obj):
"""
Returns:
True if the sequence is a collections.Sequence and not a string.
"""
return isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str)
def pack_varargs(args):
"""
Pack *args or a single list arg as list
def f(*args):
arg_list = pack_varargs(args)
# arg_list is now packed as a list
"""
assert isinstance(args, tuple), "please input the tuple `args` as in *args"
if len(args) == 1 and is_sequence(args[0]):
return args[0]
else:
return args
def f_not_empty(*fpaths):
"""
Returns:
True if and only if the file exists and file size > 0
if fpath is a dir, if and only if dir exists and has at least 1 file
"""
fpath = f_join(*fpaths)
if not os.path.exists(fpath):
return False
if os.path.isdir(fpath):
return len(os.listdir(fpath)) > 0
else:
return os.path.getsize(fpath) > 0
def f_expand(fpath):
return os.path.expandvars(os.path.expanduser(fpath))
def f_exists(*fpaths):
return os.path.exists(f_join(*fpaths))
def f_join(*fpaths):
"""
join file paths and expand special symbols like `~` for home dir
"""
fpaths = pack_varargs(fpaths)
fpath = f_expand(os.path.join(*fpaths))
if isinstance(fpath, str):
fpath = fpath.strip()
return fpath
def f_listdir(
*fpaths,
filter_ext=None,
filter=None,
sort=True,
full_path=False,
nonexist_ok=True,
recursive=False,
):
"""
Args:
full_path: True to return full paths to the dir contents
filter: function that takes in file name and returns True to include
nonexist_ok: True to return [] if the dir is non-existent, False to raise
sort: sort the file names by alphabetical
recursive: True to use os.walk to recursively list files. Note that `filter`
will be applied to the relative path string to the root dir.
e.g. filter will take "a/data1.txt" and "a/b/data3.txt" as input, instead of
just the base file names "data1.txt" and "data3.txt".
if False, will simply call os.listdir()
"""
assert not (filter_ext and filter), "filter_ext and filter are mutually exclusive"
dir_path = f_join(*fpaths)
if not os.path.exists(dir_path) and nonexist_ok:
return []
if recursive:
files = [
os.path.join(os.path.relpath(root, dir_path), file)
for root, _, files in os.walk(dir_path)
for file in files
]
else:
files = os.listdir(dir_path)
if filter is not None:
files = [f for f in files if filter(f)]
elif filter_ext is not None:
files = [f for f in files if f.endswith(filter_ext)]
if sort:
files.sort()
if full_path:
return [os.path.join(dir_path, f) for f in files]
else:
return files
def f_mkdir(*fpaths):
"""
Recursively creates all the subdirs
If exist, do nothing.
"""
fpath = f_join(*fpaths)
os.makedirs(fpath, exist_ok=True)
return fpath
def f_mkdir_in_path(*fpaths):
"""
fpath is a file,
recursively creates all the parent dirs that lead to the file
If exist, do nothing.
"""
os.makedirs(get_dir(f_join(*fpaths)), exist_ok=True)
def last_part_in_path(fpath):
"""
https://stackoverflow.com/questions/3925096/how-to-get-only-the-last-part-of-a-path-in-python
"""
return os.path.basename(os.path.normpath(f_expand(fpath)))
def is_abs_path(*fpath):
return os.path.isabs(f_join(*fpath))
def is_relative_path(*fpath):
return not is_abs_path(f_join(*fpath))
def f_time(*fpath):
"File modification time"
return str(os.path.getctime(f_join(*fpath)))
def f_append_before_ext(fpath, suffix):
"""
Append a suffix to file name and retain its extension
"""
name, ext = f_ext(fpath)
return name + suffix + ext
def f_add_ext(fpath, ext):
"""
Append an extension if not already there
Args:
ext: will add a preceding `.` if doesn't exist
"""
if not ext.startswith("."):
ext = "." + ext
if fpath.endswith(ext):
return fpath
else:
return fpath + ext
def f_has_ext(fpath, ext):
"Test if file path is a text file"
_, actual_ext = f_ext(fpath)
return actual_ext == "." + ext.lstrip(".")
def f_glob(*fpath):
return glob.glob(f_join(*fpath), recursive=True)
def f_remove(*fpath, verbose=False, dry_run=False):
"""
If exist, remove. Supports both dir and file. Supports glob wildcard.
"""
assert isinstance(verbose, bool)
fpath = f_join(fpath)
if dry_run:
print("Dry run, delete:", fpath)
return
for f in glob.glob(fpath):
try:
shutil.rmtree(f)
except OSError as e:
if e.errno == errno.ENOTDIR:
try:
os.remove(f)
except: # final resort safeguard
pass
if verbose:
print(f'Deleted "{fpath}"')
def f_copy(fsrc, fdst, ignore=None, include=None, exists_ok=True, verbose=False):
"""
Supports both dir and file. Supports glob wildcard.
"""
fsrc, fdst = f_expand(fsrc), f_expand(fdst)
for f in glob.glob(fsrc):
try:
f_copytree(f, fdst, ignore=ignore, include=include, exist_ok=exists_ok)
except OSError as e:
if e.errno == errno.ENOTDIR:
shutil.copy(f, fdst)
else:
raise
if verbose:
print(f'Copied "{fsrc}" to "{fdst}"')
def _f_copytree(
src,
dst,
symlinks=False,
ignore=None,
exist_ok=True,
copy_function=shutil.copy2,
ignore_dangling_symlinks=False,
):
"""Copied from python standard lib shutil.copytree
except that we allow exist_ok
Use f_copytree as entry
"""
names = os.listdir(src)
if ignore is not None:
ignored_names = ignore(src, names)
else:
ignored_names = set()
os.makedirs(dst, exist_ok=exist_ok)
errors = []
for name in names:
if name in ignored_names:
continue
srcname = os.path.join(src, name)
dstname = os.path.join(dst, name)
try:
if os.path.islink(srcname):
linkto = os.readlink(srcname)
if symlinks:
# We can't just leave it to `copy_function` because legacy
# code with a custom `copy_function` may rely on copytree
# doing the right thing.
os.symlink(linkto, dstname)
shutil.copystat(srcname, dstname, follow_symlinks=not symlinks)
else:
# ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks:
continue
# otherwise let the copy occurs. copy2 will raise an error
if os.path.isdir(srcname):
_f_copytree(
srcname, dstname, symlinks, ignore, exist_ok, copy_function
)
else:
copy_function(srcname, dstname)
elif os.path.isdir(srcname):
_f_copytree(srcname, dstname, symlinks, ignore, exist_ok, copy_function)
else:
# Will raise a SpecialFileError for unsupported file types
copy_function(srcname, dstname)
# catch the Error from the recursive copytree so that we can
# continue with other files
except shutil.Error as err:
errors.extend(err.args[0])
except OSError as why:
errors.append((srcname, dstname, str(why)))
try:
shutil.copystat(src, dst)
except OSError as why:
# Copying file access times may fail on Windows
if getattr(why, "winerror", None) is None:
errors.append((src, dst, str(why)))
if errors:
raise shutil.Error(errors)
return dst
def _include_patterns(*patterns):
"""Factory function that can be used with copytree() ignore parameter.
Arguments define a sequence of glob-style patterns
that are used to specify what files to NOT ignore.
Creates and returns a function that determines this for each directory
in the file hierarchy rooted at the source directory when used with
shutil.copytree().
"""
def _ignore_patterns(path, names):
keep = set(
name for pattern in patterns for name in fnmatch.filter(names, pattern)
)
ignore = set(
name
for name in names
if name not in keep and not os.path.isdir(os.path.join(path, name))
)
return ignore
return _ignore_patterns
def f_copytree(fsrc, fdst, symlinks=False, ignore=None, include=None, exist_ok=True):
fsrc, fdst = f_expand(fsrc), f_expand(fdst)
assert (ignore is None) or (
include is None
), "ignore= and include= are mutually exclusive"
if ignore:
ignore = shutil.ignore_patterns(*ignore)
elif include:
ignore = _include_patterns(*include)
_f_copytree(fsrc, fdst, ignore=ignore, symlinks=symlinks, exist_ok=exist_ok)
def f_move(fsrc, fdst):
fsrc, fdst = f_expand(fsrc), f_expand(fdst)
for f in glob.glob(fsrc):
shutil.move(f, fdst)
def f_split_path(fpath, normpath=True):
"""
Splits path into a list of its component folders
Args:
normpath: call os.path.normpath to remove redundant '/' and
up-level references like ".."
"""
if normpath:
fpath = os.path.normpath(fpath)
allparts = []
while 1:
parts = os.path.split(fpath)
if parts[0] == fpath: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == fpath: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
fpath = parts[0]
allparts.insert(0, parts[1])
return allparts
def get_script_dir():
"""
Returns: the dir of current script
"""
return os.path.dirname(os.path.realpath(sys.argv[0]))
def get_script_file_name():
"""
Returns: the dir of current script
"""
return os.path.basename(sys.argv[0])
def get_script_self_path():
"""
Returns: the dir of current script
"""
return os.path.realpath(sys.argv[0])
def get_parent_dir(location, abspath=False):
"""
Args:
location: current directory or file
Returns:
parent directory absolute or relative path
"""
_path = os.path.abspath if abspath else os.path.relpath
return _path(f_join(location, os.pardir))
def md5_checksum(*fpath):
"""
File md5 signature
"""
hash_md5 = hashlib.md5()
with open(f_join(*fpath), "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def create_tar(fsrc, output_tarball, include=None, ignore=None, compress_mode="gz"):
"""
Args:
fsrc: source file or folder
output_tarball: output tar file name
compress_mode: "gz", "bz2", "xz" or "" (empty for uncompressed write)
include: include pattern, will trigger copy to temp directory
ignore: ignore pattern, will trigger copy to temp directory
"""
fsrc, output_tarball = f_expand(fsrc), f_expand(output_tarball)
assert compress_mode in ["gz", "bz2", "xz", ""]
src_base = os.path.basename(fsrc)
tempdir = None
if include or ignore:
tempdir = tempfile.mkdtemp()
tempdest = f_join(tempdir, src_base)
f_copy(fsrc, tempdest, include=include, ignore=ignore)
fsrc = tempdest
with tarfile.open(output_tarball, "w:" + compress_mode) as tar:
tar.add(fsrc, arcname=src_base)
if tempdir:
f_remove(tempdir)
def extract_tar(source_tarball, output_dir=".", members=None):
"""
Args:
source_tarball: extract members from archive
output_dir: default to current working dir
members: must be a subset of the list returned by getmembers()
"""
source_tarball, output_dir = f_expand(source_tarball), f_expand(output_dir)
with tarfile.open(source_tarball, "r:*") as tar:
tar.extractall(output_dir, members=members)
def move_with_backup(*fpath, suffix=".bak"):
"""
Ensures that a path is not occupied. If there is a file, rename it by
adding @suffix. Resursively backs up everything.
Args:
fpath: file path to clear
suffix: Add to backed up files (default: {'.bak'})
"""
fpath = str(f_join(*fpath))
if os.path.exists(fpath):
move_with_backup(fpath + suffix)
shutil.move(fpath, fpath + suffix)
def insert_before_ext(name, insert):
"""
log.txt -> log.ep50.txt
"""
name, ext = os.path.splitext(name)
return name + insert + ext
def timestamp_file_name(fname):
timestr = datetime.now().strftime("_%H-%M-%S_%m-%d-%y")
return insert_before_ext(fname, timestr)
def get_file_lock(*fpath, timeout: int = 15, logging_level="critical"):
"""
NFS-safe filesystem-backed lock. `pip install flufl.lock`
https://flufllock.readthedocs.io/en/stable/apiref.html
Args:
fpath: should be a path on NFS so that every process can see it
timeout: seconds
"""
from flufl.lock import Lock
logging.getLogger("flufl.lock").setLevel(logging_level.upper())
return Lock(f_join(*fpath), lifetime=timeout)
def load_pickle(*fpaths):
with open(f_join(*fpaths), "rb") as fp:
return pickle.load(fp)
def dump_pickle(data, *fpaths):
with open(f_join(*fpaths), "wb") as fp:
pickle.dump(data, fp)
def load_text(*fpaths, by_lines=False):
with open(f_join(*fpaths), "r") as fp:
if by_lines:
return fp.readlines()
else:
return fp.read()
def load_text_lines(*fpaths):
return load_text(*fpaths, by_lines=True)
def dump_text(s, *fpaths):
with open(f_join(*fpaths), "w") as fp:
fp.write(s)
def dump_text_lines(lines: list[str], *fpaths, add_newline=True):
with open(f_join(*fpaths), "w") as fp:
for line in lines:
print(line, file=fp, end="\n" if add_newline else "")
# aliases to be consistent with other load_* and dump_*
pickle_load = load_pickle
pickle_dump = dump_pickle
text_load = load_text
read_text = load_text
read_text_lines = load_text_lines
write_text = dump_text
write_text_lines = dump_text_lines
text_dump = dump_text

View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
# @Date : 2023/09/25 19:27
# @Author : yuymf
# @Desc : Temp using @https://github.com/MineDojo/Voyager/blob/main/voyager/utils/json_utils.py
import json
import re
from typing import Any, Dict, Union
from .file_utils import f_join
def json_load(*file_path, **kwargs):
file_path = f_join(file_path)
with open(file_path, "r") as fp:
return json.load(fp, **kwargs)
def json_loads(string, **kwargs):
return json.loads(string, **kwargs)
def json_dump(data, *file_path, **kwargs):
file_path = f_join(file_path)
with open(file_path, "w") as fp:
json.dump(data, fp, **kwargs)
def json_dumps(data, **kwargs):
"""
Returns: string
"""
return json.dumps(data, **kwargs)
# ---------------- Aliases -----------------
# add aliases where verb goes first, json_load -> load_json
load_json = json_load
loads_json = json_loads
dump_json = json_dump
dumps_json = json_dumps
def extract_char_position(error_message: str) -> int:
"""Extract the character position from the JSONDecodeError message.
Args:
error_message (str): The error message from the JSONDecodeError
exception.
Returns:
int: The character position.
"""
import re
char_pattern = re.compile(r"\(char (\d+)\)")
if match := char_pattern.search(error_message):
return int(match[1])
else:
raise ValueError("Character position not found in the error message.")
def add_quotes_to_property_names(json_string: str) -> str:
"""
Add quotes to property names in a JSON string.
Args:
json_string (str): The JSON string.
Returns:
str: The JSON string with quotes added to property names.
"""
def replace_func(match):
return f'"{match.group(1)}":'
property_name_pattern = re.compile(r"(\w+):")
corrected_json_string = property_name_pattern.sub(replace_func, json_string)
try:
json.loads(corrected_json_string)
return corrected_json_string
except json.JSONDecodeError as e:
raise e
def balance_braces(json_string: str) -> str:
"""
Balance the braces in a JSON string.
Args:
json_string (str): The JSON string.
Returns:
str: The JSON string with braces balanced.
"""
open_braces_count = json_string.count("{")
close_braces_count = json_string.count("}")
while open_braces_count > close_braces_count:
json_string += "}"
close_braces_count += 1
while close_braces_count > open_braces_count:
json_string = json_string.rstrip("}")
close_braces_count -= 1
try:
json.loads(json_string)
return json_string
except json.JSONDecodeError as e:
raise e
def fix_invalid_escape(json_str: str, error_message: str) -> str:
while error_message.startswith("Invalid \\escape"):
bad_escape_location = extract_char_position(error_message)
json_str = json_str[:bad_escape_location] + json_str[bad_escape_location + 1 :]
try:
json.loads(json_str)
return json_str
except json.JSONDecodeError as e:
error_message = str(e)
return json_str
def correct_json(json_str: str) -> str:
"""
Correct common JSON errors.
Args:
json_str (str): The JSON string.
"""
try:
json.loads(json_str)
return json_str
except json.JSONDecodeError as e:
error_message = str(e)
if error_message.startswith("Invalid \\escape"):
json_str = fix_invalid_escape(json_str, error_message)
if error_message.startswith(
"Expecting property name enclosed in double quotes"
):
json_str = add_quotes_to_property_names(json_str)
try:
json.loads(json_str)
return json_str
except json.JSONDecodeError as e:
error_message = str(e)
if balanced_str := balance_braces(json_str):
return balanced_str
return json_str
def fix_and_parse_json(
json_str: str, try_to_fix_with_gpt: bool = True
) -> Union[str, Dict[Any, Any]]:
"""Fix and parse JSON string"""
try:
json_str = json_str.replace("\t", "")
return json.loads(json_str)
except json.JSONDecodeError as _: # noqa: F841
json_str = correct_json(json_str)
try:
return json.loads(json_str)
except json.JSONDecodeError as _: # noqa: F841
pass
# Let's do something manually:
# sometimes GPT responds with something BEFORE the braces:
# "I'm sorry, I don't understand. Please try again."
# {"text": "I'm sorry, I don't understand. Please try again.",
# "confidence": 0.0}
# So let's try to find the first brace and then parse the rest
# of the string
try:
brace_index = json_str.index("{")
json_str = json_str[brace_index:]
last_brace_index = json_str.rindex("}")
json_str = json_str[: last_brace_index + 1]
return json.loads(json_str)
except json.JSONDecodeError as e: # noqa: F841
# if try_to_fix_with_gpt:
# print(
# "Warning: Failed to parse AI output, attempting to fix."
# "\n If you see this warning frequently, it's likely that"
# " your prompt is confusing the AI. Try changing it up"
# " slightly."
# )
# # Now try to fix this up using the ai_functions
# ai_fixed_json = fix_json(json_str, JSON_SCHEMA)
#
# if ai_fixed_json != "failed":
# return json.loads(ai_fixed_json)
# else:
# # This allows the AI to react to the error message,
# # which usually results in it correcting its ways.
# print("Failed to fix ai output, telling the AI.")
# return json_str
# else:
raise e
# def fix_json(json_str: str, schema: str) -> str:
# """Fix the given JSON string to make it parseable and fully complient with the provided schema."""
#
# # Try to fix the JSON using gpt:
# function_string = "def fix_json(json_str: str, schema:str=None) -> str:"
# args = [f"'''{json_str}'''", f"'''{schema}'''"]
# description_string = (
# "Fixes the provided JSON string to make it parseable"
# " and fully complient with the provided schema.\n If an object or"
# " field specified in the schema isn't contained within the correct"
# " JSON, it is ommited.\n This function is brilliant at guessing"
# " when the format is incorrect."
# )
#
# # If it doesn't already start with a "`", add one:
# if not json_str.startswith("`"):
# json_str = "```json\n" + json_str + "\n```"
# result_string = call_ai_function(
# function_string, args, description_string, model=cfg.fast_llm_model
# )
# if cfg.debug:
# print("------------ JSON FIX ATTEMPT ---------------")
# print(f"Original JSON: {json_str}")
# print("-----------")
# print(f"Fixed JSON: {result_string}")
# print("----------- END OF FIX ATTEMPT ----------------")
#
# try:
# json.loads(result_string) # just check the validity
# return result_string
# except: # noqa: E722
# # Get the call stack:
# # import traceback
# # call_stack = traceback.format_exc()
# # print(f"Failed to fix JSON: '{json_str}' "+call_stack)
# return "failed"

View file

@ -3,7 +3,8 @@
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
import pkg_resources
from .file_utils import load_text
def load_prompt(prompt):
pass
package_path = pkg_resources.resource_filename("metagpt", "")
return load_text(f"{package_path}/prompts/minecraft/{prompt}.txt")

View file

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# @Date : 2023/09/25 16:12
# @Author : yuymf
# @Desc : Temp using:@https://github.com/MineDojo/Voyager/blob/main/voyager/env/process_monitor.py
import time
import re
import warnings
from typing import List
import psutil
import subprocess
import logging
import threading
import metagpt.utils.minecraft as U
class SubprocessMonitor:
def __init__(
self,
commands: List[str],
name: str,
ready_match: str = r".*",
log_path: str = "logs",
callback_match: str = r"^(?!x)x$", # regex that will never match
callback: callable = None,
finished_callback: callable = None,
):
self.commands = commands
start_time = time.strftime("%Y%m%d_%H%M%S")
self.name = name
self.logger = logging.getLogger(name)
handler = logging.FileHandler(U.f_join(log_path, f"{start_time}.log"))
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
self.process = None
self.ready_match = ready_match
self.ready_event = None
self.ready_line = None
self.callback_match = callback_match
self.callback = callback
self.finished_callback = finished_callback
self.thread = None
def _start(self):
self.logger.info(f"Starting subprocess with commands: {self.commands}")
self.process = psutil.Popen(
self.commands,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
print(f"Subprocess {self.name} started with PID {self.process.pid}.")
for line in iter(self.process.stdout.readline, ""):
self.logger.info(line.strip())
if re.search(self.ready_match, line):
self.ready_line = line
self.logger.info("Subprocess is ready.")
self.ready_event.set()
if re.search(self.callback_match, line):
self.callback()
if not self.ready_event.is_set():
self.ready_event.set()
warnings.warn(f"Subprocess {self.name} failed to start.")
if self.finished_callback:
self.finished_callback()
def run(self):
self.ready_event = threading.Event()
self.ready_line = None
self.thread = threading.Thread(target=self._start)
self.thread.start()
self.ready_event.wait()
def stop(self):
self.logger.info("Stopping subprocess.")
if self.process and self.process.is_running():
self.process.terminate()
self.process.wait()
# def __del__(self):
# if self.process.is_running():
# self.stop()
@property
def is_running(self):
if self.process is None:
return False
return self.process.is_running()

View file

@ -13,6 +13,7 @@ from metagpt.minecraft_team import MinecraftPlayer
async def learn(task="Start", investment: float = 50.0, n_round: int = 3):
mc_player = MinecraftPlayer()
mc_player.set_port(2253) # Modify this to your LAN port
mc_player.hire(
[
CurriculumDesigner(),