├── .gitignore
├── FAQ.md
├── LICENSE
├── README.md
├── images
└── pull.png
├── installation
├── fabric_mods_install.md
└── minecraft_instance_install.md
├── requirements.txt
├── setup.py
└── voyager
├── __init__.py
├── agents
├── __init__.py
├── action.py
├── critic.py
├── curriculum.py
└── skill.py
├── control_primitives
├── .prettierrc.json
├── __init__.py
├── craftHelper.js
├── craftItem.js
├── exploreUntil.js
├── givePlacedItemBack.js
├── killMob.js
├── mineBlock.js
├── placeItem.js
├── shoot.js
├── smeltItem.js
├── useChest.js
└── waitForMobRemoved.js
├── control_primitives_context
├── .prettierrc.json
├── __init__.py
├── craftItem.js
├── exploreUntil.js
├── killMob.js
├── mineBlock.js
├── mineflayer.js
├── placeItem.js
├── smeltItem.js
└── useChest.js
├── env
├── .gitignore
├── __init__.py
├── bridge.py
├── minecraft_launcher.py
├── mineflayer
│ ├── .prettierignore
│ ├── .prettierrc.json
│ ├── index.js
│ ├── lib
│ │ ├── observation
│ │ │ ├── base.js
│ │ │ ├── chests.js
│ │ │ ├── inventory.js
│ │ │ ├── onChat.js
│ │ │ ├── onError.js
│ │ │ ├── onSave.js
│ │ │ ├── status.js
│ │ │ └── voxels.js
│ │ ├── skillLoader.js
│ │ └── utils.js
│ ├── mineflayer-collectblock
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── _config.yml
│ │ ├── docs
│ │ │ └── api.md
│ │ ├── examples
│ │ │ ├── collector.js
│ │ │ ├── oreMiner.js
│ │ │ └── storageBot.js
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── BlockVeins.ts
│ │ │ ├── CollectBlock.ts
│ │ │ ├── Inventory.ts
│ │ │ ├── Targets.ts
│ │ │ ├── TaskQueue.ts
│ │ │ ├── TemporarySubscriber.ts
│ │ │ ├── Util.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ └── package.json
└── process_monitor.py
├── prompts
├── __init__.py
├── action_response_format.txt
├── action_template.txt
├── critic.txt
├── curriculum.txt
├── curriculum_qa_step1_ask_questions.txt
├── curriculum_qa_step2_answer_questions.txt
├── curriculum_task_decomposition.txt
└── skill.txt
├── utils
├── __init__.py
├── file_utils.py
├── json_utils.py
└── record_utils.py
└── voyager.py
/.gitignore:
--------------------------------------------------------------------------------
1 | ckpt*
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | .static_storage/
56 | .media/
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | # env/
88 | venv/
89 | # ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # Mac
107 | .DS_Store
108 |
109 | # MuJoCo License key
110 | mjkey.txt
111 |
112 | .mujocomanip_temp_model.xml
113 |
114 | # Python IDE
115 | .idea
116 |
117 | # Locally generated files
118 | dump.rdb
119 | *.local.ipynb
120 | runs/
121 | temp*
122 | debug_*
123 | *.swp
124 |
125 | .tabnine_root
126 |
127 | logs/
128 | plot_iter.py
129 | *.svg
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 | * [I got connection error after I click on the Azure login link and login to Microsoft account.](#i-get-a-connection-error-after-i-click-on-the-azure-login-link-and-login-to-my-microsoft-account)
3 | * [I get `KeyError: 'access_token'` after I copied the link](#i-get-keyerror-accesstoken-after-i-copied-the-link)
4 | * [I got `Subprocess Mineflayer failed to start` error.](#i-get-subprocess-mineflayer-failed-to-start-error)
5 | * [I see the bot left and rejoin the game after each task.](#i-see-the-bot-left-and-rejoin-the-game-after-each-task)
6 | * [How to show the bot's first-person perspective?](#how-to-show-the-bots-first-person-view)
7 | * [Can I use GPT-3.5 instead of GPT-4?](#can-i-use-gpt-35-instead-of-gpt-4)
8 | * [What's the estimated cost of running Voyager?](#whats-the-estimated-cost-of-running-voyager)
9 |
10 | ## I get a connection error after I click on the Azure login link and login to my Microsoft account.
11 |
12 | It's normal that you get a connection refused or 404 error after you log in. You will still see the new URL in your browser. You just need to copy and paste that link. It should contain things like `code=M.C....` in that link.
13 |
14 | ## I get `KeyError: 'access_token'` after I copied the link
15 |
16 | While testing Voyager, we use Redirect URI Type: `Public client/native (mobile & desktop)` in the app registration for Azure Login. However, according to the report in issue [#34](https://github.com/MineDojo/Voyager/issues/34#issuecomment-1567007133), the URI Type was changed to "Web" and it resolved the problem. Feel free to attempt both URI Types to determine which one works for you.
17 |
18 | ## I get `Subprocess Mineflayer failed to start` error.
19 |
20 | There are many reasons that may cause this problem. You can try with following solutions:
21 | 1. Make sure you install nodejs and the dependency packages correctly. You can use the following command to check your installation:
22 | ```bash
23 | cd voyager/env/mineflayer
24 | node index.js
25 | ```
26 | If you see `Server started on port {PORT}`, then your installation is correct. You can kill the process by `Ctrl+C`.
27 | 2. Make sure you install Fabric correctly. You should be able to select the Fabric version in the Minecraft launcher.
28 | 3. Each Mineflayer process can only listen to one port. If you want to start multiple instances of `Voyager`, you need to manually change the port when initialization:
29 | ```python
30 | from voyager import Voyager
31 | voyager = Voyager(
32 | server_port=3001, # default is 3000
33 | ...
34 | )
35 | ```
36 |
37 | ## I see the bot left and rejoin the game after each task.
38 |
39 | After completing each task, we'll reset the environment, which means the bot will exit and rejoin the game. This reset is necessary to synchronize Mineflayer with the Minecraft game. However, certain commands we utilize might result in lag on the Mineflayer side, causing the inventory stored in Mineflayer to differ from the actual inventory in the game. If you wish to prevent the reset, you can use `voyager.learn(reset_env=False)` and consider increasing the `env_wait_ticks` value. This will also provide Mineflayer with additional time to sync with the Minecraft game.
40 |
41 |
42 | ## How to show the bot's first-person view?
43 |
44 | Due to the Mineflayer's limitation, we currently can not directly get the bot's view in the game. Although there's a plugin called [prismarine-viewer](https://github.com/PrismarineJS/prismarine-viewer), the video quality is not good enough, so we opt not to use it. Our demo video is generated by [replay-mod](https://www.replaymod.com/). We start the recording and let the bot play for hours, then come back to the recording and render the view from the bot.
45 |
46 |
47 | ## Can I use GPT-3.5 instead of GPT-4?
48 |
49 | It's highly recommended to use GPT-4. GPT-3.5 falls behind in terms of code quality and reasoning ability compared to GPT-4. Moreover, GPT-3.5 has a limited context length, which means it may provide incomplete responses. If you insist on using GPT-3.5, it is essential to configure it with `skill_manager_retrieval_top_k=0` to disable the skill library. This will reduce the context length of the prompt.
50 |
51 | ## What's the estimated cost of running Voyager?
52 |
53 | Using Voyager for approximately 160 iterations using GPT-4 will cost you around USD 50. It's important to keep a close eye on your OpenAI API expenses and avoid unnecessary spending. Once Voyager begins running, it's recommended to monitor the bot's actions for a period and ensure that it successfully completes some tasks.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 MineDojo Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Voyager: An Open-Ended Embodied Agent with Large Language Models
2 |
3 |
4 | [[Website]](https://voyager.minedojo.org/)
5 | [[Arxiv]](https://arxiv.org/abs/2305.16291)
6 | [[PDF]](https://voyager.minedojo.org/assets/documents/voyager.pdf)
7 | [[Tweet]](https://twitter.com/DrJimFan/status/1662115266933972993?s=20)
8 |
9 | [](https://github.com/MineDojo/Voyager)
10 | [](https://github.com/MineDojo/Voyager/blob/main/LICENSE)
11 | ______________________________________________________________________
12 |
13 |
14 | https://github.com/MineDojo/Voyager/assets/25460983/ce29f45b-43a5-4399-8fd8-5dd105fd64f2
15 |
16 | 
17 |
18 |
19 |
20 |
21 | We introduce Voyager, the first LLM-powered embodied lifelong learning agent
22 | in Minecraft that continuously explores the world, acquires diverse skills, and
23 | makes novel discoveries without human intervention. Voyager consists of three
24 | key components: 1) an automatic curriculum that maximizes exploration, 2) an
25 | ever-growing skill library of executable code for storing and retrieving complex
26 | behaviors, and 3) a new iterative prompting mechanism that incorporates environment
27 | feedback, execution errors, and self-verification for program improvement.
28 | Voyager interacts with GPT-4 via blackbox queries, which bypasses the need for
29 | model parameter fine-tuning. The skills developed by Voyager are temporally
30 | extended, interpretable, and compositional, which compounds the agent’s abilities
31 | rapidly and alleviates catastrophic forgetting. Empirically, Voyager shows
32 | strong in-context lifelong learning capability and exhibits exceptional proficiency
33 | in playing Minecraft. It obtains 3.3× more unique items, travels 2.3× longer
34 | distances, and unlocks key tech tree milestones up to 15.3× faster than prior SOTA.
35 | Voyager is able to utilize the learned skill library in a new Minecraft world to
36 | solve novel tasks from scratch, while other techniques struggle to generalize.
37 |
38 | In this repo, we provide Voyager code. This codebase is under [MIT License](LICENSE).
39 |
40 | # Installation
41 | Voyager requires Python ≥ 3.9 and Node.js ≥ 16.13.0. We have tested on Ubuntu 20.04, Windows 11, and macOS. You need to follow the instructions below to install Voyager.
42 |
43 | ## Python Install
44 | ```
45 | git clone https://github.com/MineDojo/Voyager
46 | cd Voyager
47 | pip install -e .
48 | ```
49 |
50 | ## Node.js Install
51 | In addition to the Python dependencies, you need to install the following Node.js packages:
52 | ```
53 | cd voyager/env/mineflayer
54 | npm install -g npx
55 | npm install
56 | cd mineflayer-collectblock
57 | npx tsc
58 | cd ..
59 | npm install
60 | ```
61 |
62 | ## Minecraft Instance Install
63 |
64 | Voyager depends on Minecraft game. You need to install Minecraft game and set up a Minecraft instance.
65 |
66 | Follow the instructions in [Minecraft Login Tutorial](installation/minecraft_instance_install.md) to set up your Minecraft Instance.
67 |
68 | ## Fabric Mods Install
69 |
70 | You need to install fabric mods to support all the features in Voyager. Remember to use the correct Fabric version of all the mods.
71 |
72 | Follow the instructions in [Fabric Mods Install](installation/fabric_mods_install.md) to install the mods.
73 |
74 | # Getting Started
75 | Voyager uses OpenAI's GPT-4 as the language model. You need to have an OpenAI API key to use Voyager. You can get one from [here](https://platform.openai.com/account/api-keys).
76 |
77 | After the installation process, you can run Voyager by:
78 | ```python
79 | from voyager import Voyager
80 |
81 | # you can also use mc_port instead of azure_login, but azure_login is highly recommended
82 | azure_login = {
83 | "client_id": "YOUR_CLIENT_ID",
84 | "redirect_url": "https://127.0.0.1/auth-response",
85 | "secret_value": "[OPTIONAL] YOUR_SECRET_VALUE",
86 | "version": "fabric-loader-0.14.18-1.19", # the version Voyager is tested on
87 | }
88 | openai_api_key = "YOUR_API_KEY"
89 |
90 | voyager = Voyager(
91 | azure_login=azure_login,
92 | openai_api_key=openai_api_key,
93 | )
94 |
95 | # start lifelong learning
96 | voyager.learn()
97 | ```
98 |
99 | * If you are running with `Azure Login` for the first time, it will ask you to follow the command line instruction to generate a config file.
100 | * For `Azure Login`, you also need to select the world and open the world to LAN by yourself. After you run `voyager.learn()` the game will pop up soon, you need to:
101 | 1. Select `Singleplayer` and press `Create New World`.
102 | 2. Set Game Mode to `Creative` and Difficulty to `Peaceful`.
103 | 3. After the world is created, press `Esc` key and press `Open to LAN`.
104 | 4. Select `Allow cheats: ON` and press `Start LAN World`. You will see the bot join the world soon.
105 |
106 | # Resume from a checkpoint during learning
107 |
108 | If you stop the learning process and want to resume from a checkpoint later, you can initialize Voyager by:
109 | ```python
110 | from voyager import Voyager
111 |
112 | voyager = Voyager(
113 | azure_login=azure_login,
114 | openai_api_key=openai_api_key,
115 | ckpt_dir="YOUR_CHECKPOINT_DIR",
116 | resume=True,
117 | )
118 | ```
119 |
120 | # FAQ
121 | If you have any questions, please check our [FAQ](FAQ.md) first before opening an issue.
122 |
123 | # Paper and Citation
124 |
125 | If you find our work useful, please consider citing us!
126 |
127 | ```bibtex
128 | @article{wang2023voyager,
129 | title = {Voyager: An Open-Ended Embodied Agent with Large Language Models},
130 | author = {Guanzhi Wang and Yuqi Xie and Yunfan Jiang and Ajay Mandlekar and Chaowei Xiao and Yuke Zhu and Linxi Fan and Anima Anandkumar},
131 | year = {2023},
132 | journal = {arXiv preprint arXiv: Arxiv-2305.16291}
133 | }
134 | ```
135 |
136 | Disclaimer: This project is strictly for research purposes, and not an official product from NVIDIA.
137 |
--------------------------------------------------------------------------------
/images/pull.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/better-py/annotated-voyager/39bb8738dea8b95b601fe2b868c5442950d87edb/images/pull.png
--------------------------------------------------------------------------------
/installation/fabric_mods_install.md:
--------------------------------------------------------------------------------
1 | # Fabric Mods Install
2 | In this tutorial, we will install the Fabric launcher and 5 mods. Remember to use the correct Fabric version that matches your game version (1.19) of all the mods.
3 | 1. You can download the latest Fabric Installer from [here](https://fabricmc.net/use/installer/). For Windows users, just download the `.exe` file. For Mac or Ubuntu users, download the jar file and call `java -jar fabric-installer-0.11.2.jar` to install. Select game version to be `1.19` and loader version to be `0.14.18`. It will automatically detect your Minecraft game install location.
4 | 2. After installing Fabric, you will have a `YOUR_MINECRAFT_GAME_LOCATION/mod` folder. You need to put all the mods under this folder. Also, you will have a `YOUR_MINECRAFT_GAME_LOCATION/versions/fabric-loader-0.14.18-1.19`. This is the version you will run the game with.
5 | 3. Here are 4 mods that can be directly downloaded to `YOUR_MINECRAFT_GAME_LOCATION/mod` folder:
6 | * [Fabric API](https://modrinth.com/mod/fabric-api): Basic Fabric APIs.
7 | * [Mod Menu](https://cdn.modrinth.com/data/mOgUt4GM/versions/4.0.4/modmenu-4.0.4.jar): Used to manage all the mods that you download.
8 | * [Complete Config](https://www.curseforge.com/minecraft/mc-mods/completeconfig/download/3821056): Dependency of server pause.
9 | * [Multi Server Pause](https://www.curseforge.com/minecraft/mc-mods/multiplayer-server-pause-fabric/download/3822586): Used to pause the server when waiting for GPT-4 to reply.
10 | 4. For the last mod [Better Respawn](https://github.com/xieleo5/better-respawn/tree/1.19), you need to manually clone and compile.
11 |
12 | * After you clone the repo, remove the `'forge'` string in the last line of `settings.gradle`. Then run `gradlew build` to compile the mod. You will find the compiled jar file in `better-respawn/fabric/build/libs/better-respawn-fabric-1.19-2.0.0.jar`. Put the jar file to the mod folder.
13 | * You will need a Java Runtime Environment v17+ to build `better-respawn`. Some newer JRE versions will error during build. Find the JRE v17 archive [here](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html).
14 | * After you launch the game, go to `YOUR_MINECRAFT_GAME_LOCATION/config/better-respawn`, and modify the properties file with:
15 | ```
16 | respawn_block_range=32
17 | max_respawn_distance=32
18 | min_respawn_distance=0
19 | ```
20 | 5. Don't forget to change the `version` in `azure_login` to `fabric-loader-0.14.18-1.19` that you are using. You can find it under `YOUR_MINECRAFT_GAME_LOCATION/version` folder.
21 |
22 | You can return to [README.md](../README.md#getting-started) and getting started now.
23 |
--------------------------------------------------------------------------------
/installation/minecraft_instance_install.md:
--------------------------------------------------------------------------------
1 | # Minecraft Instance Install
2 | To start using Voyager, you should first make sure to have an official [Minecraft](https://www.minecraft.net/) game (version 1.19) installed.
3 |
4 | There are two ways to start a Minecraft instance for Voyager. Sometimes GPT-4 will write an infinite loop that runs forever. In this case, there'll be a request timeout. Using Azure login can automatically resume the running if there's a request timeout.
5 |
6 | ## Option 1: Microsoft Azure Login (Recommended)
7 | Using this method will allow Voyager to automatically resume when there's a request timeout. This is dependent on the [minecraft-launcher-lib](https://minecraft-launcher-lib.readthedocs.io/en/stable/tutorial/microsoft_login.html#let-the-user-log-in) library.
8 |
9 | 1. Sign in to [Azure Portal](https://portal.azure.com/).
10 | 2. Go to [Azure Active Directory](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview).
11 | 3. Click on the `App Registrations` tab on the left panel.
12 | 4. Click on the `New registration` button.
13 | 5. Fill the form with the following values:
14 | - Name: `YOUR_APP_NAME`
15 | - Supported account types: `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts`
16 | - Redirect URI Type: `Public client/native (mobile & desktop)`, Value: `https://127.0.0.1/auth-response` (If you get `KeyError: 'access_token'` in the end, you can try to change the type to `Web`, see [FAQ](https://github.com/MineDojo/Voyager/blob/main/FAQ.md) for more information)
17 | 6. Click on the `Register` button.
18 | 7. The `Application (client) ID` will be your `client_id`.
19 | 8. [Optional] Go to the `Certificates & Secrets` tab and click on the `New client secret` button. Fill the description by yourself. After you click `Add`, you will see your value, this will be your `secret_value`.
20 | 9. Go to your Minecraft install location `YOUR_MINECRAFT_GAME_LOCATION/versions`, and check all the versions you have. All the folder names are your valid `version` value.
21 |
22 | After these steps, you will finally get your azure_login information:
23 | ```python
24 | azure_login = {
25 | "client_id": "CLIENT_ID FROM STEP 7",
26 | "redirect_url": "https://127.0.0.1/auth-response",
27 | "secret_value": "[OPTIONAL] SECRET_KEY FROM STEP 8",
28 | "version": "MINECRAFT VERSION YOU WANT TO USE",
29 | }
30 | ```
31 | **Voyager use `fabric-loader-0.14.18-1.19` version to run all the experiments.** You may not have this version currently, you can move on to the [Fabric Mods Install](fabric_mods_install.md#fabric-mods-install) section and follow the instructions there to install the fabric version of the game.
32 |
33 | ## Option 2: Minecraft Official Launcher
34 |
35 | After you install official Minecraft, you should have a Minecraft official launcher, open it, and follow the instructions here:
36 | 1. Select the version you want to play and start the game.
37 | 2. Select `Singleplayer` and create a new world.
38 | 3. Set Game Mode to `Creative` and Difficulty to `Peaceful`.
39 | 4. After the world is created, press `Esc` and select `Open to LAN`.
40 | 5. Select `Allow cheats: ON` and press `Start LAN World`.
41 | 6. You will see a port number in the chat log, that is your `mc-port`, use this number to instantiate Voyager later.
42 |
43 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tqdm
2 | langchain
3 | javascript
4 | setuptools
5 | openai
6 | chardet
7 | cchardet
8 | chromadb
9 | tiktoken
10 | requests
11 | setuptools
12 | gymnasium
13 | psutil
14 | minecraft_launcher_lib
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 | import pkg_resources
4 | from setuptools import setup, find_packages
5 |
6 |
7 | PKG_NAME = "voyager"
8 | VERSION = "0.1"
9 | EXTRAS = {}
10 |
11 |
12 | def _read_file(fname):
13 | # this_dir = os.path.abspath(os.path.dirname(__file__))
14 | # with open(os.path.join(this_dir, fname)) as f:
15 | with pathlib.Path(fname).open() as fp:
16 | return fp.read()
17 |
18 |
19 | def _read_install_requires():
20 | with pathlib.Path("requirements.txt").open() as fp:
21 | return [
22 | str(requirement) for requirement in pkg_resources.parse_requirements(fp)
23 | ]
24 |
25 |
26 | def _fill_extras(extras):
27 | if extras:
28 | extras["all"] = list(set([item for group in extras.values() for item in group]))
29 | return extras
30 |
31 |
32 | setup(
33 | name=PKG_NAME,
34 | version=VERSION,
35 | author=f"MineDojo Team",
36 | url="https://github.com/MineDojo/Voyager",
37 | description="research project",
38 | long_description=_read_file("README.md"),
39 | long_description_content_type="text/markdown",
40 | keywords=[
41 | "Open-Ended Learning",
42 | "Lifelong Learning",
43 | "Embodied Agents",
44 | "Large Language Models",
45 | ],
46 | license="MIT License",
47 | packages=find_packages(include=f"{PKG_NAME}.*"),
48 | include_package_data=True,
49 | zip_safe=False,
50 | install_requires=_read_install_requires(),
51 | extras_require=_fill_extras(EXTRAS),
52 | python_requires=">=3.9",
53 | classifiers=[
54 | "Development Status :: 5 - Production/Stable",
55 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
56 | "Environment :: Console",
57 | "Programming Language :: Python :: 3.9",
58 | ],
59 | )
60 |
--------------------------------------------------------------------------------
/voyager/__init__.py:
--------------------------------------------------------------------------------
1 | from .voyager import Voyager
2 |
--------------------------------------------------------------------------------
/voyager/agents/__init__.py:
--------------------------------------------------------------------------------
1 | from .action import ActionAgent
2 | from .critic import CriticAgent
3 | from .curriculum import CurriculumAgent
4 | from .skill import SkillManager
5 |
--------------------------------------------------------------------------------
/voyager/agents/action.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 |
4 | import voyager.utils as U
5 | from javascript import require
6 | from langchain.chat_models import ChatOpenAI
7 | from langchain.prompts import SystemMessagePromptTemplate
8 | from langchain.schema import AIMessage, HumanMessage, SystemMessage
9 |
10 | from voyager.prompts import load_prompt
11 | from voyager.control_primitives_context import load_control_primitives_context
12 |
13 |
14 | class ActionAgent:
15 | def __init__(
16 | self,
17 | model_name="gpt-3.5-turbo",
18 | temperature=0,
19 | request_timout=120,
20 | ckpt_dir="ckpt",
21 | resume=False,
22 | chat_log=True,
23 | execution_error=True,
24 | ):
25 | self.ckpt_dir = ckpt_dir
26 | self.chat_log = chat_log
27 | self.execution_error = execution_error
28 | U.f_mkdir(f"{ckpt_dir}/action")
29 | if resume:
30 | print(f"\033[32mLoading Action Agent from {ckpt_dir}/action\033[0m")
31 | self.chest_memory = U.load_json(f"{ckpt_dir}/action/chest_memory.json")
32 | else:
33 | self.chest_memory = {}
34 | self.llm = ChatOpenAI(
35 | model_name=model_name,
36 | temperature=temperature,
37 | request_timeout=request_timout,
38 | )
39 |
40 | def update_chest_memory(self, chests):
41 | for position, chest in chests.items():
42 | if position in self.chest_memory:
43 | if isinstance(chest, dict):
44 | self.chest_memory[position] = chest
45 | else:
46 | print(f"\033[32mAction Agent saving chest {position}: {chest}\033[0m")
47 | self.chest_memory[position] = chest
48 | U.dump_json(self.chest_memory, f"{self.ckpt_dir}/action/chest_memory.json")
49 |
50 | def render_chest_observation(self):
51 | chests = []
52 | for chest_position, chest in self.chest_memory.items():
53 | if isinstance(chest, dict) and len(chest) > 0:
54 | chests.append(f"{chest_position}: {chest}")
55 | for chest_position, chest in self.chest_memory.items():
56 | if isinstance(chest, dict) and len(chest) == 0:
57 | chests.append(f"{chest_position}: Empty")
58 | for chest_position, chest in self.chest_memory.items():
59 | if isinstance(chest, str):
60 | assert chest == "Unknown"
61 | chests.append(f"{chest_position}: Unknown items inside")
62 | assert len(chests) == len(self.chest_memory)
63 | if chests:
64 | chests = "\n".join(chests)
65 | return f"Chests:\n{chests}\n\n"
66 | else:
67 | return f"Chests: None\n\n"
68 |
69 | def render_system_message(self, skills=[]):
70 | system_template = load_prompt("action_template")
71 | # FIXME: Hardcoded control_primitives
72 | base_skills = [
73 | "exploreUntil",
74 | "mineBlock",
75 | "craftItem",
76 | "placeItem",
77 | "smeltItem",
78 | "killMob",
79 | ]
80 | if not self.llm.model_name == "gpt-3.5-turbo":
81 | base_skills += [
82 | "useChest",
83 | "mineflayer",
84 | ]
85 | programs = "\n\n".join(load_control_primitives_context(base_skills) + skills)
86 | response_format = load_prompt("action_response_format")
87 | system_message_prompt = SystemMessagePromptTemplate.from_template(
88 | system_template
89 | )
90 | system_message = system_message_prompt.format(
91 | programs=programs, response_format=response_format
92 | )
93 | assert isinstance(system_message, SystemMessage)
94 | return system_message
95 |
96 | def render_human_message(
97 | self, *, events, code="", task="", context="", critique=""
98 | ):
99 | chat_messages = []
100 | error_messages = []
101 | # FIXME: damage_messages is not used
102 | damage_messages = []
103 | assert events[-1][0] == "observe", "Last event must be observe"
104 | for i, (event_type, event) in enumerate(events):
105 | if event_type == "onChat":
106 | chat_messages.append(event["onChat"])
107 | elif event_type == "onError":
108 | error_messages.append(event["onError"])
109 | elif event_type == "onDamage":
110 | damage_messages.append(event["onDamage"])
111 | elif event_type == "observe":
112 | biome = event["status"]["biome"]
113 | time_of_day = event["status"]["timeOfDay"]
114 | voxels = event["voxels"]
115 | entities = event["status"]["entities"]
116 | health = event["status"]["health"]
117 | hunger = event["status"]["food"]
118 | position = event["status"]["position"]
119 | equipment = event["status"]["equipment"]
120 | inventory_used = event["status"]["inventoryUsed"]
121 | inventory = event["inventory"]
122 | assert i == len(events) - 1, "observe must be the last event"
123 |
124 | observation = ""
125 |
126 | if code:
127 | observation += f"Code from the last round:\n{code}\n\n"
128 | else:
129 | observation += f"Code from the last round: No code in the first round\n\n"
130 |
131 | if self.execution_error:
132 | if error_messages:
133 | error = "\n".join(error_messages)
134 | observation += f"Execution error:\n{error}\n\n"
135 | else:
136 | observation += f"Execution error: No error\n\n"
137 |
138 | if self.chat_log:
139 | if chat_messages:
140 | chat_log = "\n".join(chat_messages)
141 | observation += f"Chat log: {chat_log}\n\n"
142 | else:
143 | observation += f"Chat log: None\n\n"
144 |
145 | observation += f"Biome: {biome}\n\n"
146 |
147 | observation += f"Time: {time_of_day}\n\n"
148 |
149 | if voxels:
150 | observation += f"Nearby blocks: {', '.join(voxels)}\n\n"
151 | else:
152 | observation += f"Nearby blocks: None\n\n"
153 |
154 | if entities:
155 | nearby_entities = [
156 | k for k, v in sorted(entities.items(), key=lambda x: x[1])
157 | ]
158 | observation += f"Nearby entities (nearest to farthest): {', '.join(nearby_entities)}\n\n"
159 | else:
160 | observation += f"Nearby entities (nearest to farthest): None\n\n"
161 |
162 | observation += f"Health: {health:.1f}/20\n\n"
163 |
164 | observation += f"Hunger: {hunger:.1f}/20\n\n"
165 |
166 | observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n"
167 |
168 | observation += f"Equipment: {equipment}\n\n"
169 |
170 | if inventory:
171 | observation += f"Inventory ({inventory_used}/36): {inventory}\n\n"
172 | else:
173 | observation += f"Inventory ({inventory_used}/36): Empty\n\n"
174 |
175 | if not (
176 | task == "Place and deposit useless items into a chest"
177 | or task.startswith("Deposit useless items into the chest at")
178 | ):
179 | observation += self.render_chest_observation()
180 |
181 | observation += f"Task: {task}\n\n"
182 |
183 | if context:
184 | observation += f"Context: {context}\n\n"
185 | else:
186 | observation += f"Context: None\n\n"
187 |
188 | if critique:
189 | observation += f"Critique: {critique}\n\n"
190 | else:
191 | observation += f"Critique: None\n\n"
192 |
193 | return HumanMessage(content=observation)
194 |
195 | def process_ai_message(self, message):
196 | assert isinstance(message, AIMessage)
197 |
198 | retry = 3
199 | error = None
200 | while retry > 0:
201 | try:
202 | babel = require("@babel/core")
203 | babel_generator = require("@babel/generator").default
204 |
205 | code_pattern = re.compile(r"```(?:javascript|js)(.*?)```", re.DOTALL)
206 | code = "\n".join(code_pattern.findall(message.content))
207 | parsed = babel.parse(code)
208 | functions = []
209 | assert len(list(parsed.program.body)) > 0, "No functions found"
210 | for i, node in enumerate(parsed.program.body):
211 | if node.type != "FunctionDeclaration":
212 | continue
213 | node_type = (
214 | "AsyncFunctionDeclaration"
215 | if node["async"]
216 | else "FunctionDeclaration"
217 | )
218 | functions.append(
219 | {
220 | "name": node.id.name,
221 | "type": node_type,
222 | "body": babel_generator(node).code,
223 | "params": list(node["params"]),
224 | }
225 | )
226 | # find the last async function
227 | main_function = None
228 | for function in reversed(functions):
229 | if function["type"] == "AsyncFunctionDeclaration":
230 | main_function = function
231 | break
232 | assert (
233 | main_function is not None
234 | ), "No async function found. Your main function must be async."
235 | assert (
236 | len(main_function["params"]) == 1
237 | and main_function["params"][0].name == "bot"
238 | ), f"Main function {main_function['name']} must take a single argument named 'bot'"
239 | program_code = "\n\n".join(function["body"] for function in functions)
240 | exec_code = f"await {main_function['name']}(bot);"
241 | return {
242 | "program_code": program_code,
243 | "program_name": main_function["name"],
244 | "exec_code": exec_code,
245 | }
246 | except Exception as e:
247 | retry -= 1
248 | error = e
249 | time.sleep(1)
250 | return f"Error parsing action response (before program execution): {error}"
251 |
252 | def summarize_chatlog(self, events):
253 | def filter_item(message: str):
254 | craft_pattern = r"I cannot make \w+ because I need: (.*)"
255 | craft_pattern2 = (
256 | r"I cannot make \w+ because there is no crafting table nearby"
257 | )
258 | mine_pattern = r"I need at least a (.*) to mine \w+!"
259 | if re.match(craft_pattern, message):
260 | return re.match(craft_pattern, message).groups()[0]
261 | elif re.match(craft_pattern2, message):
262 | return "a nearby crafting table"
263 | elif re.match(mine_pattern, message):
264 | return re.match(mine_pattern, message).groups()[0]
265 | else:
266 | return ""
267 |
268 | chatlog = set()
269 | for event_type, event in events:
270 | if event_type == "onChat":
271 | item = filter_item(event["onChat"])
272 | if item:
273 | chatlog.add(item)
274 | return "I also need " + ", ".join(chatlog) + "." if chatlog else ""
275 |
--------------------------------------------------------------------------------
/voyager/agents/critic.py:
--------------------------------------------------------------------------------
1 | from voyager.prompts import load_prompt
2 | from voyager.utils.json_utils import fix_and_parse_json
3 | from langchain.chat_models import ChatOpenAI
4 | from langchain.schema import HumanMessage, SystemMessage
5 |
6 |
7 | class CriticAgent:
8 | def __init__(
9 | self,
10 | model_name="gpt-3.5-turbo",
11 | temperature=0,
12 | request_timout=120,
13 | mode="auto",
14 | ):
15 | self.llm = ChatOpenAI(
16 | model_name=model_name,
17 | temperature=temperature,
18 | request_timeout=request_timout,
19 | )
20 | assert mode in ["auto", "manual"]
21 | self.mode = mode
22 |
23 | def render_system_message(self):
24 | system_message = SystemMessage(content=load_prompt("critic"))
25 | return system_message
26 |
27 | def render_human_message(self, *, events, task, context, chest_observation):
28 | assert events[-1][0] == "observe", "Last event must be observe"
29 | biome = events[-1][1]["status"]["biome"]
30 | time_of_day = events[-1][1]["status"]["timeOfDay"]
31 | voxels = events[-1][1]["voxels"]
32 | health = events[-1][1]["status"]["health"]
33 | hunger = events[-1][1]["status"]["food"]
34 | position = events[-1][1]["status"]["position"]
35 | equipment = events[-1][1]["status"]["equipment"]
36 | inventory_used = events[-1][1]["status"]["inventoryUsed"]
37 | inventory = events[-1][1]["inventory"]
38 |
39 | for i, (event_type, event) in enumerate(events):
40 | if event_type == "onError":
41 | print(f"\033[31mCritic Agent: Error occurs {event['onError']}\033[0m")
42 | return None
43 |
44 | observation = ""
45 |
46 | observation += f"Biome: {biome}\n\n"
47 |
48 | observation += f"Time: {time_of_day}\n\n"
49 |
50 | if voxels:
51 | observation += f"Nearby blocks: {', '.join(voxels)}\n\n"
52 | else:
53 | observation += f"Nearby blocks: None\n\n"
54 |
55 | observation += f"Health: {health:.1f}/20\n\n"
56 | observation += f"Hunger: {hunger:.1f}/20\n\n"
57 |
58 | observation += f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n"
59 |
60 | observation += f"Equipment: {equipment}\n\n"
61 |
62 | if inventory:
63 | observation += f"Inventory ({inventory_used}/36): {inventory}\n\n"
64 | else:
65 | observation += f"Inventory ({inventory_used}/36): Empty\n\n"
66 |
67 | observation += chest_observation
68 |
69 | observation += f"Task: {task}\n\n"
70 |
71 | if context:
72 | observation += f"Context: {context}\n\n"
73 | else:
74 | observation += f"Context: None\n\n"
75 |
76 | print(f"\033[31m****Critic Agent human message****\n{observation}\033[0m")
77 | return HumanMessage(content=observation)
78 |
79 | def human_check_task_success(self):
80 | confirmed = False
81 | success = False
82 | critique = ""
83 | while not confirmed:
84 | success = input("Success? (y/n)")
85 | success = success.lower() == "y"
86 | critique = input("Enter your critique:")
87 | print(f"Success: {success}\nCritique: {critique}")
88 | confirmed = input("Confirm? (y/n)") in ["y", ""]
89 | return success, critique
90 |
91 | def ai_check_task_success(self, messages, max_retries=5):
92 | if max_retries == 0:
93 | print(
94 | "\033[31mFailed to parse Critic Agent response. Consider updating your prompt.\033[0m"
95 | )
96 | return False, ""
97 |
98 | if messages[1] is None:
99 | return False, ""
100 |
101 | critic = self.llm(messages).content
102 | print(f"\033[31m****Critic Agent ai message****\n{critic}\033[0m")
103 | try:
104 | response = fix_and_parse_json(critic)
105 | assert response["success"] in [True, False]
106 | if "critique" not in response:
107 | response["critique"] = ""
108 | return response["success"], response["critique"]
109 | except Exception as e:
110 | print(f"\033[31mError parsing critic response: {e} Trying again!\033[0m")
111 | return self.ai_check_task_success(
112 | messages=messages,
113 | max_retries=max_retries - 1,
114 | )
115 |
116 | def check_task_success(
117 | self, *, events, task, context, chest_observation, max_retries=5
118 | ):
119 | human_message = self.render_human_message(
120 | events=events,
121 | task=task,
122 | context=context,
123 | chest_observation=chest_observation,
124 | )
125 |
126 | messages = [
127 | self.render_system_message(),
128 | human_message,
129 | ]
130 |
131 | if self.mode == "human":
132 | return self.human_check_task_success()
133 | elif self.mode == "auto":
134 | return self.ai_check_task_success(
135 | messages=messages, max_retries=max_retries
136 | )
137 | else:
138 | raise ValueError(f"Invalid critic agent mode: {self.mode}")
139 |
--------------------------------------------------------------------------------
/voyager/agents/skill.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import voyager.utils as U
4 | from langchain.chat_models import ChatOpenAI
5 | from langchain.embeddings.openai import OpenAIEmbeddings
6 | from langchain.schema import HumanMessage, SystemMessage
7 | from langchain.vectorstores import Chroma
8 |
9 | from voyager.prompts import load_prompt
10 | from voyager.control_primitives import load_control_primitives
11 |
12 |
13 | class SkillManager:
14 | def __init__(
15 | self,
16 | model_name="gpt-3.5-turbo",
17 | temperature=0,
18 | retrieval_top_k=5,
19 | request_timout=120,
20 | ckpt_dir="ckpt",
21 | resume=False,
22 | ):
23 | self.llm = ChatOpenAI(
24 | model_name=model_name,
25 | temperature=temperature,
26 | request_timeout=request_timout,
27 | )
28 | U.f_mkdir(f"{ckpt_dir}/skill/code")
29 | U.f_mkdir(f"{ckpt_dir}/skill/description")
30 | U.f_mkdir(f"{ckpt_dir}/skill/vectordb")
31 | # programs for env execution
32 | self.control_primitives = load_control_primitives()
33 | if resume:
34 | print(f"\033[33mLoading Skill Manager from {ckpt_dir}/skill\033[0m")
35 | self.skills = U.load_json(f"{ckpt_dir}/skill/skills.json")
36 | else:
37 | self.skills = {}
38 | self.retrieval_top_k = retrieval_top_k
39 | self.ckpt_dir = ckpt_dir
40 | self.vectordb = Chroma(
41 | collection_name="skill_vectordb",
42 | embedding_function=OpenAIEmbeddings(),
43 | persist_directory=f"{ckpt_dir}/skill/vectordb",
44 | )
45 | assert self.vectordb._collection.count() == len(self.skills), (
46 | f"Skill Manager's vectordb is not synced with skills.json.\n"
47 | f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\n"
48 | f"Did you set resume=False when initializing the manager?\n"
49 | f"You may need to manually delete the vectordb directory for running from scratch."
50 | )
51 |
52 | @property
53 | def programs(self):
54 | programs = ""
55 | for skill_name, entry in self.skills.items():
56 | programs += f"{entry['code']}\n\n"
57 | for primitives in self.control_primitives:
58 | programs += f"{primitives}\n\n"
59 | return programs
60 |
61 | def add_skill(self, program_name, program_code):
62 | skill_description = self.generate_skill_description(program_name, program_code)
63 | print(
64 | f"\033[33mSkill Manager generated description for {program_name}:\n{skill_description}\033[0m"
65 | )
66 | if program_name in self.skills:
67 | print(f"\033[33mSkill {program_name} already exists. Rewriting!\033[0m")
68 | self.vectordb._collection.delete(ids=[program_name])
69 | i = 2
70 | while f"{program_name}V{i}.js" in os.listdir(f"{self.ckpt_dir}/skill/code"):
71 | i += 1
72 | dumped_program_name = f"{program_name}V{i}"
73 | else:
74 | dumped_program_name = program_name
75 | self.vectordb.add_texts(
76 | texts=[skill_description],
77 | ids=[program_name],
78 | metadatas=[{"name": program_name}],
79 | )
80 | self.skills[program_name] = {
81 | "code": program_code,
82 | "description": skill_description,
83 | }
84 | assert self.vectordb._collection.count() == len(
85 | self.skills
86 | ), "vectordb is not synced with skills.json"
87 | U.dump_text(
88 | program_code, f"{self.ckpt_dir}/skill/code/{dumped_program_name}.js"
89 | )
90 | U.dump_text(
91 | skill_description,
92 | f"{self.ckpt_dir}/skill/description/{dumped_program_name}.txt",
93 | )
94 | U.dump_json(self.skills, f"{self.ckpt_dir}/skill/skills.json")
95 | self.vectordb.persist()
96 |
97 | def generate_skill_description(self, program_name, program_code):
98 | messages = [
99 | SystemMessage(content=load_prompt("skill")),
100 | HumanMessage(
101 | content=program_code
102 | + "\n\n"
103 | + f"The main function is `{program_name}`."
104 | ),
105 | ]
106 | skill_description = f" // { self.llm(messages).content}"
107 | return f"async function {program_name}(bot) {{\n{skill_description}\n}}"
108 |
109 | def retrieve_skills(self, query):
110 | k = min(self.vectordb._collection.count(), self.retrieval_top_k)
111 | if k == 0:
112 | return []
113 | print(f"\033[33mSkill Manager retrieving for {k} skills\033[0m")
114 | docs_and_scores = self.vectordb.similarity_search_with_score(query, k=k)
115 | print(
116 | f"\033[33mSkill Manager retrieved skills: "
117 | f"{', '.join([doc.metadata['name'] for doc, _ in docs_and_scores])}\033[0m"
118 | )
119 | skills = []
120 | for doc, _ in docs_and_scores:
121 | skills.append(self.skills[doc.metadata["name"]]["code"])
122 | return skills
123 |
--------------------------------------------------------------------------------
/voyager/control_primitives/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
4 |
--------------------------------------------------------------------------------
/voyager/control_primitives/__init__.py:
--------------------------------------------------------------------------------
1 | import pkg_resources
2 | import os
3 | import voyager.utils as U
4 |
5 |
6 | def load_control_primitives(primitive_names=None):
7 | package_path = pkg_resources.resource_filename("voyager", "")
8 | if primitive_names is None:
9 | primitive_names = [
10 | primitives[:-3]
11 | for primitives in os.listdir(f"{package_path}/control_primitives")
12 | if primitives.endswith(".js")
13 | ]
14 | primitives = [
15 | U.load_text(f"{package_path}/control_primitives/{primitive_name}.js")
16 | for primitive_name in primitive_names
17 | ]
18 | return primitives
19 |
--------------------------------------------------------------------------------
/voyager/control_primitives/craftHelper.js:
--------------------------------------------------------------------------------
1 | function failedCraftFeedback(bot, name, item, craftingTable) {
2 | const recipes = bot.recipesAll(item.id, null, craftingTable);
3 | if (!recipes.length) {
4 | throw new Error(`No crafting table nearby`);
5 | } else {
6 | const recipes = bot.recipesAll(
7 | item.id,
8 | null,
9 | mcData.blocksByName.crafting_table.id
10 | );
11 | // find the recipe with the fewest missing ingredients
12 | var min = 999;
13 | var min_recipe = null;
14 | for (const recipe of recipes) {
15 | const delta = recipe.delta;
16 | var missing = 0;
17 | for (const delta_item of delta) {
18 | if (delta_item.count < 0) {
19 | const inventory_item = bot.inventory.findInventoryItem(
20 | mcData.items[delta_item.id].name,
21 | null
22 | );
23 | if (!inventory_item) {
24 | missing += -delta_item.count;
25 | } else {
26 | missing += Math.max(
27 | -delta_item.count - inventory_item.count,
28 | 0
29 | );
30 | }
31 | }
32 | }
33 | if (missing < min) {
34 | min = missing;
35 | min_recipe = recipe;
36 | }
37 | }
38 | const delta = min_recipe.delta;
39 | let message = "";
40 | for (const delta_item of delta) {
41 | if (delta_item.count < 0) {
42 | const inventory_item = bot.inventory.findInventoryItem(
43 | mcData.items[delta_item.id].name,
44 | null
45 | );
46 | if (!inventory_item) {
47 | message += ` ${-delta_item.count} more ${
48 | mcData.items[delta_item.id].name
49 | }, `;
50 | } else {
51 | if (inventory_item.count < -delta_item.count) {
52 | message += `${
53 | -delta_item.count - inventory_item.count
54 | } more ${mcData.items[delta_item.id].name}`;
55 | }
56 | }
57 | }
58 | }
59 | bot.chat(`I cannot make ${name} because I need: ${message}`);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/voyager/control_primitives/craftItem.js:
--------------------------------------------------------------------------------
1 | async function craftItem(bot, name, count = 1) {
2 | // return if name is not string
3 | if (typeof name !== "string") {
4 | throw new Error("name for craftItem must be a string");
5 | }
6 | // return if count is not number
7 | if (typeof count !== "number") {
8 | throw new Error("count for craftItem must be a number");
9 | }
10 | const itemByName = mcData.itemsByName[name];
11 | if (!itemByName) {
12 | throw new Error(`No item named ${name}`);
13 | }
14 | const craftingTable = bot.findBlock({
15 | matching: mcData.blocksByName.crafting_table.id,
16 | maxDistance: 32,
17 | });
18 | if (!craftingTable) {
19 | bot.chat("Craft without a crafting table");
20 | } else {
21 | await bot.pathfinder.goto(
22 | new GoalLookAtBlock(craftingTable.position, bot.world)
23 | );
24 | }
25 | const recipe = bot.recipesFor(itemByName.id, null, 1, craftingTable)[0];
26 | if (recipe) {
27 | bot.chat(`I can make ${name}`);
28 | try {
29 | await bot.craft(recipe, count, craftingTable);
30 | bot.chat(`I did the recipe for ${name} ${count} times`);
31 | } catch (err) {
32 | bot.chat(`I cannot do the recipe for ${name} ${count} times`);
33 | }
34 | } else {
35 | failedCraftFeedback(bot, name, itemByName, craftingTable);
36 | _craftItemFailCount++;
37 | if (_craftItemFailCount > 10) {
38 | throw new Error(
39 | "craftItem failed too many times, check chat log to see what happened"
40 | );
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/voyager/control_primitives/exploreUntil.js:
--------------------------------------------------------------------------------
1 | // Explore downward for 60 seconds: exploreUntil(bot, new Vec3(0, -1, 0), 60);
2 | async function exploreUntil(
3 | bot,
4 | direction,
5 | maxTime = 60,
6 | callback = () => {
7 | return false;
8 | }
9 | ) {
10 | if (typeof maxTime !== "number") {
11 | throw new Error("maxTime must be a number");
12 | }
13 | if (typeof callback !== "function") {
14 | throw new Error("callback must be a function");
15 | }
16 | const test = callback();
17 | if (test) {
18 | bot.chat("Explore success.");
19 | return Promise.resolve(test);
20 | }
21 | if (direction.x === 0 && direction.y === 0 && direction.z === 0) {
22 | throw new Error("direction cannot be 0, 0, 0");
23 | }
24 | if (
25 | !(
26 | (direction.x === 0 || direction.x === 1 || direction.x === -1) &&
27 | (direction.y === 0 || direction.y === 1 || direction.y === -1) &&
28 | (direction.z === 0 || direction.z === 1 || direction.z === -1)
29 | )
30 | ) {
31 | throw new Error(
32 | "direction must be a Vec3 only with value of -1, 0 or 1"
33 | );
34 | }
35 | maxTime = Math.min(maxTime, 1200);
36 | return new Promise((resolve, reject) => {
37 | const dx = direction.x;
38 | const dy = direction.y;
39 | const dz = direction.z;
40 |
41 | let explorationInterval;
42 | let maxTimeTimeout;
43 |
44 | const cleanUp = () => {
45 | clearInterval(explorationInterval);
46 | clearTimeout(maxTimeTimeout);
47 | bot.pathfinder.setGoal(null);
48 | };
49 |
50 | const explore = () => {
51 | const x =
52 | bot.entity.position.x +
53 | Math.floor(Math.random() * 20 + 10) * dx;
54 | const y =
55 | bot.entity.position.y +
56 | Math.floor(Math.random() * 20 + 10) * dy;
57 | const z =
58 | bot.entity.position.z +
59 | Math.floor(Math.random() * 20 + 10) * dz;
60 | let goal = new GoalNear(x, y, z);
61 | if (dy === 0) {
62 | goal = new GoalNearXZ(x, z);
63 | }
64 | bot.pathfinder.setGoal(goal);
65 |
66 | try {
67 | const result = callback();
68 | if (result) {
69 | cleanUp();
70 | bot.chat("Explore success.");
71 | resolve(result);
72 | }
73 | } catch (err) {
74 | cleanUp();
75 | reject(err);
76 | }
77 | };
78 |
79 | explorationInterval = setInterval(explore, 2000);
80 |
81 | maxTimeTimeout = setTimeout(() => {
82 | cleanUp();
83 | bot.chat("Max exploration time reached");
84 | resolve(null);
85 | }, maxTime * 1000);
86 | });
87 | }
88 |
--------------------------------------------------------------------------------
/voyager/control_primitives/givePlacedItemBack.js:
--------------------------------------------------------------------------------
1 | async function givePlacedItemBack(bot, name, position) {
2 | await bot.chat("/gamerule doTileDrops false");
3 | // iterate name and position
4 | const history = [];
5 | for (let i = 0; i < name.length; i++) {
6 | await givePlacedItemBackSingle(bot, name[i], position[i]);
7 | }
8 | await bot.chat("/gamerule doTileDrops true");
9 |
10 | async function givePlacedItemBackSingle(bot, name, position) {
11 | bot.chat(`/give bot ${name} 1`);
12 | const x = Math.floor(position.x);
13 | const y = Math.floor(position.y);
14 | const z = Math.floor(position.z);
15 | // loop through 125 blocks around the block
16 | const size = 3;
17 | for (let dx = -size; dx <= size; dx++) {
18 | for (let dy = -size; dy <= size; dy++) {
19 | for (let dz = -size; dz <= size; dz++) {
20 | const block = bot.blockAt(new Vec3(x + dx, y + dy, z + dz));
21 | if (
22 | block?.name === name &&
23 | !history.includes(block.position)
24 | ) {
25 | await bot.chat(
26 | `/setblock ${x + dx} ${y + dy} ${
27 | z + dz
28 | } air destroy`
29 | );
30 | history.push(block.position);
31 | await bot.waitForTicks(20);
32 | return;
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/voyager/control_primitives/killMob.js:
--------------------------------------------------------------------------------
1 | async function killMob(bot, mobName, timeout = 300) {
2 | // return if mobName is not string
3 | if (typeof mobName !== "string") {
4 | throw new Error(`mobName for killMob must be a string`);
5 | }
6 | // return if timeout is not number
7 | if (typeof timeout !== "number") {
8 | throw new Error(`timeout for killMob must be a number`);
9 | }
10 |
11 | const weaponsForShooting = [
12 | "bow",
13 | "crossbow",
14 | "snowball",
15 | "ender_pearl",
16 | "egg",
17 | "splash_potion",
18 | "trident",
19 | ];
20 | const mainHandItem = bot.inventory.slots[bot.getEquipmentDestSlot("hand")];
21 |
22 | const entity = bot.nearestEntity(
23 | (entity) =>
24 | entity.name === mobName &&
25 | // kill mob distance should be slightly bigger than explore distance
26 | entity.position.distanceTo(bot.entity.position) < 48
27 | );
28 | if (!entity) {
29 | bot.chat(`No ${mobName} nearby, please explore first`);
30 | _killMobFailCount++;
31 | if (_killMobFailCount > 10) {
32 | throw new Error(
33 | `killMob failed too many times, make sure you explore before calling killMob`
34 | );
35 | }
36 | return;
37 | }
38 |
39 | let droppedItem;
40 | if (mainHandItem && weaponsForShooting.includes(mainHandItem.name)) {
41 | bot.hawkEye.autoAttack(entity, mainHandItem.name);
42 | droppedItem = await waitForMobShot(bot, entity, timeout);
43 | } else {
44 | await bot.pvp.attack(entity);
45 | droppedItem = await waitForMobRemoved(bot, entity, timeout);
46 | }
47 | if (droppedItem) {
48 | await bot.collectBlock.collect(droppedItem, { ignoreNoPath: true });
49 | }
50 | bot.save(`${mobName}_killed`);
51 | }
52 |
--------------------------------------------------------------------------------
/voyager/control_primitives/mineBlock.js:
--------------------------------------------------------------------------------
1 | async function mineBlock(bot, name, count = 1) {
2 | // return if name is not string
3 | if (typeof name !== "string") {
4 | throw new Error(`name for mineBlock must be a string`);
5 | }
6 | if (typeof count !== "number") {
7 | throw new Error(`count for mineBlock must be a number`);
8 | }
9 | const blockByName = mcData.blocksByName[name];
10 | if (!blockByName) {
11 | throw new Error(`No block named ${name}`);
12 | }
13 | const blocks = bot.findBlocks({
14 | matching: [blockByName.id],
15 | maxDistance: 32,
16 | count: 1024,
17 | });
18 | if (blocks.length === 0) {
19 | bot.chat(`No ${name} nearby, please explore first`);
20 | _mineBlockFailCount++;
21 | if (_mineBlockFailCount > 10) {
22 | throw new Error(
23 | "mineBlock failed too many times, make sure you explore before calling mineBlock"
24 | );
25 | }
26 | return;
27 | }
28 | const targets = [];
29 | for (let i = 0; i < blocks.length; i++) {
30 | targets.push(bot.blockAt(blocks[i]));
31 | }
32 | await bot.collectBlock.collect(targets, {
33 | ignoreNoPath: true,
34 | count: count,
35 | });
36 | bot.save(`${name}_mined`);
37 | }
38 |
--------------------------------------------------------------------------------
/voyager/control_primitives/placeItem.js:
--------------------------------------------------------------------------------
1 | async function placeItem(bot, name, position) {
2 | // return if name is not string
3 | if (typeof name !== "string") {
4 | throw new Error(`name for placeItem must be a string`);
5 | }
6 | // return if position is not Vec3
7 | if (!(position instanceof Vec3)) {
8 | throw new Error(`position for placeItem must be a Vec3`);
9 | }
10 | const itemByName = mcData.itemsByName[name];
11 | if (!itemByName) {
12 | throw new Error(`No item named ${name}`);
13 | }
14 | const item = bot.inventory.findInventoryItem(itemByName.id);
15 | if (!item) {
16 | bot.chat(`No ${name} in inventory`);
17 | return;
18 | }
19 | const item_count = item.count;
20 | // find a reference block
21 | const faceVectors = [
22 | new Vec3(0, 1, 0),
23 | new Vec3(0, -1, 0),
24 | new Vec3(1, 0, 0),
25 | new Vec3(-1, 0, 0),
26 | new Vec3(0, 0, 1),
27 | new Vec3(0, 0, -1),
28 | ];
29 | let referenceBlock = null;
30 | let faceVector = null;
31 | for (const vector of faceVectors) {
32 | const block = bot.blockAt(position.minus(vector));
33 | if (block?.name !== "air") {
34 | referenceBlock = block;
35 | faceVector = vector;
36 | bot.chat(`Placing ${name} on ${block.name} at ${block.position}`);
37 | break;
38 | }
39 | }
40 | if (!referenceBlock) {
41 | bot.chat(
42 | `No block to place ${name} on. You cannot place a floating block.`
43 | );
44 | _placeItemFailCount++;
45 | if (_placeItemFailCount > 10) {
46 | throw new Error(
47 | `placeItem failed too many times. You cannot place a floating block.`
48 | );
49 | }
50 | return;
51 | }
52 |
53 | // You must use try catch to placeBlock
54 | try {
55 | // You must first go to the block position you want to place
56 | await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
57 | // You must equip the item right before calling placeBlock
58 | await bot.equip(item, "hand");
59 | await bot.placeBlock(referenceBlock, faceVector);
60 | bot.chat(`Placed ${name}`);
61 | bot.save(`${name}_placed`);
62 | } catch (err) {
63 | const item = bot.inventory.findInventoryItem(itemByName.id);
64 | if (item?.count === item_count) {
65 | bot.chat(
66 | `Error placing ${name}: ${err.message}, please find another position to place`
67 | );
68 | _placeItemFailCount++;
69 | if (_placeItemFailCount > 10) {
70 | throw new Error(
71 | `placeItem failed too many times, please find another position to place.`
72 | );
73 | }
74 | } else {
75 | bot.chat(`Placed ${name}`);
76 | bot.save(`${name}_placed`);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/voyager/control_primitives/shoot.js:
--------------------------------------------------------------------------------
1 | // shoot 1 pig with a bow: shoot(bot, "bow", "pig");
2 | async function shoot(bot, weapon, target) {
3 | const validWeapons = [
4 | "bow",
5 | "crossbow",
6 | "snowball",
7 | "ender_pearl",
8 | "egg",
9 | "splash_potion",
10 | "trident",
11 | ];
12 | if (!validWeapons.includes(weapon)) {
13 | bot.chat(`${weapon} is not a valid weapon for shooting`);
14 | return;
15 | }
16 |
17 | const weaponItem = mcData.itemsByName[weapon];
18 | if (!bot.inventory.findInventoryItem(weaponItem.id, null)) {
19 | bot.chat(`No ${weapon} in inventory for shooting`);
20 | return;
21 | }
22 |
23 | const targetEntity = bot.nearestEntity(
24 | (entity) =>
25 | entity.name === target
26 | );
27 | if (!targetEntity) {
28 | bot.chat(`No ${target} nearby`);
29 | return;
30 | }
31 | bot.hawkEye.autoAttack(targetEntity, "bow");
32 | bot.on('auto_shot_stopped', (target) => {
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/voyager/control_primitives/smeltItem.js:
--------------------------------------------------------------------------------
1 | async function smeltItem(bot, itemName, fuelName, count = 1) {
2 | // return if itemName or fuelName is not string
3 | if (typeof itemName !== "string" || typeof fuelName !== "string") {
4 | throw new Error("itemName or fuelName for smeltItem must be a string");
5 | }
6 | // return if count is not a number
7 | if (typeof count !== "number") {
8 | throw new Error("count for smeltItem must be a number");
9 | }
10 | const item = mcData.itemsByName[itemName];
11 | const fuel = mcData.itemsByName[fuelName];
12 | if (!item) {
13 | throw new Error(`No item named ${itemName}`);
14 | }
15 | if (!fuel) {
16 | throw new Error(`No item named ${fuelName}`);
17 | }
18 | const furnaceBlock = bot.findBlock({
19 | matching: mcData.blocksByName.furnace.id,
20 | maxDistance: 32,
21 | });
22 | if (!furnaceBlock) {
23 | throw new Error("No furnace nearby");
24 | } else {
25 | await bot.pathfinder.goto(
26 | new GoalLookAtBlock(furnaceBlock.position, bot.world)
27 | );
28 | }
29 | const furnace = await bot.openFurnace(furnaceBlock);
30 | let success_count = 0;
31 | for (let i = 0; i < count; i++) {
32 | if (!bot.inventory.findInventoryItem(item.id, null)) {
33 | bot.chat(`No ${itemName} to smelt in inventory`);
34 | break;
35 | }
36 | if (furnace.fuelSeconds < 15 && furnace.fuelItem()?.name !== fuelName) {
37 | if (!bot.inventory.findInventoryItem(fuel.id, null)) {
38 | bot.chat(`No ${fuelName} as fuel in inventory`);
39 | break;
40 | }
41 | await furnace.putFuel(fuel.id, null, 1);
42 | await bot.waitForTicks(20);
43 | if (!furnace.fuel && furnace.fuelItem()?.name !== fuelName) {
44 | throw new Error(`${fuelName} is not a valid fuel`);
45 | }
46 | }
47 | await furnace.putInput(item.id, null, 1);
48 | await bot.waitForTicks(12 * 20);
49 | if (!furnace.outputItem()) {
50 | throw new Error(`${itemName} is not a valid input`);
51 | }
52 | await furnace.takeOutput();
53 | success_count++;
54 | }
55 | furnace.close();
56 | if (success_count > 0) bot.chat(`Smelted ${success_count} ${itemName}.`);
57 | else {
58 | bot.chat(
59 | `Failed to smelt ${itemName}, please check the fuel and input.`
60 | );
61 | _smeltItemFailCount++;
62 | if (_smeltItemFailCount > 10) {
63 | throw new Error(
64 | `smeltItem failed too many times, please check the fuel and input.`
65 | );
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/voyager/control_primitives/useChest.js:
--------------------------------------------------------------------------------
1 | async function getItemFromChest(bot, chestPosition, itemsToGet) {
2 | // return if chestPosition is not Vec3
3 | if (!(chestPosition instanceof Vec3)) {
4 | bot.chat("chestPosition for getItemFromChest must be a Vec3");
5 | return;
6 | }
7 | await moveToChest(bot, chestPosition);
8 | const chestBlock = bot.blockAt(chestPosition);
9 | const chest = await bot.openContainer(chestBlock);
10 | for (const name in itemsToGet) {
11 | const itemByName = mcData.itemsByName[name];
12 | if (!itemByName) {
13 | bot.chat(`No item named ${name}`);
14 | continue;
15 | }
16 |
17 | const item = chest.findContainerItem(itemByName.id);
18 | if (!item) {
19 | bot.chat(`I don't see ${name} in this chest`);
20 | continue;
21 | }
22 | try {
23 | await chest.withdraw(item.type, null, itemsToGet[name]);
24 | } catch (err) {
25 | bot.chat(`Not enough ${name} in chest.`);
26 | }
27 | }
28 | await closeChest(bot, chestBlock);
29 | }
30 |
31 | async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
32 | // return if chestPosition is not Vec3
33 | if (!(chestPosition instanceof Vec3)) {
34 | throw new Error(
35 | "chestPosition for depositItemIntoChest must be a Vec3"
36 | );
37 | }
38 | await moveToChest(bot, chestPosition);
39 | const chestBlock = bot.blockAt(chestPosition);
40 | const chest = await bot.openContainer(chestBlock);
41 | for (const name in itemsToDeposit) {
42 | const itemByName = mcData.itemsByName[name];
43 | if (!itemByName) {
44 | bot.chat(`No item named ${name}`);
45 | continue;
46 | }
47 | const item = bot.inventory.findInventoryItem(itemByName.id);
48 | if (!item) {
49 | bot.chat(`No ${name} in inventory`);
50 | continue;
51 | }
52 | try {
53 | await chest.deposit(item.type, null, itemsToDeposit[name]);
54 | } catch (err) {
55 | bot.chat(`Not enough ${name} in inventory.`);
56 | }
57 | }
58 | await closeChest(bot, chestBlock);
59 | }
60 |
61 | async function checkItemInsideChest(bot, chestPosition) {
62 | // return if chestPosition is not Vec3
63 | if (!(chestPosition instanceof Vec3)) {
64 | throw new Error(
65 | "chestPosition for depositItemIntoChest must be a Vec3"
66 | );
67 | }
68 | await moveToChest(bot, chestPosition);
69 | const chestBlock = bot.blockAt(chestPosition);
70 | await bot.openContainer(chestBlock);
71 | await closeChest(bot, chestBlock);
72 | }
73 |
74 | async function moveToChest(bot, chestPosition) {
75 | if (!(chestPosition instanceof Vec3)) {
76 | throw new Error(
77 | "chestPosition for depositItemIntoChest must be a Vec3"
78 | );
79 | }
80 | if (chestPosition.distanceTo(bot.entity.position) > 32) {
81 | bot.chat(
82 | `/tp ${chestPosition.x} ${chestPosition.y} ${chestPosition.z}`
83 | );
84 | await bot.waitForTicks(20);
85 | }
86 | const chestBlock = bot.blockAt(chestPosition);
87 | if (chestBlock.name !== "chest" || chestBlock.name !== "chest_minecart") {
88 | bot.emit("removeChest", chestPosition);
89 | throw new Error(
90 | `No chest at ${chestPosition}, it is ${chestBlock.name}`
91 | );
92 | }
93 | await bot.pathfinder.goto(
94 | new GoalLookAtBlock(chestBlock.position, bot.world, {})
95 | );
96 | return chestBlock;
97 | }
98 |
99 | async function listItemsInChest(bot, chestBlock) {
100 | const chest = await bot.openContainer(chestBlock);
101 | const items = chest.containerItems();
102 | if (items.length > 0) {
103 | const itemNames = items.reduce((acc, obj) => {
104 | if (acc[obj.name]) {
105 | acc[obj.name] += obj.count;
106 | } else {
107 | acc[obj.name] = obj.count;
108 | }
109 | return acc;
110 | }, {});
111 | bot.emit("closeChest", itemNames, chestBlock.position);
112 | } else {
113 | bot.emit("closeChest", {}, chestBlock.position);
114 | }
115 | return chest;
116 | }
117 |
118 | async function closeChest(bot, chestBlock) {
119 | try {
120 | const chest = await listItemsInChest(bot, chestBlock);
121 | await chest.close();
122 | } catch (err) {
123 | await bot.closeWindow(chestBlock);
124 | }
125 | }
126 |
127 | function itemByName(items, name) {
128 | for (let i = 0; i < items.length; ++i) {
129 | const item = items[i];
130 | if (item && item.name === name) return item;
131 | }
132 | return null;
133 | }
134 | //
135 | // function itemToString(item) {
136 | // return item && `${item.count} ${item.name}`;
137 | // }
138 |
--------------------------------------------------------------------------------
/voyager/control_primitives/waitForMobRemoved.js:
--------------------------------------------------------------------------------
1 | function waitForMobRemoved(bot, entity, timeout = 300) {
2 | return new Promise((resolve, reject) => {
3 | let success = false;
4 | let droppedItem = null;
5 | // Set up timeout
6 | const timeoutId = setTimeout(() => {
7 | success = false;
8 | bot.pvp.stop();
9 | }, timeout * 1000);
10 |
11 | // Function to handle entityRemoved event
12 | function onEntityGone(e) {
13 | if (e === entity) {
14 | success = true;
15 | clearTimeout(timeoutId);
16 | bot.chat(`Killed ${entity.name}!`);
17 | bot.pvp.stop();
18 | }
19 | }
20 |
21 | function onItemDrop(item) {
22 | if (entity.position.distanceTo(item.position) <= 1) {
23 | droppedItem = item;
24 | }
25 | }
26 |
27 | function onStoppedAttacking() {
28 | clearTimeout(timeoutId);
29 | bot.removeListener("entityGone", onEntityGone);
30 | bot.removeListener("stoppedAttacking", onStoppedAttacking);
31 | bot.removeListener("itemDrop", onItemDrop);
32 | if (!success) reject(new Error(`Failed to kill ${entity.name}.`));
33 | else resolve(droppedItem);
34 | }
35 |
36 | // Listen for entityRemoved event
37 | bot.on("entityGone", onEntityGone);
38 | bot.on("stoppedAttacking", onStoppedAttacking);
39 | bot.on("itemDrop", onItemDrop);
40 | });
41 | }
42 |
43 |
44 | function waitForMobShot(bot, entity, timeout = 300) {
45 | return new Promise((resolve, reject) => {
46 | let success = false;
47 | let droppedItem = null;
48 | // Set up timeout
49 | const timeoutId = setTimeout(() => {
50 | success = false;
51 | bot.hawkEye.stop();
52 | }, timeout * 1000);
53 |
54 | // Function to handle entityRemoved event
55 | function onEntityGone(e) {
56 | if (e === entity) {
57 | success = true;
58 | clearTimeout(timeoutId);
59 | bot.chat(`Shot ${entity.name}!`);
60 | bot.hawkEye.stop();
61 | }
62 | }
63 |
64 | function onItemDrop(item) {
65 | if (entity.position.distanceTo(item.position) <= 1) {
66 | droppedItem = item;
67 | }
68 | }
69 |
70 | function onAutoShotStopped() {
71 | clearTimeout(timeoutId);
72 | bot.removeListener("entityGone", onEntityGone);
73 | bot.removeListener("auto_shot_stopped", onAutoShotStopped);
74 | bot.removeListener("itemDrop", onItemDrop);
75 | if (!success) reject(new Error(`Failed to shoot ${entity.name}.`));
76 | else resolve(droppedItem);
77 | }
78 |
79 | // Listen for entityRemoved event
80 | bot.on("entityGone", onEntityGone);
81 | bot.on("auto_shot_stopped", onAutoShotStopped);
82 | bot.on("itemDrop", onItemDrop);
83 | });
84 | }
85 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
4 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/__init__.py:
--------------------------------------------------------------------------------
1 | import pkg_resources
2 | import os
3 | import voyager.utils as U
4 |
5 |
6 | def load_control_primitives_context(primitive_names=None):
7 | package_path = pkg_resources.resource_filename("voyager", "")
8 | if primitive_names is None:
9 | primitive_names = [
10 | primitive[:-3]
11 | for primitive in os.listdir(f"{package_path}/control_primitives_context")
12 | if primitive.endswith(".js")
13 | ]
14 | primitives = [
15 | U.load_text(f"{package_path}/control_primitives_context/{primitive_name}.js")
16 | for primitive_name in primitive_names
17 | ]
18 | return primitives
19 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/craftItem.js:
--------------------------------------------------------------------------------
1 | // Craft 8 oak_planks from 2 oak_log (do the recipe 2 times): craftItem(bot, "oak_planks", 2);
2 | // You must place a crafting table before calling this function
3 | async function craftItem(bot, name, count = 1) {
4 | const item = mcData.itemsByName[name];
5 | const craftingTable = bot.findBlock({
6 | matching: mcData.blocksByName.crafting_table.id,
7 | maxDistance: 32,
8 | });
9 | await bot.pathfinder.goto(
10 | new GoalLookAtBlock(craftingTable.position, bot.world)
11 | );
12 | const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0];
13 | await bot.craft(recipe, count, craftingTable);
14 | }
15 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/exploreUntil.js:
--------------------------------------------------------------------------------
1 | /*
2 | Explore until find an iron_ore, use Vec3(0, -1, 0) because iron ores are usually underground
3 | await exploreUntil(bot, new Vec3(0, -1, 0), 60, () => {
4 | const iron_ore = bot.findBlock({
5 | matching: mcData.blocksByName["iron_ore"].id,
6 | maxDistance: 32,
7 | });
8 | return iron_ore;
9 | });
10 |
11 | Explore until find a pig, use Vec3(1, 0, 1) because pigs are usually on the surface
12 | let pig = await exploreUntil(bot, new Vec3(1, 0, 1), 60, () => {
13 | const pig = bot.nearestEntity((entity) => {
14 | return (
15 | entity.name === "pig" &&
16 | entity.position.distanceTo(bot.entity.position) < 32
17 | );
18 | });
19 | return pig;
20 | });
21 | */
22 | async function exploreUntil(bot, direction, maxTime = 60, callback) {
23 | /*
24 | Implementation of this function is omitted.
25 | direction: Vec3, can only contain value of -1, 0 or 1
26 | maxTime: number, the max time for exploration
27 | callback: function, early stop condition, will be called each second, exploration will stop if return value is not null
28 |
29 | Return: null if explore timeout, otherwise return the return value of callback
30 | */
31 | }
32 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/killMob.js:
--------------------------------------------------------------------------------
1 | // Kill a pig and collect the dropped item: killMob(bot, "pig", 300);
2 | async function killMob(bot, mobName, timeout = 300) {
3 | const entity = bot.nearestEntity(
4 | (entity) =>
5 | entity.name === mobName &&
6 | entity.position.distanceTo(bot.entity.position) < 32
7 | );
8 | await bot.pvp.attack(entity);
9 | await bot.pathfinder.goto(
10 | new GoalBlock(entity.position.x, entity.position.y, entity.position.z)
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/mineBlock.js:
--------------------------------------------------------------------------------
1 | // Mine 3 cobblestone: mineBlock(bot, "stone", 3);
2 | async function mineBlock(bot, name, count = 1) {
3 | const blocks = bot.findBlocks({
4 | matching: (block) => {
5 | return block.name === name;
6 | },
7 | maxDistance: 32,
8 | count: count,
9 | });
10 | const targets = [];
11 | for (let i = 0; i < Math.min(blocks.length, count); i++) {
12 | targets.push(bot.blockAt(blocks[i]));
13 | }
14 | await bot.collectBlock.collect(targets, { ignoreNoPath: true });
15 | }
16 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/mineflayer.js:
--------------------------------------------------------------------------------
1 | await bot.pathfinder.goto(goal); // A very useful function. This function may change your main-hand equipment.
2 | // Following are some Goals you can use:
3 | new GoalNear(x, y, z, range); // Move the bot to a block within the specified range of the specified block. `x`, `y`, `z`, and `range` are `number`
4 | new GoalXZ(x, z); // Useful for long-range goals that don't have a specific Y level. `x` and `z` are `number`
5 | new GoalGetToBlock(x, y, z); // Not get into the block, but get directly adjacent to it. Useful for fishing, farming, filling bucket, and beds. `x`, `y`, and `z` are `number`
6 | new GoalFollow(entity, range); // Follow the specified entity within the specified range. `entity` is `Entity`, `range` is `number`
7 | new GoalPlaceBlock(position, bot.world, {}); // Position the bot in order to place a block. `position` is `Vec3`
8 | new GoalLookAtBlock(position, bot.world, {}); // Path into a position where a blockface of the block at position is visible. `position` is `Vec3`
9 |
10 | // These are other Mineflayer functions you can use:
11 | bot.isABed(bedBlock); // Return true if `bedBlock` is a bed
12 | bot.blockAt(position); // Return the block at `position`. `position` is `Vec3`
13 |
14 | // These are other Mineflayer async functions you can use:
15 | await bot.equip(item, destination); // Equip the item in the specified destination. `item` is `Item`, `destination` can only be "hand", "head", "torso", "legs", "feet", "off-hand"
16 | await bot.consume(); // Consume the item in the bot's hand. You must equip the item to consume first. Useful for eating food, drinking potions, etc.
17 | await bot.fish(); // Let bot fish. Before calling this function, you must first get to a water block and then equip a fishing rod. The bot will automatically stop fishing when it catches a fish
18 | await bot.sleep(bedBlock); // Sleep until sunrise. You must get to a bed block first
19 | await bot.activateBlock(block); // This is the same as right-clicking a block in the game. Useful for buttons, doors, etc. You must get to the block first
20 | await bot.lookAt(position); // Look at the specified position. You must go near the position before you look at it. To fill bucket with water, you must lookAt first. `position` is `Vec3`
21 | await bot.activateItem(); // This is the same as right-clicking to use the item in the bot's hand. Useful for using buckets, etc. You must equip the item to activate first
22 | await bot.useOn(entity); // This is the same as right-clicking an entity in the game. Useful for shearing sheep, equipping harnesses, etc. You must get to the entity first
23 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/placeItem.js:
--------------------------------------------------------------------------------
1 | // Place a crafting_table near the player, Vec3(1, 0, 0) is just an example, you shouldn't always use that: placeItem(bot, "crafting_table", bot.entity.position.offset(1, 0, 0));
2 | async function placeItem(bot, name, position) {
3 | const item = bot.inventory.findInventoryItem(mcData.itemsByName[name].id);
4 | // find a reference block
5 | const faceVectors = [
6 | new Vec3(0, 1, 0),
7 | new Vec3(0, -1, 0),
8 | new Vec3(1, 0, 0),
9 | new Vec3(-1, 0, 0),
10 | new Vec3(0, 0, 1),
11 | new Vec3(0, 0, -1),
12 | ];
13 | let referenceBlock = null;
14 | let faceVector = null;
15 | for (const vector of faceVectors) {
16 | const block = bot.blockAt(position.minus(vector));
17 | if (block?.name !== "air") {
18 | referenceBlock = block;
19 | faceVector = vector;
20 | break;
21 | }
22 | }
23 | // You must first go to the block position you want to place
24 | await bot.pathfinder.goto(new GoalPlaceBlock(position, bot.world, {}));
25 | // You must equip the item right before calling placeBlock
26 | await bot.equip(item, "hand");
27 | await bot.placeBlock(referenceBlock, faceVector);
28 | }
29 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/smeltItem.js:
--------------------------------------------------------------------------------
1 | // Smelt 1 raw_iron into 1 iron_ingot using 1 oak_planks as fuel: smeltItem(bot, "raw_iron", "oak_planks");
2 | // You must place a furnace before calling this function
3 | async function smeltItem(bot, itemName, fuelName, count = 1) {
4 | const item = mcData.itemsByName[itemName];
5 | const fuel = mcData.itemsByName[fuelName];
6 | const furnaceBlock = bot.findBlock({
7 | matching: mcData.blocksByName.furnace.id,
8 | maxDistance: 32,
9 | });
10 | await bot.pathfinder.goto(
11 | new GoalLookAtBlock(furnaceBlock.position, bot.world)
12 | );
13 | const furnace = await bot.openFurnace(furnaceBlock);
14 | for (let i = 0; i < count; i++) {
15 | await furnace.putFuel(fuel.id, null, 1);
16 | await furnace.putInput(item.id, null, 1);
17 | // Wait 12 seconds for the furnace to smelt the item
18 | await bot.waitForTicks(12 * 20);
19 | await furnace.takeOutput();
20 | }
21 | await furnace.close();
22 | }
23 |
--------------------------------------------------------------------------------
/voyager/control_primitives_context/useChest.js:
--------------------------------------------------------------------------------
1 | // Get a torch from chest at (30, 65, 100): getItemFromChest(bot, new Vec3(30, 65, 100), {"torch": 1});
2 | // This function will work no matter how far the bot is from the chest.
3 | async function getItemFromChest(bot, chestPosition, itemsToGet) {
4 | await moveToChest(bot, chestPosition);
5 | const chestBlock = bot.blockAt(chestPosition);
6 | const chest = await bot.openContainer(chestBlock);
7 | for (const name in itemsToGet) {
8 | const itemByName = mcData.itemsByName[name];
9 | const item = chest.findContainerItem(itemByName.id);
10 | await chest.withdraw(item.type, null, itemsToGet[name]);
11 | }
12 | await closeChest(bot, chestBlock);
13 | }
14 | // Deposit a torch into chest at (30, 65, 100): depositItemIntoChest(bot, new Vec3(30, 65, 100), {"torch": 1});
15 | // This function will work no matter how far the bot is from the chest.
16 | async function depositItemIntoChest(bot, chestPosition, itemsToDeposit) {
17 | await moveToChest(bot, chestPosition);
18 | const chestBlock = bot.blockAt(chestPosition);
19 | const chest = await bot.openContainer(chestBlock);
20 | for (const name in itemsToDeposit) {
21 | const itemByName = mcData.itemsByName[name];
22 | const item = bot.inventory.findInventoryItem(itemByName.id);
23 | await chest.deposit(item.type, null, itemsToDeposit[name]);
24 | }
25 | await closeChest(bot, chestBlock);
26 | }
27 | // Check the items inside the chest at (30, 65, 100): checkItemInsideChest(bot, new Vec3(30, 65, 100));
28 | // You only need to call this function once without any action to finish task of checking items inside the chest.
29 | async function checkItemInsideChest(bot, chestPosition) {
30 | await moveToChest(bot, chestPosition);
31 | const chestBlock = bot.blockAt(chestPosition);
32 | await bot.openContainer(chestBlock);
33 | // You must close the chest after opening it if you are asked to open a chest
34 | await closeChest(bot, chestBlock);
35 | }
36 |
--------------------------------------------------------------------------------
/voyager/env/.gitignore:
--------------------------------------------------------------------------------
1 | # MCP-Reborn
2 | MCP-Reborn/
3 | run/
4 | *.jar
5 | config.json
6 |
7 | # Byte-compiled / optimized / DLL files
8 | __pycache__/
9 | *.py[cod]
10 | *$py.class
11 |
12 | # C extensions
13 | *.so
14 |
15 | # Distribution / packaging
16 | .Python
17 | build/
18 | develop-eggs/
19 | dist/
20 | downloads/
21 | eggs/
22 | .eggs/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | share/python-wheels/
29 | *.egg-info/
30 | .installed.cfg
31 | *.egg
32 | MANIFEST
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | *.py,cover
55 | .hypothesis/
56 | .pytest_cache/
57 | cover/
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 | local_settings.py
66 | db.sqlite3
67 | db.sqlite3-journal
68 |
69 | # Flask stuff:
70 | instance/
71 | .webassets-cache
72 |
73 | # Scrapy stuff:
74 | .scrapy
75 |
76 | # Sphinx documentation
77 | docs/_build/
78 |
79 | # PyBuilder
80 | .pybuilder/
81 | target/
82 |
83 | # Jupyter Notebook
84 | .ipynb_checkpoints
85 |
86 | # IPython
87 | profile_default/
88 | ipython_config.py
89 |
90 | # pyenv
91 | # For a library or package, you might want to ignore these files since the code is
92 | # intended to run in multiple environments; otherwise, check them in:
93 | # .python-version
94 |
95 | # pipenv
96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
99 | # install all needed dependencies.
100 | #Pipfile.lock
101 |
102 | # poetry
103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
104 | # This is especially recommended for binary packages to ensure reproducibility, and is more
105 | # commonly ignored for libraries.
106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
107 | #poetry.lock
108 |
109 | # pdm
110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
111 | #pdm.lock
112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
113 | # in version control.
114 | # https://pdm.fming.dev/#use-with-ide
115 | .pdm.toml
116 |
117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
118 | __pypackages__/
119 |
120 | # Celery stuff
121 | celerybeat-schedule
122 | celerybeat.pid
123 |
124 | # SageMath parsed files
125 | *.sage.py
126 |
127 | # Environments
128 | .env
129 | .venv
130 | env/
131 | venv/
132 | ENV/
133 | env.bak/
134 | venv.bak/
135 |
136 | # Spyder project settings
137 | .spyderproject
138 | .spyproject
139 |
140 | # Rope project settings
141 | .ropeproject
142 |
143 | # mkdocs documentation
144 | /site
145 |
146 | # mypy
147 | .mypy_cache/
148 | .dmypy.json
149 | dmypy.json
150 |
151 | # Pyre type checker
152 | .pyre/
153 |
154 | # pytype static type analyzer
155 | .pytype/
156 |
157 | # Cython debug symbols
158 | cython_debug/
159 |
160 | # PyCharm
161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
163 | # and can be added to the global gitignore or merged into this file. For a more nuclear
164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
165 | .idea/
166 |
167 | # Logs
168 | logs
169 | npm-debug.log*
170 | yarn-debug.log*
171 | yarn-error.log*
172 | lerna-debug.log*
173 | .pnpm-debug.log*
174 |
175 | # Diagnostic reports (https://nodejs.org/api/report.html)
176 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
177 |
178 | # Runtime data
179 | pids
180 | *.pid
181 | *.seed
182 | *.pid.lock
183 |
184 | # Directory for instrumented libs generated by jscoverage/JSCover
185 | lib-cov
186 |
187 | # Coverage directory used by tools like istanbul
188 | coverage
189 | *.lcov
190 |
191 | # nyc test coverage
192 | .nyc_output
193 |
194 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
195 | .grunt
196 |
197 | # Bower dependency directory (https://bower.io/)
198 | bower_components
199 |
200 | # node-waf configuration
201 | .lock-wscript
202 |
203 | # Compiled binary addons (https://nodejs.org/api/addons.html)
204 | build/Release
205 |
206 | # Dependency directories
207 | node_modules/
208 | jspm_packages/
209 |
210 | # Snowpack dependency directory (https://snowpack.dev/)
211 | web_modules/
212 |
213 | # TypeScript cache
214 | *.tsbuildinfo
215 |
216 | # Optional npm cache directory
217 | .npm
218 |
219 | # Optional eslint cache
220 | .eslintcache
221 |
222 | # Optional stylelint cache
223 | .stylelintcache
224 |
225 | # Microbundle cache
226 | .rpt2_cache/
227 | .rts2_cache_cjs/
228 | .rts2_cache_es/
229 | .rts2_cache_umd/
230 |
231 | # Optional REPL history
232 | .node_repl_history
233 |
234 | # Output of 'npm pack'
235 | *.tgz
236 |
237 | # Yarn Integrity file
238 | .yarn-integrity
239 |
240 | # dotenv environment variable files
241 | .env.development.local
242 | .env.test.local
243 | .env.production.local
244 | .env.local
245 |
246 | # parcel-bundler cache (https://parceljs.org/)
247 | .parcel-cache
248 |
249 | # Next.js build output
250 | .next
251 | out
252 |
253 | # Nuxt.js build / generate output
254 | .nuxt
255 | dist
256 |
257 | # Gatsby files
258 | .cache/
259 | # Comment in the public line in if your project uses Gatsby and not Next.js
260 | # https://nextjs.org/blog/next-9-1#public-directory-support
261 | # public
262 |
263 | # vuepress build output
264 | .vuepress/dist
265 |
266 | # vuepress v2.x temp and cache directory
267 | .temp
268 |
269 | # Docusaurus cache and generated files
270 | .docusaurus
271 |
272 | # Serverless directories
273 | .serverless/
274 |
275 | # FuseBox cache
276 | .fusebox/
277 |
278 | # DynamoDB Local files
279 | .dynamodb/
280 |
281 | # TernJS port file
282 | .tern-port
283 |
284 | # Stores VSCode versions used for testing VSCode extensions
285 | .vscode-test
286 |
287 | # yarn v2
288 | .yarn/cache
289 | .yarn/unplugged
290 | .yarn/build-state.yml
291 | .yarn/install-state.gz
292 | .pnp.*
293 |
294 | package-lock.json
--------------------------------------------------------------------------------
/voyager/env/__init__.py:
--------------------------------------------------------------------------------
1 | from .bridge import VoyagerEnv
2 |
--------------------------------------------------------------------------------
/voyager/env/bridge.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import time
3 | import warnings
4 | from typing import SupportsFloat, Any, Tuple, Dict
5 |
6 | import requests
7 | import json
8 |
9 | import gymnasium as gym
10 | from gymnasium.core import ObsType
11 |
12 | import voyager.utils as U
13 |
14 | from .minecraft_launcher import MinecraftInstance
15 | from .process_monitor import SubprocessMonitor
16 |
17 |
18 | class VoyagerEnv(gym.Env):
19 | def __init__(
20 | self,
21 | mc_port=None,
22 | azure_login=None,
23 | server_host="http://127.0.0.1",
24 | server_port=3000,
25 | request_timeout=600,
26 | log_path="./logs",
27 | ):
28 | if not mc_port and not azure_login:
29 | raise ValueError("Either mc_port or azure_login must be specified")
30 | if mc_port and azure_login:
31 | warnings.warn(
32 | "Both mc_port and mc_login are specified, mc_port will be ignored"
33 | )
34 | self.mc_port = mc_port
35 | self.azure_login = azure_login
36 | self.server = f"{server_host}:{server_port}"
37 | self.server_port = server_port
38 | self.request_timeout = request_timeout
39 | self.log_path = log_path
40 | self.mineflayer = self.get_mineflayer_process(server_port)
41 | if azure_login:
42 | self.mc_instance = self.get_mc_instance()
43 | else:
44 | self.mc_instance = None
45 | self.has_reset = False
46 | self.reset_options = None
47 | self.connected = False
48 | self.server_paused = False
49 |
50 | def get_mineflayer_process(self, server_port):
51 | U.f_mkdir(self.log_path, "mineflayer")
52 | file_path = os.path.abspath(os.path.dirname(__file__))
53 | return SubprocessMonitor(
54 | commands=[
55 | "node",
56 | U.f_join(file_path, "mineflayer/index.js"),
57 | str(server_port),
58 | ],
59 | name="mineflayer",
60 | ready_match=r"Server started on port (\d+)",
61 | log_path=U.f_join(self.log_path, "mineflayer"),
62 | )
63 |
64 | def get_mc_instance(self):
65 | print("Creating Minecraft server")
66 | U.f_mkdir(self.log_path, "minecraft")
67 | return MinecraftInstance(
68 | **self.azure_login,
69 | mineflayer=self.mineflayer,
70 | log_path=U.f_join(self.log_path, "minecraft"),
71 | )
72 |
73 | def check_process(self):
74 | if self.mc_instance and not self.mc_instance.is_running:
75 | # if self.mc_instance:
76 | # self.mc_instance.check_process()
77 | # if not self.mc_instance.is_running:
78 | print("Starting Minecraft server")
79 | self.mc_instance.run()
80 | self.mc_port = self.mc_instance.port
81 | self.reset_options["port"] = self.mc_instance.port
82 | print(f"Server started on port {self.reset_options['port']}")
83 | retry = 0
84 | while not self.mineflayer.is_running:
85 | print("Mineflayer process has exited, restarting")
86 | self.mineflayer.run()
87 | if not self.mineflayer.is_running:
88 | if retry > 3:
89 | raise RuntimeError("Mineflayer process failed to start")
90 | else:
91 | continue
92 | print(self.mineflayer.ready_line)
93 | res = requests.post(
94 | f"{self.server}/start",
95 | json=self.reset_options,
96 | timeout=self.request_timeout,
97 | )
98 | if res.status_code != 200:
99 | self.mineflayer.stop()
100 | raise RuntimeError(
101 | f"Minecraft server reply with code {res.status_code}"
102 | )
103 | return res.json()
104 |
105 | def step(
106 | self,
107 | code: str,
108 | programs: str = "",
109 | ) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]:
110 | if not self.has_reset:
111 | raise RuntimeError("Environment has not been reset yet")
112 | self.check_process()
113 | self.unpause()
114 | data = {
115 | "code": code,
116 | "programs": programs,
117 | }
118 | res = requests.post(
119 | f"{self.server}/step", json=data, timeout=self.request_timeout
120 | )
121 | if res.status_code != 200:
122 | raise RuntimeError("Failed to step Minecraft server")
123 | returned_data = res.json()
124 | self.pause()
125 | return json.loads(returned_data)
126 |
127 | def render(self):
128 | raise NotImplementedError("render is not implemented")
129 |
130 | def reset(
131 | self,
132 | *,
133 | seed=None,
134 | options=None,
135 | ) -> Tuple[ObsType, Dict[str, Any]]:
136 | if options is None:
137 | options = {}
138 |
139 | if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
140 | raise RuntimeError("inventory can only be set when options is hard")
141 |
142 | self.reset_options = {
143 | "port": self.mc_port,
144 | "reset": options.get("mode", "hard"),
145 | "inventory": options.get("inventory", {}),
146 | "equipment": options.get("equipment", []),
147 | "spread": options.get("spread", False),
148 | "waitTicks": options.get("wait_ticks", 5),
149 | "position": options.get("position", None),
150 | }
151 |
152 | self.unpause()
153 | self.mineflayer.stop()
154 | time.sleep(1) # wait for mineflayer to exit
155 |
156 | returned_data = self.check_process()
157 | self.has_reset = True
158 | self.connected = True
159 | # All the reset in step will be soft
160 | self.reset_options["reset"] = "soft"
161 | self.pause()
162 | return json.loads(returned_data)
163 |
164 | def close(self):
165 | self.unpause()
166 | if self.connected:
167 | res = requests.post(f"{self.server}/stop")
168 | if res.status_code == 200:
169 | self.connected = False
170 | if self.mc_instance:
171 | self.mc_instance.stop()
172 | self.mineflayer.stop()
173 | return not self.connected
174 |
175 | def pause(self):
176 | if self.mineflayer.is_running and not self.server_paused:
177 | res = requests.post(f"{self.server}/pause")
178 | if res.status_code == 200:
179 | self.server_paused = True
180 | return self.server_paused
181 |
182 | def unpause(self):
183 | if self.mineflayer.is_running and self.server_paused:
184 | res = requests.post(f"{self.server}/pause")
185 | if res.status_code == 200:
186 | self.server_paused = False
187 | else:
188 | print(res.json())
189 | return self.server_paused
190 |
--------------------------------------------------------------------------------
/voyager/env/minecraft_launcher.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | import minecraft_launcher_lib
5 | import sys
6 | import voyager.utils as U
7 |
8 | from .process_monitor import SubprocessMonitor
9 |
10 |
11 | class MinecraftInstance:
12 | def __init__(
13 | self,
14 | client_id,
15 | redirect_url,
16 | secret_value,
17 | version,
18 | mineflayer,
19 | log_path="logs",
20 | ):
21 | self.client_id = client_id
22 | self.redirect_url = redirect_url
23 | self.secret_value = secret_value
24 | self.version = version
25 | self.log_path = log_path
26 | self.mc_dir = minecraft_launcher_lib.utils.get_minecraft_directory()
27 | self.port = None
28 |
29 | def stop_mineflayer():
30 | print("Stopping mineflayer")
31 | try:
32 | mineflayer.stop()
33 | except Exception as e:
34 | print(e)
35 |
36 | self.mc_command = self.get_mc_command()
37 | self.mc_process = SubprocessMonitor(
38 | commands=self.mc_command,
39 | name="minecraft",
40 | ready_match=r"Started serving on (\d+)",
41 | log_path=self.log_path,
42 | callback=stop_mineflayer,
43 | callback_match=r"\[Server thread/INFO\]: bot left the game",
44 | finished_callback=stop_mineflayer,
45 | )
46 |
47 | def get_mineflayer_process(self, server_port):
48 | U.f_mkdir(self.log_path, "../mineflayer")
49 | file_path = os.path.abspath(os.path.dirname(__file__))
50 | return SubprocessMonitor(
51 | commands=[
52 | "node",
53 | U.f_join(file_path, "mineflayer/index.js"),
54 | str(server_port),
55 | ],
56 | name="mineflayer",
57 | ready_match=r"Server started on port (\d+)",
58 | log_path=U.f_join(self.log_path, "mineflayer"),
59 | )
60 |
61 | def get_mc_command(self):
62 | file_path = os.path.abspath(os.path.dirname(__file__))
63 | if not U.f_exists(file_path, "config.json"):
64 | (
65 | login_url,
66 | state,
67 | code_verifier,
68 | ) = minecraft_launcher_lib.microsoft_account.get_secure_login_data(
69 | self.client_id, self.redirect_url
70 | )
71 | print(
72 | f"Please open {login_url} in your browser and copy the url you are redirected into the prompt below."
73 | )
74 | code_url = input()
75 |
76 | try:
77 | auth_code = (
78 | minecraft_launcher_lib.microsoft_account.parse_auth_code_url(
79 | code_url, state
80 | )
81 | )
82 | except AssertionError:
83 | print("States do not match!")
84 | sys.exit(1)
85 | except KeyError:
86 | print("Url not valid")
87 | sys.exit(1)
88 |
89 | login_data = minecraft_launcher_lib.microsoft_account.complete_login(
90 | self.client_id,
91 | self.secret_value,
92 | self.redirect_url,
93 | auth_code,
94 | code_verifier,
95 | )
96 |
97 | options = {
98 | "username": login_data["name"],
99 | "uuid": login_data["id"],
100 | "token": login_data["access_token"],
101 | }
102 | U.json_dump(options, file_path, "config.json")
103 | print(f"Login success, save to {U.f_join(file_path, 'config.json')}")
104 |
105 | options = U.json_load(file_path, "config.json")
106 | mc_command = minecraft_launcher_lib.command.get_minecraft_command(
107 | self.version, self.mc_dir, options
108 | )
109 |
110 | return mc_command
111 |
112 | def run(self):
113 | self.mc_process.run()
114 | pattern = r"Started serving on (\d+)"
115 | match = re.search(pattern, self.mc_process.ready_line)
116 | if match:
117 | self.port = int(match.group(1))
118 | print("The mc server is listening on port", self.port)
119 | else:
120 | raise RuntimeError("Port not found")
121 |
122 | def stop(self):
123 | self.mc_process.stop()
124 |
125 | @property
126 | def is_running(self):
127 | return self.mc_process.is_running
128 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
--------------------------------------------------------------------------------
/voyager/env/mineflayer/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
4 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const mineflayer = require("mineflayer");
5 |
6 | const skills = require("./lib/skillLoader");
7 | const { initCounter, getNextTime } = require("./lib/utils");
8 | const obs = require("./lib/observation/base");
9 | const OnChat = require("./lib/observation/onChat");
10 | const OnError = require("./lib/observation/onError");
11 | const { Voxels, BlockRecords } = require("./lib/observation/voxels");
12 | const Status = require("./lib/observation/status");
13 | const Inventory = require("./lib/observation/inventory");
14 | const OnSave = require("./lib/observation/onSave");
15 | const Chests = require("./lib/observation/chests");
16 | const { plugin: tool } = require("mineflayer-tool");
17 |
18 | let bot = null;
19 |
20 | const app = express();
21 |
22 | app.use(bodyParser.json({ limit: "50mb" }));
23 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: false }));
24 |
25 | app.post("/start", (req, res) => {
26 | if (bot) onDisconnect("Restarting bot");
27 | bot = null;
28 | console.log(req.body);
29 | bot = mineflayer.createBot({
30 | host: "localhost", // minecraft server ip
31 | port: req.body.port, // minecraft server port
32 | username: "bot",
33 | disableChatSigning: true,
34 | checkTimeoutInterval: 60 * 60 * 1000,
35 | });
36 | bot.once("error", onConnectionFailed);
37 |
38 | // Event subscriptions
39 | bot.waitTicks = req.body.waitTicks;
40 | bot.globalTickCounter = 0;
41 | bot.stuckTickCounter = 0;
42 | bot.stuckPosList = [];
43 | bot.iron_pickaxe = false;
44 |
45 | bot.on("kicked", onDisconnect);
46 |
47 | // mounting will cause physicsTick to stop
48 | bot.on("mount", () => {
49 | bot.dismount();
50 | });
51 |
52 | bot.once("spawn", async () => {
53 | bot.removeListener("error", onConnectionFailed);
54 | let itemTicks = 1;
55 | if (req.body.reset === "hard") {
56 | bot.chat("/clear @s");
57 | bot.chat("/kill @s");
58 | const inventory = req.body.inventory ? req.body.inventory : {};
59 | const equipment = req.body.equipment
60 | ? req.body.equipment
61 | : [null, null, null, null, null, null];
62 | for (let key in inventory) {
63 | bot.chat(`/give @s minecraft:${key} ${inventory[key]}`);
64 | itemTicks += 1;
65 | }
66 | const equipmentNames = [
67 | "armor.head",
68 | "armor.chest",
69 | "armor.legs",
70 | "armor.feet",
71 | "weapon.mainhand",
72 | "weapon.offhand",
73 | ];
74 | for (let i = 0; i < 6; i++) {
75 | if (i === 4) continue;
76 | if (equipment[i]) {
77 | bot.chat(
78 | `/item replace entity @s ${equipmentNames[i]} with minecraft:${equipment[i]}`
79 | );
80 | itemTicks += 1;
81 | }
82 | }
83 | }
84 |
85 | if (req.body.position) {
86 | bot.chat(
87 | `/tp @s ${req.body.position.x} ${req.body.position.y} ${req.body.position.z}`
88 | );
89 | }
90 |
91 | // if iron_pickaxe is in bot's inventory
92 | if (
93 | bot.inventory.items().find((item) => item.name === "iron_pickaxe")
94 | ) {
95 | bot.iron_pickaxe = true;
96 | }
97 |
98 | const { pathfinder } = require("mineflayer-pathfinder");
99 | const tool = require("mineflayer-tool").plugin;
100 | const collectBlock = require("mineflayer-collectblock").plugin;
101 | const pvp = require("mineflayer-pvp").plugin;
102 | const minecraftHawkEye = require("minecrafthawkeye");
103 | bot.loadPlugin(pathfinder);
104 | bot.loadPlugin(tool);
105 | bot.loadPlugin(collectBlock);
106 | bot.loadPlugin(pvp);
107 | bot.loadPlugin(minecraftHawkEye);
108 |
109 | // bot.collectBlock.movements.digCost = 0;
110 | // bot.collectBlock.movements.placeCost = 0;
111 |
112 | obs.inject(bot, [
113 | OnChat,
114 | OnError,
115 | Voxels,
116 | Status,
117 | Inventory,
118 | OnSave,
119 | Chests,
120 | BlockRecords,
121 | ]);
122 | skills.inject(bot);
123 |
124 | if (req.body.spread) {
125 | bot.chat(`/spreadplayers ~ ~ 0 300 under 80 false @s`);
126 | await bot.waitForTicks(bot.waitTicks);
127 | }
128 |
129 | await bot.waitForTicks(bot.waitTicks * itemTicks);
130 | res.json(bot.observe());
131 |
132 | initCounter(bot);
133 | bot.chat("/gamerule keepInventory true");
134 | bot.chat("/gamerule doDaylightCycle false");
135 | });
136 |
137 | function onConnectionFailed(e) {
138 | console.log(e);
139 | bot = null;
140 | res.status(400).json({ error: e });
141 | }
142 | function onDisconnect(message) {
143 | if (bot.viewer) {
144 | bot.viewer.close();
145 | }
146 | bot.end();
147 | console.log(message);
148 | bot = null;
149 | }
150 | });
151 |
152 | app.post("/step", async (req, res) => {
153 | // import useful package
154 | let response_sent = false;
155 | function otherError(err) {
156 | console.log("Uncaught Error");
157 | bot.emit("error", handleError(err));
158 | bot.waitForTicks(bot.waitTicks).then(() => {
159 | if (!response_sent) {
160 | response_sent = true;
161 | res.json(bot.observe());
162 | }
163 | });
164 | }
165 |
166 | process.on("uncaughtException", otherError);
167 |
168 | const mcData = require("minecraft-data")(bot.version);
169 | mcData.itemsByName["leather_cap"] = mcData.itemsByName["leather_helmet"];
170 | mcData.itemsByName["leather_tunic"] =
171 | mcData.itemsByName["leather_chestplate"];
172 | mcData.itemsByName["leather_pants"] =
173 | mcData.itemsByName["leather_leggings"];
174 | mcData.itemsByName["leather_boots"] = mcData.itemsByName["leather_boots"];
175 | mcData.itemsByName["lapis_lazuli_ore"] = mcData.itemsByName["lapis_ore"];
176 | mcData.blocksByName["lapis_lazuli_ore"] = mcData.blocksByName["lapis_ore"];
177 | const {
178 | Movements,
179 | goals: {
180 | Goal,
181 | GoalBlock,
182 | GoalNear,
183 | GoalXZ,
184 | GoalNearXZ,
185 | GoalY,
186 | GoalGetToBlock,
187 | GoalLookAtBlock,
188 | GoalBreakBlock,
189 | GoalCompositeAny,
190 | GoalCompositeAll,
191 | GoalInvert,
192 | GoalFollow,
193 | GoalPlaceBlock,
194 | },
195 | pathfinder,
196 | Move,
197 | ComputedPath,
198 | PartiallyComputedPath,
199 | XZCoordinates,
200 | XYZCoordinates,
201 | SafeBlock,
202 | GoalPlaceBlockOptions,
203 | } = require("mineflayer-pathfinder");
204 | const { Vec3 } = require("vec3");
205 |
206 | // Set up pathfinder
207 | const movements = new Movements(bot, mcData);
208 | bot.pathfinder.setMovements(movements);
209 |
210 | bot.globalTickCounter = 0;
211 | bot.stuckTickCounter = 0;
212 | bot.stuckPosList = [];
213 |
214 | function onTick() {
215 | bot.globalTickCounter++;
216 | if (bot.pathfinder.isMoving()) {
217 | bot.stuckTickCounter++;
218 | if (bot.stuckTickCounter >= 100) {
219 | onStuck(1.5);
220 | bot.stuckTickCounter = 0;
221 | }
222 | }
223 | }
224 |
225 | bot.on("physicTick", onTick);
226 |
227 | // initialize fail count
228 | let _craftItemFailCount = 0;
229 | let _killMobFailCount = 0;
230 | let _mineBlockFailCount = 0;
231 | let _placeItemFailCount = 0;
232 | let _smeltItemFailCount = 0;
233 |
234 | // Retrieve array form post bod
235 | const code = req.body.code;
236 | const programs = req.body.programs;
237 | bot.cumulativeObs = [];
238 | await bot.waitForTicks(bot.waitTicks);
239 | const r = await evaluateCode(code, programs);
240 | process.off("uncaughtException", otherError);
241 | if (r !== "success") {
242 | bot.emit("error", handleError(r));
243 | }
244 | await returnItems();
245 | // wait for last message
246 | await bot.waitForTicks(bot.waitTicks);
247 | if (!response_sent) {
248 | response_sent = true;
249 | res.json(bot.observe());
250 | }
251 | bot.removeListener("physicTick", onTick);
252 |
253 | async function evaluateCode(code, programs) {
254 | // 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
255 | try {
256 | await eval("(async () => {" + programs + "\n" + code + "})()");
257 | return "success";
258 | } catch (err) {
259 | return err;
260 | }
261 | }
262 |
263 | function onStuck(posThreshold) {
264 | const currentPos = bot.entity.position;
265 | bot.stuckPosList.push(currentPos);
266 |
267 | // Check if the list is full
268 | if (bot.stuckPosList.length === 5) {
269 | const oldestPos = bot.stuckPosList[0];
270 | const posDifference = currentPos.distanceTo(oldestPos);
271 |
272 | if (posDifference < posThreshold) {
273 | teleportBot(); // execute the function
274 | }
275 |
276 | // Remove the oldest time from the list
277 | bot.stuckPosList.shift();
278 | }
279 | }
280 |
281 | function teleportBot() {
282 | const blocks = bot.findBlocks({
283 | matching: (block) => {
284 | return block.type === 0;
285 | },
286 | maxDistance: 1,
287 | count: 27,
288 | });
289 |
290 | if (blocks) {
291 | // console.log(blocks.length);
292 | const randomIndex = Math.floor(Math.random() * blocks.length);
293 | const block = blocks[randomIndex];
294 | bot.chat(`/tp @s ${block.x} ${block.y} ${block.z}`);
295 | } else {
296 | bot.chat("/tp @s ~ ~1.25 ~");
297 | }
298 | }
299 |
300 | function returnItems() {
301 | bot.chat("/gamerule doTileDrops false");
302 | const crafting_table = bot.findBlock({
303 | matching: mcData.blocksByName.crafting_table.id,
304 | maxDistance: 128,
305 | });
306 | if (crafting_table) {
307 | bot.chat(
308 | `/setblock ${crafting_table.position.x} ${crafting_table.position.y} ${crafting_table.position.z} air destroy`
309 | );
310 | bot.chat("/give @s crafting_table");
311 | }
312 | const furnace = bot.findBlock({
313 | matching: mcData.blocksByName.furnace.id,
314 | maxDistance: 128,
315 | });
316 | if (furnace) {
317 | bot.chat(
318 | `/setblock ${furnace.position.x} ${furnace.position.y} ${furnace.position.z} air destroy`
319 | );
320 | bot.chat("/give @s furnace");
321 | }
322 | if (bot.inventoryUsed() >= 32) {
323 | // if chest is not in bot's inventory
324 | if (!bot.inventory.items().find((item) => item.name === "chest")) {
325 | bot.chat("/give @s chest");
326 | }
327 | }
328 | // if iron_pickaxe not in bot's inventory and bot.iron_pickaxe
329 | if (
330 | bot.iron_pickaxe &&
331 | !bot.inventory.items().find((item) => item.name === "iron_pickaxe")
332 | ) {
333 | bot.chat("/give @s iron_pickaxe");
334 | }
335 | bot.chat("/gamerule doTileDrops true");
336 | }
337 |
338 | function handleError(err) {
339 | let stack = err.stack;
340 | if (!stack) {
341 | return err;
342 | }
343 | console.log(stack);
344 | const final_line = stack.split("\n")[1];
345 | const regex = /:(\d+):\d+\)/;
346 |
347 | const programs_length = programs.split("\n").length;
348 | let match_line = null;
349 | for (const line of stack.split("\n")) {
350 | const match = regex.exec(line);
351 | if (match) {
352 | const line_num = parseInt(match[1]);
353 | if (line_num >= programs_length) {
354 | match_line = line_num - programs_length;
355 | break;
356 | }
357 | }
358 | }
359 | if (!match_line) {
360 | return err.message;
361 | }
362 | let f_line = final_line.match(
363 | /\((?.*):(?\d+):(?\d+)\)/
364 | );
365 | if (f_line && f_line.groups && fs.existsSync(f_line.groups.file)) {
366 | const { file, line, pos } = f_line.groups;
367 | const f = fs.readFileSync(file, "utf8").split("\n");
368 | // let filename = file.match(/(?<=node_modules\\)(.*)/)[1];
369 | let source = file + `:${line}\n${f[line - 1].trim()}\n `;
370 |
371 | const code_source =
372 | "at " +
373 | code.split("\n")[match_line - 1].trim() +
374 | " in your code";
375 | return source + err.message + "\n" + code_source;
376 | } else if (
377 | f_line &&
378 | f_line.groups &&
379 | f_line.groups.file.includes("")
380 | ) {
381 | const { file, line, pos } = f_line.groups;
382 | let source =
383 | "Your code" +
384 | `:${match_line}\n${code.split("\n")[match_line - 1].trim()}\n `;
385 | let code_source = "";
386 | if (line < programs_length) {
387 | source =
388 | "In your program code: " +
389 | programs.split("\n")[line - 1].trim() +
390 | "\n";
391 | code_source = `at line ${match_line}:${code
392 | .split("\n")
393 | [match_line - 1].trim()} in your code`;
394 | }
395 | return source + err.message + "\n" + code_source;
396 | }
397 | return err.message;
398 | }
399 | });
400 |
401 | app.post("/stop", (req, res) => {
402 | bot.end();
403 | res.json({
404 | message: "Bot stopped",
405 | });
406 | });
407 |
408 | app.post("/pause", (req, res) => {
409 | if (!bot) {
410 | res.status(400).json({ error: "Bot not spawned" });
411 | return;
412 | }
413 | bot.chat("/pause");
414 | bot.waitForTicks(bot.waitTicks).then(() => {
415 | res.json({ message: "Success" });
416 | });
417 | });
418 |
419 | // Server listening to PORT 3000
420 |
421 | const DEFAULT_PORT = 3000;
422 | const PORT = process.argv[2] || DEFAULT_PORT;
423 | app.listen(PORT, () => {
424 | console.log(`Server started on port ${PORT}`);
425 | });
426 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/base.js:
--------------------------------------------------------------------------------
1 | class Observation {
2 | constructor(bot) {
3 | if (new.target === Observation) {
4 | throw new TypeError(
5 | "Cannot instantiate abstract class Observation"
6 | );
7 | }
8 |
9 | this.bot = bot;
10 | this.name = "Observation";
11 | }
12 |
13 | observe() {
14 | throw new TypeError("Method 'observe()' must be implemented.");
15 | }
16 |
17 | reset() {}
18 | }
19 |
20 | function inject(bot, obs_list) {
21 | bot.obsList = [];
22 | bot.cumulativeObs = [];
23 | bot.eventMemory = {};
24 | obs_list.forEach((obs) => {
25 | bot.obsList.push(new obs(bot));
26 | });
27 | bot.event = function (event_name) {
28 | let result = {};
29 | bot.obsList.forEach((obs) => {
30 | if (obs.name.startsWith("on") && obs.name !== event_name) {
31 | return;
32 | }
33 | result[obs.name] = obs.observe();
34 | });
35 | bot.cumulativeObs.push([event_name, result]);
36 | };
37 | bot.observe = function () {
38 | bot.event("observe");
39 | const result = bot.cumulativeObs;
40 | bot.cumulativeObs = [];
41 | return JSON.stringify(result);
42 | };
43 | }
44 |
45 | module.exports = { Observation, inject };
46 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/chests.js:
--------------------------------------------------------------------------------
1 | const { Observation } = require("./base");
2 |
3 | class Chests extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "nearbyChests";
7 | this.chestsItems = {};
8 | bot.on("closeChest", (chestItems, position) => {
9 | this.chestsItems[position] = chestItems;
10 | });
11 | bot.on("removeChest", (chestPosition) => {
12 | delete this.chestsItems[chestPosition];
13 | });
14 | }
15 |
16 | observe() {
17 | const chests = this.bot.findBlocks({
18 | matching: this.bot.registry.blocksByName.chest.id,
19 | maxDistance: 16,
20 | count: 999,
21 | });
22 | chests.forEach((chest) => {
23 | if (!this.chestsItems.hasOwnProperty(chest)) {
24 | this.chestsItems[chest] = "Unknown";
25 | }
26 | });
27 | return this.chestsItems;
28 | }
29 | }
30 |
31 | module.exports = Chests;
32 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/inventory.js:
--------------------------------------------------------------------------------
1 | const { Observation } = require("./base");
2 |
3 | class Inventory extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "inventory";
7 | }
8 |
9 | observe() {
10 | return listItems(this.bot);
11 | }
12 | }
13 |
14 | function listItems(bot) {
15 | const items = getInventoryItems(bot);
16 | return items.reduce(itemToDict, {});
17 | }
18 |
19 | function getInventoryItems(bot) {
20 | const inventory = bot.currentWindow || bot.inventory;
21 | return inventory.items();
22 | }
23 |
24 | function itemToDict(acc, cur) {
25 | if (cur.name && cur.count) {
26 | //if both name and count property are defined
27 | if (acc[cur.name]) {
28 | //if the item is already in the dict
29 | acc[cur.name] += cur.count;
30 | } else {
31 | //if the item is not in the dict
32 | acc[cur.name] = cur.count;
33 | }
34 | }
35 | return acc;
36 | }
37 |
38 | //export modules
39 | module.exports = Inventory;
40 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/onChat.js:
--------------------------------------------------------------------------------
1 | const Observation = require("./base.js").Observation;
2 |
3 | class onChat extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "onChat";
7 | this.obs = "";
8 | bot.on("chatEvent", (username, message) => {
9 | // Save entity status to local variable
10 | if (message.startsWith("/")) {
11 | return;
12 | }
13 |
14 | this.obs += message;
15 | this.bot.event(this.name);
16 | });
17 | }
18 |
19 | observe() {
20 | const result = this.obs;
21 | this.obs = "";
22 | return result;
23 | }
24 | }
25 |
26 | module.exports = onChat;
27 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/onError.js:
--------------------------------------------------------------------------------
1 | const Observation = require("./base.js").Observation;
2 |
3 | class onError extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "onError";
7 | this.obs = null;
8 | bot.on("error", (err) => {
9 | // Save entity status to local variable
10 | this.obs = err;
11 | this.bot.event(this.name);
12 | });
13 | }
14 |
15 | observe() {
16 | const result = this.obs;
17 | this.obs = null;
18 | return result;
19 | }
20 | }
21 |
22 | module.exports = onError;
23 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/onSave.js:
--------------------------------------------------------------------------------
1 | const Observation = require("./base.js").Observation;
2 |
3 | class onSave extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "onSave";
7 | this.obs = null;
8 | bot.on("save", (eventName) => {
9 | // Save entity status to local variable
10 | this.obs = eventName;
11 | this.bot.event(this.name);
12 | });
13 | }
14 |
15 | observe() {
16 | const result = this.obs;
17 | this.obs = null;
18 | return result;
19 | }
20 | }
21 |
22 | module.exports = onSave;
23 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/status.js:
--------------------------------------------------------------------------------
1 | const Observation = require("./base.js").Observation;
2 |
3 | class Status extends Observation {
4 | constructor(bot) {
5 | super(bot);
6 | this.name = "status";
7 | }
8 |
9 | observe() {
10 | return {
11 | health: this.bot.health,
12 | food: this.bot.food,
13 | saturation: this.bot.foodSaturation,
14 | oxygen: this.bot.oxygenLevel,
15 | position: this.bot.entity.position,
16 | velocity: this.bot.entity.velocity,
17 | yaw: this.bot.entity.yaw,
18 | pitch: this.bot.entity.pitch,
19 | onGround: this.bot.entity.onGround,
20 | equipment: this.getEquipment(),
21 | name: this.bot.entity.username,
22 | timeSinceOnGround: this.bot.entity.timeSinceOnGround,
23 | isInWater: this.bot.entity.isInWater,
24 | isInLava: this.bot.entity.isInLava,
25 | isInWeb: this.bot.entity.isInWeb,
26 | isCollidedHorizontally: this.bot.entity.isCollidedHorizontally,
27 | isCollidedVertically: this.bot.entity.isCollidedVertically,
28 | biome: this.bot.blockAt(this.bot.entity.position)
29 | ? this.bot.blockAt(this.bot.entity.position).biome.name
30 | : "None",
31 | entities: this.getEntities(),
32 | timeOfDay: this.getTime(),
33 | inventoryUsed: this.bot.inventoryUsed(),
34 | elapsedTime: this.bot.globalTickCounter,
35 | };
36 | }
37 |
38 | itemToObs(item) {
39 | if (!item) return null;
40 | return item.name;
41 | }
42 |
43 | getTime() {
44 | const timeOfDay = this.bot.time.timeOfDay;
45 | let time = "";
46 | if (timeOfDay < 1000) {
47 | time = "sunrise";
48 | } else if (timeOfDay < 6000) {
49 | time = "day";
50 | } else if (timeOfDay < 12000) {
51 | time = "noon";
52 | } else if (timeOfDay < 13000) {
53 | time = "sunset";
54 | } else if (timeOfDay < 18000) {
55 | time = "night";
56 | } else if (timeOfDay < 22000) {
57 | time = "midnight";
58 | } else {
59 | time = "sunrise";
60 | }
61 | return time;
62 | }
63 |
64 | // For each item in equipment, if it exists, return the name of the item
65 | // otherwise return null
66 | getEquipment() {
67 | const slots = this.bot.inventory.slots;
68 | const mainHand = this.bot.heldItem;
69 | return slots
70 | .slice(5, 9)
71 | .concat(mainHand, slots[45])
72 | .map(this.itemToObs);
73 | }
74 |
75 | getEntities() {
76 | const entities = this.bot.entities;
77 | if (!entities) return {};
78 | // keep all monsters in one list, keep other mobs in another list
79 | const mobs = {};
80 | for (const id in entities) {
81 | const entity = entities[id];
82 | if (!entity.displayName) continue;
83 | if (entity.name === "player" || entity.name === "item") continue;
84 | if (entity.position.distanceTo(this.bot.entity.position) < 32) {
85 | if (!mobs[entity.name]) {
86 | mobs[entity.name] = entity.position.distanceTo(
87 | this.bot.entity.position
88 | );
89 | } else if (
90 | mobs[entity.name] >
91 | entity.position.distanceTo(this.bot.entity.position)
92 | ) {
93 | mobs[entity.name] = entity.position.distanceTo(
94 | this.bot.entity.position
95 | );
96 | }
97 | }
98 | }
99 | return mobs;
100 | }
101 | }
102 |
103 | module.exports = Status;
104 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/observation/voxels.js:
--------------------------------------------------------------------------------
1 | // Blocks = require("./blocks")
2 | const { Observation } = require("./base");
3 |
4 | class Voxels extends Observation {
5 | constructor(bot) {
6 | super(bot);
7 | this.name = "voxels";
8 | }
9 |
10 | observe() {
11 | return Array.from(getSurroundingBlocks(this.bot, 8, 2, 8));
12 | }
13 | }
14 |
15 | class BlockRecords extends Observation {
16 | constructor(bot) {
17 | super(bot);
18 | this.name = "blockRecords";
19 | this.records = new Set();
20 | this.tick = 0;
21 | bot.on("physicsTick", () => {
22 | this.tick++;
23 | if (this.tick >= 100) {
24 | const items = getInventoryItems(this.bot);
25 | getSurroundingBlocks(this.bot, 8, 2, 8).forEach((block) => {
26 | if (!items.has(block)) this.records.add(block);
27 | });
28 | this.tick = 0;
29 | }
30 | });
31 | }
32 |
33 | observe() {
34 | return Array.from(this.records);
35 | }
36 |
37 | reset() {
38 | this.records = new Set();
39 | }
40 | }
41 |
42 | function getSurroundingBlocks(bot, x_distance, y_distance, z_distance) {
43 | const surroundingBlocks = new Set();
44 |
45 | for (let x = -x_distance; x <= x_distance; x++) {
46 | for (let y = -y_distance; y <= y_distance; y++) {
47 | for (let z = -z_distance; z <= z_distance; z++) {
48 | const block = bot.blockAt(bot.entity.position.offset(x, y, z));
49 | if (block && block.type !== 0) {
50 | surroundingBlocks.add(block.name);
51 | }
52 | }
53 | }
54 | }
55 | // console.log(surroundingBlocks);
56 | return surroundingBlocks;
57 | }
58 |
59 | function getInventoryItems(bot) {
60 | const items = new Set();
61 | bot.inventory.items().forEach((item) => {
62 | if (item) items.add(item.name);
63 | });
64 | return items;
65 | }
66 |
67 | module.exports = { Voxels, BlockRecords };
68 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/skillLoader.js:
--------------------------------------------------------------------------------
1 | function inject(bot) {
2 | bot._sleep = bot.sleep;
3 | bot.sleep = async (bedBlock) => {
4 | await bot.waitForTicks(20);
5 | await bot._sleep(bedBlock);
6 | await bot.waitForTicks(135);
7 | };
8 |
9 | bot._fish = bot.fish;
10 | bot.fish = async () => {
11 | if (bot.heldItem?.name !== "fishing_rod") {
12 | bot.chat("I'm not holding a fishing rod!");
13 | return;
14 | }
15 | let timeout = null;
16 | await Promise.race([
17 | bot._fish(),
18 | new Promise(
19 | (resolve, reject) =>
20 | (timeout = setTimeout(() => {
21 | bot.activateItem();
22 | reject(
23 | new Error(
24 | "Finishing timeout, make sure you get to and look at a water block!"
25 | )
26 | );
27 | }, 60000))
28 | ),
29 | ]);
30 | clearTimeout(timeout);
31 | await bot.waitForTicks(20);
32 | };
33 |
34 | bot._consume = bot.consume;
35 | bot.consume = async () => {
36 | // action_count.activateItem++;
37 | await bot._consume();
38 | await bot.waitForTicks(20);
39 | };
40 |
41 | bot._useOn = bot.useOn;
42 | bot.useOn = async (entity) => {
43 | if (entity.position.distanceTo(bot.entity.position) > 6) {
44 | bot.chat("Please goto a place near the entity first!");
45 | return;
46 | }
47 | await bot._useOn(entity);
48 | await bot.waitForTicks(20);
49 | };
50 |
51 | bot._activateBlock = bot.activateBlock;
52 | bot.activateBlock = async (block) => {
53 | if (block.position.distanceTo(bot.entity.position) > 6) {
54 | bot.chat("Please goto a place near the block first!");
55 | return;
56 | }
57 | // action_count.activateBlock++;
58 | await bot._activateBlock(block);
59 | };
60 |
61 | bot._chat = bot.chat;
62 | bot.chat = (message) => {
63 | // action_count.chat++;
64 | bot.emit("chatEvent", "bot", message);
65 | bot._chat(message);
66 | };
67 |
68 | bot.inventoryUsed = () => {
69 | return bot.inventory.slots.slice(9, 45).filter((item) => item !== null)
70 | .length;
71 | };
72 |
73 | bot.save = function (eventName) {
74 | bot.emit("save", eventName);
75 | };
76 | }
77 |
78 | // export all control_primitives
79 | module.exports = { inject };
80 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/lib/utils.js:
--------------------------------------------------------------------------------
1 | let gameTimeCounter = 0;
2 | let gameTimeList = [];
3 | const initCounter = (bot) => {
4 | gameTimeList = [];
5 | for (let i = 0; i < 13000; i += 1000) {
6 | gameTimeList.push(i);
7 | }
8 | for (let i = 13000; i < 24000; i += 2000) {
9 | gameTimeList.push(i);
10 | }
11 | const timeOfDay = bot.time.timeOfDay;
12 | for (let i = 0; i < gameTimeList.length; i++) {
13 | if (gameTimeList[i] > timeOfDay) {
14 | gameTimeCounter = i - 1;
15 | break;
16 | }
17 | }
18 | };
19 |
20 | const getNextTime = () => {
21 | gameTimeCounter++;
22 | if (gameTimeCounter >= gameTimeList.length) {
23 | gameTimeCounter = 0;
24 | }
25 | return gameTimeList[gameTimeCounter];
26 | };
27 |
28 | module.exports = {
29 | initCounter,
30 | getNextTime,
31 | };
32 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | lib/
107 | package-lock.json
108 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 TheDudeFromCI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/README.md:
--------------------------------------------------------------------------------
1 | mineflayer-collectblock
2 | A small utility plugin for allowing users to collect blocks using a higher level API.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ---
14 | ## This is a modified version to better support Voyager
15 |
16 | ## Showcase
17 |
18 | You can see a video of the plugin in action, [here.](https://youtu.be/5T_rcCnNnf4)
19 | 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)
20 |
21 | ### Description
22 |
23 | 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.
24 |
25 | 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.
26 |
27 | ### Getting Started
28 |
29 | This plugin is built using Node and can be installed using:
30 | ```bash
31 | npm install --save mineflayer-collectblock
32 | ```
33 |
34 | ### Simple Bot
35 |
36 | The brief description goes here.
37 |
38 | ```js
39 | // Create your bot
40 | const mineflayer = require("mineflayer")
41 | const bot = mineflayer.createBot({
42 | host: 'localhost',
43 | username: 'Player',
44 | })
45 | let mcData
46 |
47 | // Load collect block
48 | bot.loadPlugin(require('mineflayer-collectblock').plugin)
49 |
50 | async function collectGrass() {
51 | // Find a nearby grass block
52 | const grass = bot.findBlock({
53 | matching: mcData.blocksByName.grass_block.id,
54 | maxDistance: 64
55 | })
56 |
57 | if (grass) {
58 | // If we found one, collect it.
59 | try {
60 | await bot.collectBlock.collect(grass)
61 | collectGrass() // Collect another grass block
62 | } catch (err) {
63 | console.log(err) // Handle errors, if any
64 | }
65 | }
66 | }
67 |
68 | // On spawn, start collecting all nearby grass
69 | bot.once('spawn', () => {
70 | mcData = require('minecraft-data')(bot.version)
71 | collectGrass()
72 | })
73 | ```
74 |
75 | ### Documentation
76 |
77 | [API](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/docs/api.md)
78 |
79 | [Examples](https://github.com/TheDudeFromCI/mineflayer-collectblock/tree/master/examples)
80 |
81 | ### License
82 |
83 | This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/LICENSE) license.
84 |
85 | ### Contributions
86 |
87 | This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated!
88 |
89 | 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.
90 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | Welcome to the *mineflayer-collectblock* API documentation page.
4 |
5 | ## Table of Contents
6 |
7 | - [1. Summary](#1-summary)
8 | - [Properties](#properties)
9 | - [`bot.collectblock.movements: Movements`](#botcollectblockmovements-movements)
10 | - [Functions](#functions)
11 | - [collect](#collect)
12 | - [Options:](#options)
13 |
14 | ## 1. Summary
15 |
16 | 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.
17 |
18 | ## Properties
19 |
20 | ### `bot.collectblock.movements: Movements`
21 |
22 | 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.
23 |
24 | If set to null, the pathfinder plugin movements is not updated.
25 |
26 | Defaults to a new movements object instance.
27 |
28 | ## Functions
29 |
30 | ### collect
31 |
32 | Usage: `bot.collectblock.collect(target: Collectable | Collectable[], options?: CollectOptions, cb: (err?: Error) => void): void`
33 |
34 | 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.
35 |
36 | #### Options:
37 |
38 | * `append: boolean`
39 |
40 | If true, the target(s) will be appended to the existing target list instead of starting a new task. Defaults to false.
41 |
42 | * `ignoreNoPath: boolean`
43 |
44 | 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.
45 |
46 | * `chestLocations: Vec3[]`
47 |
48 | 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.
49 |
50 | * `itemFilter: ItemFilter`
51 |
52 | 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.
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/examples/collector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This bot example show how to direct a bot to collect a specific block type
3 | * or a group of nearby blocks of that type.
4 | */
5 |
6 | const mineflayer = require('mineflayer')
7 | const collectBlock = require('mineflayer-collectblock').plugin
8 |
9 | if (process.argv.length < 4 || process.argv.length > 6) {
10 | console.log('Usage : node collector.js [] []')
11 | process.exit(1)
12 | }
13 |
14 | const bot = mineflayer.createBot({
15 | host: process.argv[2],
16 | port: process.argv[3],
17 | username: process.argv[4] || 'collector',
18 | password: process.argv[5]
19 | })
20 |
21 | bot.loadPlugin(collectBlock)
22 |
23 | let mcData
24 | bot.once('spawn', () => {
25 | mcData = require('minecraft-data')(bot.version)
26 | })
27 |
28 | bot.on('chat', async (username, message) => {
29 | const args = message.split(' ')
30 | if (args[0] !== 'collect') return
31 |
32 | let count = 1
33 | if (args.length === 3) count = parseInt(args[1])
34 |
35 | let type = args[1]
36 | if (args.length === 3) type = args[2]
37 |
38 | const blockType = mcData.blocksByName[type]
39 | if (!blockType) {
40 | return
41 | }
42 |
43 | const blocks = bot.findBlocks({
44 | matching: blockType.id,
45 | maxDistance: 64,
46 | count: count
47 | })
48 |
49 | if (blocks.length === 0) {
50 | bot.chat("I don't see that block nearby.")
51 | return
52 | }
53 |
54 | const targets = []
55 | for (let i = 0; i < Math.min(blocks.length, count); i++) {
56 | targets.push(bot.blockAt(blocks[i]))
57 | }
58 |
59 | bot.chat(`Found ${targets.length} ${type}(s)`)
60 |
61 | try {
62 | await bot.collectBlock.collect(targets)
63 | // All blocks have been collected.
64 | bot.chat('Done')
65 | } catch (err) {
66 | // An error occurred, report it.
67 | bot.chat(err.message)
68 | console.log(err)
69 | }
70 | })
71 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/examples/oreMiner.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This bot example shows how to collect a vein of ores quickly after only finding a single block.
3 | * This makes it easy to collect a vein of ores or mine a tree without looking for every block in the
4 | * area.
5 | */
6 |
7 | const mineflayer = require('mineflayer')
8 | const collectBlock = require('mineflayer-collectblock').plugin
9 |
10 | if (process.argv.length < 4 || process.argv.length > 6) {
11 | console.log('Usage : node oreMiner.js [] []')
12 | process.exit(1)
13 | }
14 |
15 | const bot = mineflayer.createBot({
16 | host: process.argv[2],
17 | port: process.argv[3],
18 | username: process.argv[4] || 'oreMiner',
19 | password: process.argv[5]
20 | })
21 |
22 | bot.loadPlugin(collectBlock)
23 |
24 | let mcData
25 | bot.once('spawn', () => {
26 | mcData = require('minecraft-data')(bot.version)
27 | })
28 |
29 | bot.on('chat', async (username, message) => {
30 | const args = message.split(' ')
31 | if (args[0] !== 'collect') return
32 |
33 | const blockType = mcData.blocksByName[args[1]]
34 | if (!blockType) {
35 | bot.chat(`I don't know any blocks named ${args[1]}.`)
36 | return
37 | }
38 |
39 | const block = bot.findBlock({
40 | matching: blockType.id,
41 | maxDistance: 64
42 | })
43 |
44 | if (!block) {
45 | bot.chat("I don't see that block nearby.")
46 | return
47 | }
48 |
49 | const targets = bot.collectBlock.findFromVein(block)
50 | try {
51 | await bot.collectBlock.collect(targets)
52 | // All blocks have been collected.
53 | bot.chat('Done')
54 | } catch (err) {
55 | // An error occurred, report it.
56 | bot.chat(err.message)
57 | console.log(err)
58 | }
59 | })
60 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/examples/storageBot.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This bot example shows how to use the chest filling mechanic of the plugin.
3 | * Simply provide a given storage chest, and the bot will automatically try and
4 | * store it's inventory in that chest when the bot's inventory becomes full.
5 | */
6 |
7 | if (process.argv.length < 4 || process.argv.length > 6) {
8 | console.log('Usage : node storageBot.js [] []')
9 | process.exit(1)
10 | }
11 |
12 | // Load your libraries
13 | const mineflayer = require('mineflayer')
14 | const collectBlock = require('mineflayer-collectblock').plugin
15 |
16 | // Create your bot
17 | const bot = mineflayer.createBot({
18 | host: process.argv[2],
19 | port: parseInt(process.argv[3]),
20 | username: process.argv[4] ? process.argv[4] : 'storageBot',
21 | password: process.argv[5]
22 | })
23 |
24 | // Load the collect block plugin
25 | bot.loadPlugin(collectBlock)
26 |
27 | // Load mcData on login
28 | let mcData
29 | bot.once('login', () => {
30 | mcData = require('minecraft-data')(bot.version)
31 | })
32 |
33 | // On spawn, try to find any nearby chests and save those as storage locations.
34 | // When the bot's inventory becomes too full, it will empty it's inventory into
35 | // these chests before collecting more resources. If a chest gets full, it moves
36 | // to the next one in order until it's inventory is empty or it runs out of chests.
37 | bot.once('spawn', () => {
38 | bot.collectBlock.chestLocations = bot.findBlocks({
39 | matching: mcData.blocksByName.chest.id,
40 | maxDistance: 16,
41 | count: 999999 // Get as many chests as we can
42 | })
43 |
44 | if (bot.collectBlock.chestLocations.length === 0) {
45 | bot.chat("I don't see any chests nearby.")
46 | } else {
47 | for (const chestPos of bot.collectBlock.chestLocations) {
48 | bot.chat(`I found a chest at ${chestPos}`)
49 | }
50 | }
51 | })
52 |
53 | // Wait for someone to say something
54 | bot.on('chat', async (username, message) => {
55 | // If the player says something start starts with "collect"
56 | // Otherwise, do nothing
57 | const args = message.split(' ')
58 | if (args[0] !== 'collect') return
59 |
60 | // If the player specifies a number, collect that many. Otherwise, default to 1.
61 | let count = 1
62 | if (args.length === 3) count = parseInt(args[1])
63 |
64 | // If a number was given the item number is the 3rd arg, not the 2nd.
65 | let type = args[1]
66 | if (args.length === 3) type = args[2]
67 |
68 | // Get the id of that block type for this version of Minecraft.
69 | const blockType = mcData.blocksByName[type]
70 | if (!blockType) {
71 | bot.chat(`I don't know any blocks named ${type}.`)
72 | return
73 | }
74 |
75 | // Find all nearby blocks of that type, up to the given count, within 64 blocks.
76 | const blocks = bot.findBlocks({
77 | matching: blockType.id,
78 | maxDistance: 64,
79 | count: count
80 | })
81 |
82 | // Complain if we can't find any nearby blocks of that type.
83 | if (blocks.length === 0) {
84 | bot.chat("I don't see that block nearby.")
85 | return
86 | }
87 |
88 | // Convert the block position array into a block array to pass to collect block.
89 | const targets = []
90 | for (let i = 0; i < Math.min(blocks.length, count); i++) {
91 | targets.push(bot.blockAt(blocks[i]))
92 | }
93 |
94 | // Announce what we found.
95 | bot.chat(`Found ${targets.length} ${type}(s)`)
96 |
97 | // Tell the bot to collect all of the given blocks in the block list.
98 | try {
99 | await bot.collectBlock.collect(targets)
100 | // All blocks have been collected.
101 | bot.chat('Done')
102 | } catch (err) {
103 | // An error occurred, report it.
104 | bot.chat(err.message)
105 | console.log(err)
106 | }
107 | })
108 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mineflayer-collectblock",
3 | "version": "1.4.1",
4 | "description": "A simple utility plugin for Mineflayer that add a higher level API for collecting blocks.",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "scripts": {
8 | "build": "ts-standard && tsc && require-self",
9 | "clean": "rm -rf lib",
10 | "test": "test"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/TheDudeFromCI/mineflayer-collectblock.git"
15 | },
16 | "keywords": [
17 | "mineflayer",
18 | "plugin",
19 | "api",
20 | "utility",
21 | "helper",
22 | "collect"
23 | ],
24 | "author": "TheDudeFromCI",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/TheDudeFromCI/mineflayer-collectblock/issues"
28 | },
29 | "homepage": "https://github.com/TheDudeFromCI/mineflayer-collectblock#readme",
30 | "dependencies": {
31 | "mineflayer": "^4.0.0",
32 | "mineflayer-pathfinder": "^2.1.1",
33 | "mineflayer-tool": "^1.1.0"
34 | },
35 | "devDependencies": {
36 | "@types/node": "^18.6.4",
37 | "require-self": "^0.2.3",
38 | "ts-standard": "^11.0.0",
39 | "typescript": "^4.1.3"
40 | },
41 | "files": [
42 | "lib/**/*"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts:
--------------------------------------------------------------------------------
1 | import { Bot } from 'mineflayer'
2 | import { Block } from 'prismarine-block'
3 |
4 | export function findFromVein (bot: Bot, block: Block, maxBlocks: number, maxDistance: number, floodRadius: number): Block[] {
5 | const targets: Block[] = []
6 | const open: Block[] = [block]
7 | const type = block.type
8 | const center = block.position
9 |
10 | for (let i = 0; i < maxBlocks; i++) {
11 | const next = open.pop()
12 | if (next == null) break
13 |
14 | targets.push(next)
15 |
16 | for (let x = -floodRadius; x <= floodRadius; x++) {
17 | for (let y = -floodRadius; y <= floodRadius; y++) {
18 | for (let z = -floodRadius; z <= floodRadius; z++) {
19 | const neighborPos = next.position.offset(x, y, z)
20 | if (neighborPos.manhattanDistanceTo(center) > maxDistance) continue
21 |
22 | const neighbor = bot.blockAt(neighborPos)
23 | if (neighbor == null || neighbor.type !== type) continue
24 |
25 | if (targets.includes(neighbor)) continue
26 | if (open.includes(neighbor)) continue
27 |
28 | open.push(neighbor)
29 | }
30 | }
31 | }
32 | }
33 |
34 | return targets
35 | }
36 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/Inventory.ts:
--------------------------------------------------------------------------------
1 | import { Bot } from 'mineflayer'
2 | import { Callback } from './CollectBlock'
3 | import { Vec3 } from 'vec3'
4 | import { error } from './Util'
5 | import { Item } from 'prismarine-item'
6 | import { goals } from 'mineflayer-pathfinder'
7 | import { callbackify } from 'util'
8 |
9 | export type ItemFilter = (item: Item) => boolean
10 |
11 | function getClosestChest (bot: Bot, chestLocations: Vec3[]): Vec3 | null {
12 | let chest = null
13 | let distance = 0
14 |
15 | for (const c of chestLocations) {
16 | const dist = c.distanceTo(bot.entity.position)
17 | if (chest == null || dist < distance) {
18 | chest = c
19 | distance = dist
20 | }
21 | }
22 |
23 | if (chest != null) {
24 | chestLocations.splice(chestLocations.indexOf(chest), 1)
25 | }
26 |
27 | return chest
28 | }
29 |
30 | export async function emptyInventoryIfFull (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise {
31 | // @ts-expect-error
32 | if (cb != null) return callbackify(emptyInventoryIfFull)(bot, chestLocations, cb)
33 | if (bot.inventory.emptySlotCount() > 0) return
34 | return await emptyInventory(bot, chestLocations, itemFilter)
35 | }
36 |
37 | export async function emptyInventory (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise {
38 | // @ts-expect-error
39 | if (cb != null) return callbackify(emptyInventory)(bot, chestLocations, cb)
40 | if (chestLocations.length === 0) {
41 | throw error('NoChests', 'There are no defined chest locations!')
42 | }
43 |
44 | // Shallow clone so we can safely remove chests from the list that are full.
45 | chestLocations = [...chestLocations]
46 |
47 | while (true) {
48 | const chest = getClosestChest(bot, chestLocations)
49 | if (chest == null) {
50 | throw error('NoChests', 'All chests are full.')
51 | }
52 | const hasRemaining = await tryEmptyInventory(bot, chest, itemFilter)
53 | if (!hasRemaining) return
54 | }
55 | }
56 |
57 | async function tryEmptyInventory (bot: Bot, chestLocation: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise {
58 | // @ts-expect-error
59 | if (cb != null) return callbackify(tryEmptyInventory)(bot, chestLocation, itemFilter, cb)
60 | await gotoChest(bot, chestLocation)
61 | return await placeItems(bot, chestLocation, itemFilter)
62 | }
63 |
64 | async function gotoChest (bot: Bot, location: Vec3, cb?: Callback): Promise {
65 | // @ts-expect-error
66 | if (cb != null) return callbackify(gotoChest)(bot, location)
67 | await bot.pathfinder.goto(new goals.GoalGetToBlock(location.x, location.y, location.z))
68 | }
69 |
70 | async function placeItems (bot: Bot, chestPos: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise {
71 | // @ts-expect-error
72 | if (cb != null) return callbackify(placeItems)(bot, chestPos, itemFilter, cb)
73 | const chestBlock = bot.blockAt(chestPos)
74 | if (chestBlock == null) {
75 | throw error('UnloadedChunk', 'Chest is in an unloaded chunk!')
76 | }
77 | const chest = await bot.openChest(chestBlock)
78 | for (const item of bot.inventory.items()) {
79 | if (!itemFilter(item)) continue
80 | if (chest.firstEmptyContainerSlot() === null) {
81 | // We have items that didn't fit.
82 | return true
83 | }
84 | await chest.deposit(item.type, item.metadata, item.count)
85 | }
86 | return false
87 | }
88 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/Targets.ts:
--------------------------------------------------------------------------------
1 | import { Bot } from 'mineflayer'
2 | import { Block } from 'prismarine-block'
3 | import { Entity } from 'prismarine-entity'
4 |
5 | export type Collectable = Block | Entity
6 |
7 | export class Targets {
8 | private readonly bot: Bot
9 | private targets: Collectable[] = []
10 |
11 | constructor (bot: Bot) {
12 | this.bot = bot
13 | }
14 |
15 | appendTargets (targets: Collectable[]): void {
16 | for (const target of targets) {
17 | this.appendTarget(target)
18 | }
19 | }
20 |
21 | appendTarget (target: Collectable): void {
22 | if (this.targets.includes(target)) return
23 | this.targets.push(target)
24 | }
25 |
26 | /**
27 | * Gets the closest target to the bot in this list.
28 | *
29 | * @returns The closest target, or null if there are no targets.
30 | */
31 | getClosest (): Collectable | null {
32 | let closest: Collectable | null = null
33 | let distance: number = 0
34 |
35 | for (const target of this.targets) {
36 | const dist = target.position.distanceTo(this.bot.entity.position)
37 |
38 | if (closest == null || dist < distance) {
39 | closest = target
40 | distance = dist
41 | }
42 | }
43 |
44 | return closest
45 | }
46 |
47 | get empty (): boolean {
48 | return this.targets.length === 0
49 | }
50 |
51 | clear (): void {
52 | this.targets.length = 0
53 | }
54 |
55 | removeTarget (target: Collectable): void {
56 | const index = this.targets.indexOf(target)
57 | if (index < 0) return
58 | this.targets.splice(index, 1)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts:
--------------------------------------------------------------------------------
1 | import type { Callback } from './index'
2 | export type Task = (cb: Callback) => void
3 | export type SyncTask = () => void
4 |
5 | /**
6 | * A simple utility class for queuing up a series of async tasks to execute.
7 | */
8 | export class TaskQueue {
9 | private tasks: Task[] = []
10 |
11 | /**
12 | * If true, the task list will stop executing if one of the tasks throws an error.
13 | */
14 | readonly stopOnError: boolean = true
15 |
16 | /**
17 | * Adds a new async task to this queue. The provided callback should be executed when
18 | * the async task is complete.
19 | *
20 | * @param task - The async task to add.
21 | */
22 | add (task: Task): void {
23 | this.tasks.push(task)
24 | }
25 |
26 | /**
27 | * Adds a synchronous task toi this queue.
28 | *
29 | * @param task - The sync task to add.
30 | */
31 | addSync (task: SyncTask): void {
32 | this.add((cb) => {
33 | try {
34 | task()
35 | cb()
36 | } catch (err: any) {
37 | cb(err)
38 | }
39 | })
40 | }
41 |
42 | /**
43 | * Runs all tasks currently in this queue and empties the queue.
44 | *
45 | * @param cb - The optional callback to be executed when all tasks in this queue have
46 | * finished executing.
47 | */
48 | runAll (cb?: Callback): void {
49 | const taskList = this.tasks
50 | this.tasks = []
51 |
52 | let index = -1
53 | const runNext: () => void = () => {
54 | index++
55 | if (index >= taskList.length) {
56 | if (cb !== undefined) cb()
57 | return
58 | }
59 |
60 | try {
61 | taskList[index]((err) => {
62 | if (err !== undefined) {
63 | if (cb !== undefined) cb(err)
64 |
65 | if (this.stopOnError) return
66 | }
67 |
68 | runNext()
69 | })
70 | } catch (err: any) {
71 | if (cb !== undefined) cb(err)
72 | }
73 | }
74 |
75 | runNext()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts:
--------------------------------------------------------------------------------
1 | import { Bot } from 'mineflayer'
2 |
3 | class Subscription {
4 | constructor (readonly eventName: string, readonly callback: Function) {}
5 | }
6 |
7 | export class TemporarySubscriber {
8 | private readonly subscriptions: Subscription[] = []
9 |
10 | constructor (readonly bot: Bot) {}
11 |
12 | /**
13 | * Adds a new temporary event listener to the bot.
14 | *
15 | * @param event - The event to subscribe to.
16 | * @param callback - The function to execute.
17 | */
18 | subscribeTo (event: string, callback: Function): void {
19 | this.subscriptions.push(new Subscription(event, callback))
20 |
21 | // @ts-expect-error
22 | this.bot.on(event, callback)
23 | }
24 |
25 | /**
26 | * Removes all attached event listeners from the bot.
27 | */
28 | cleanup (): void {
29 | for (const sub of this.subscriptions) {
30 | // @ts-expect-error
31 | this.bot.removeListener(sub.eventName, sub.callback)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/Util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates a new error object with the given type and message.
3 | *
4 | * @param type - The error type.
5 | * @param message - The error message.
6 | *
7 | * @returns The error object.
8 | */
9 | export function error (type: string, message: string): Error {
10 | const e = new Error(message)
11 | e.name = type
12 | return e
13 | }
14 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Bot } from 'mineflayer'
2 | import { CollectBlock } from './CollectBlock'
3 | import { pathfinder as pathfinderPlugin } from 'mineflayer-pathfinder'
4 | import { plugin as toolPlugin } from 'mineflayer-tool'
5 |
6 | export function plugin (bot: Bot): void {
7 | // @ts-expect-error
8 | bot.collectBlock = new CollectBlock(bot)
9 |
10 | // Load plugins if not loaded manually.
11 | setTimeout(() => loadPathfinderPlugin(bot), 0)
12 | setTimeout(() => loadToolPlugin(bot), 0)
13 | }
14 |
15 | function loadPathfinderPlugin (bot: Bot): void {
16 | if (bot.pathfinder != null) return
17 | bot.loadPlugin(pathfinderPlugin)
18 | }
19 |
20 | function loadToolPlugin (bot: Bot): void {
21 | if (bot.tool != null) return
22 | bot.loadPlugin(toolPlugin)
23 | }
24 |
25 | export { CollectBlock, Callback, CollectOptions } from './CollectBlock'
26 |
--------------------------------------------------------------------------------
/voyager/env/mineflayer/mineflayer-collectblock/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | /* Basic Options */
5 | // "incremental": true, /* Enable incremental compilation */
6 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
8 | // "lib": [], /* Specify library files to be included in the compilation. */
9 | "allowJs": true, /* Allow javascript files to be compiled. */
10 | "checkJs": true, /* Report errors in .js files. */
11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | "declaration": true,
13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
14 | // "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | "outDir": "./lib",
17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
18 | // "composite": true, /* Enable project compilation */
19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
20 | // "removeComments": true, /* Do not emit comments to output. */
21 | // "noEmit": true, /* Do not emit outputs. */
22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 | /* Additional Checks */
35 | "noUnusedLocals": true, /* Report errors on unused locals. */
36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
37 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
39 | /* Module Resolution Options */
40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
44 | // "typeRoots": [], /* List of folders to include type definitions from. */
45 | // "types": [], /* Type declaration files to be included in compilation. */
46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
47 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
49 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
50 | /* Source Map Options */
51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
55 | /* Experimental Options */
56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
58 | /* Advanced Options */
59 | "skipLibCheck": true, /* Skip type checking of declaration files. */
60 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
61 | },
62 | "include": [
63 | "src"
64 | ],
65 | "exclude": [
66 | "node_modules",
67 | "**/__tests__/*"
68 | ]
69 | }
--------------------------------------------------------------------------------
/voyager/env/mineflayer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voyager",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "body-parser": "^1.20.2",
14 | "express": "^4.18.2",
15 | "magic-string": "^0.30.0",
16 | "minecraft-data": "^3.31.0",
17 | "minecrafthawkeye": "^1.3.6",
18 | "mineflayer": "^4.8.1",
19 | "mineflayer-collectblock": "file:mineflayer-collectblock",
20 | "mineflayer-pathfinder": "^2.4.2",
21 | "mineflayer-pvp": "^1.3.2",
22 | "mineflayer-tool": "^1.2.0",
23 | "mocha": "^10.2.0",
24 | "prismarine-biome": "^1.3.0",
25 | "prismarine-block": "^1.16.3",
26 | "prismarine-entity": "^2.2.0",
27 | "prismarine-item": "^1.12.1",
28 | "prismarine-nbt": "^2.2.1",
29 | "prismarine-recipe": "^1.3.1",
30 | "prismarine-viewer": "^1.24.0",
31 | "typescript": "^4.9.5",
32 | "vec3": "^0.1.8"
33 | },
34 | "devDependencies": {
35 | "prettier": "2.8.5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/voyager/env/process_monitor.py:
--------------------------------------------------------------------------------
1 | import time
2 | import re
3 | import warnings
4 | from typing import List
5 |
6 | import psutil
7 | import subprocess
8 | import logging
9 | import threading
10 |
11 | import voyager.utils as U
12 |
13 |
14 | class SubprocessMonitor:
15 | def __init__(
16 | self,
17 | commands: List[str],
18 | name: str,
19 | ready_match: str = r".*",
20 | log_path: str = "logs",
21 | callback_match: str = r"^(?!x)x$", # regex that will never match
22 | callback: callable = None,
23 | finished_callback: callable = None,
24 | ):
25 | self.commands = commands
26 | start_time = time.strftime("%Y%m%d_%H%M%S")
27 | self.name = name
28 | self.logger = logging.getLogger(name)
29 | handler = logging.FileHandler(U.f_join(log_path, f"{start_time}.log"))
30 | formatter = logging.Formatter(
31 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
32 | )
33 | handler.setFormatter(formatter)
34 | self.logger.addHandler(handler)
35 | self.logger.setLevel(logging.INFO)
36 | self.process = None
37 | self.ready_match = ready_match
38 | self.ready_event = None
39 | self.ready_line = None
40 | self.callback_match = callback_match
41 | self.callback = callback
42 | self.finished_callback = finished_callback
43 | self.thread = None
44 |
45 | def _start(self):
46 | self.logger.info(f"Starting subprocess with commands: {self.commands}")
47 |
48 | self.process = psutil.Popen(
49 | self.commands,
50 | stdout=subprocess.PIPE,
51 | stderr=subprocess.STDOUT,
52 | universal_newlines=True,
53 | )
54 | print(f"Subprocess {self.name} started with PID {self.process.pid}.")
55 | for line in iter(self.process.stdout.readline, ""):
56 | self.logger.info(line.strip())
57 | if re.search(self.ready_match, line):
58 | self.ready_line = line
59 | self.logger.info("Subprocess is ready.")
60 | self.ready_event.set()
61 | if re.search(self.callback_match, line):
62 | self.callback()
63 | if not self.ready_event.is_set():
64 | self.ready_event.set()
65 | warnings.warn(f"Subprocess {self.name} failed to start.")
66 | if self.finished_callback:
67 | self.finished_callback()
68 |
69 | def run(self):
70 | self.ready_event = threading.Event()
71 | self.ready_line = None
72 | self.thread = threading.Thread(target=self._start)
73 | self.thread.start()
74 | self.ready_event.wait()
75 |
76 | def stop(self):
77 | self.logger.info("Stopping subprocess.")
78 | if self.process and self.process.is_running():
79 | self.process.terminate()
80 | self.process.wait()
81 |
82 | # def __del__(self):
83 | # if self.process.is_running():
84 | # self.stop()
85 |
86 | @property
87 | def is_running(self):
88 | if self.process is None:
89 | return False
90 | return self.process.is_running()
91 |
--------------------------------------------------------------------------------
/voyager/prompts/__init__.py:
--------------------------------------------------------------------------------
1 | import pkg_resources
2 | import voyager.utils as U
3 |
4 |
5 | def load_prompt(prompt):
6 | package_path = pkg_resources.resource_filename("voyager", "")
7 | return U.load_text(f"{package_path}/prompts/{prompt}.txt")
8 |
--------------------------------------------------------------------------------
/voyager/prompts/action_response_format.txt:
--------------------------------------------------------------------------------
1 | Explain: ...
2 | Plan:
3 | 1) ...
4 | 2) ...
5 | 3) ...
6 | ...
7 | Code:
8 | ```javascript
9 | // helper functions (only if needed, try to avoid them)
10 | ...
11 | // main function after the helper functions
12 | async function yourMainFunctionName(bot) {
13 | // ...
14 | }
15 | ```
--------------------------------------------------------------------------------
/voyager/prompts/action_template.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that writes Mineflayer javascript code to complete any Minecraft task specified by me.
2 |
3 | Here are some useful programs written with Mineflayer APIs.
4 |
5 | {programs}
6 |
7 |
8 | At each round of conversation, I will give you
9 | Code from the last round: ...
10 | Execution error: ...
11 | Chat log: ...
12 | Biome: ...
13 | Time: ...
14 | Nearby blocks: ...
15 | Nearby entities (nearest to farthest):
16 | Health: ...
17 | Hunger: ...
18 | Position: ...
19 | Equipment: ...
20 | Inventory (xx/36): ...
21 | Chests: ...
22 | Task: ...
23 | Context: ...
24 | Critique: ...
25 |
26 | You should then respond to me with
27 | Explain (if applicable): Are there any steps missing in your plan? Why does the code not complete the task? What does the chat log and execution error imply?
28 | Plan: How to complete the task step by step. You should pay attention to Inventory since it tells what you have. The task completeness check is also based on your final inventory.
29 | Code:
30 | 1) Write an async function taking the bot as the only argument.
31 | 2) Reuse the above useful programs as much as possible.
32 | - Use `mineBlock(bot, name, count)` to collect blocks. Do not use `bot.dig` directly.
33 | - Use `craftItem(bot, name, count)` to craft items. Do not use `bot.craft` or `bot.recipesFor` directly.
34 | - Use `smeltItem(bot, name count)` to smelt items. Do not use `bot.openFurnace` directly.
35 | - Use `placeItem(bot, name, position)` to place blocks. Do not use `bot.placeBlock` directly.
36 | - Use `killMob(bot, name, timeout)` to kill mobs. Do not use `bot.attack` directly.
37 | 3) Your function will be reused for building more complex functions. Therefore, you should make it generic and reusable. You should not make strong assumption about the inventory (as it may be changed at a later time), and therefore you should always check whether you have the required items before using them. If not, you should first collect the required items and reuse the above useful programs.
38 | 4) Functions in the "Code from the last round" section will not be saved or executed. Do not reuse functions listed there.
39 | 5) Anything defined outside a function will be ignored, define all your variables inside your functions.
40 | 6) Call `bot.chat` to show the intermediate progress.
41 | 7) Use `exploreUntil(bot, direction, maxDistance, callback)` when you cannot find something. You should frequently call this before mining blocks or killing mobs. You should select a direction at random every time instead of constantly using (1, 0, 1).
42 | 8) `maxDistance` should always be 32 for `bot.findBlocks` and `bot.findBlock`. Do not cheat.
43 | 9) Do not write infinite loops or recursive functions.
44 | 10) Do not use `bot.on` or `bot.once` to register event listeners. You definitely do not need them.
45 | 11) Name your function in a meaningful way (can infer the task from the name).
46 |
47 | You should only respond in the format as described below:
48 | RESPONSE FORMAT:
49 | {response_format}
50 |
--------------------------------------------------------------------------------
/voyager/prompts/critic.txt:
--------------------------------------------------------------------------------
1 | You are an assistant that assesses my progress of playing Minecraft and provides useful guidance.
2 |
3 | You are required to evaluate if I have met the task requirements. Exceeding the task requirements is also considered a success while failing to meet them requires you to provide critique to help me improve.
4 |
5 | I will give you the following information:
6 |
7 | Biome: The biome after the task execution.
8 | Time: The current time.
9 | Nearby blocks: The surrounding blocks. These blocks are not collected yet. However, this is useful for some placing or planting tasks.
10 | Health: My current health.
11 | Hunger: My current hunger level. For eating task, if my hunger level is 20.0, then I successfully ate the food.
12 | Position: My current position.
13 | Equipment: My final equipment. For crafting tasks, I sometimes equip the crafted item.
14 | Inventory (xx/36): My final inventory. For mining and smelting tasks, you only need to check inventory.
15 | Chests: If the task requires me to place items in a chest, you can find chest information here.
16 | Task: The objective I need to accomplish.
17 | Context: The context of the task.
18 |
19 | You should only respond in JSON format as described below:
20 | {
21 | "reasoning": "reasoning",
22 | "success": boolean,
23 | "critique": "critique",
24 | }
25 | Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
26 |
27 | Here are some examples:
28 | INPUT:
29 | Inventory (2/36): {'oak_log':2, 'spruce_log':2}
30 |
31 | Task: Mine 3 wood logs
32 |
33 | RESPONSE:
34 | {
35 | "reasoning": "You need to mine 3 wood logs. You have 2 oak logs and 2 spruce logs, which add up to 4 wood logs.",
36 | "success": true,
37 | "critique": ""
38 | }
39 |
40 | INPUT:
41 | Inventory (3/36): {'crafting_table': 1, 'spruce_planks': 6, 'stick': 4}
42 |
43 | Task: Craft a wooden pickaxe
44 |
45 | RESPONSE:
46 | {
47 | "reasoning": "You have enough materials to craft a wooden pickaxe, but you didn't craft it.",
48 | "success": false,
49 | "critique": "Craft a wooden pickaxe with a crafting table using 3 spruce planks and 2 sticks."
50 | }
51 |
52 | INPUT:
53 | Inventory (2/36): {'raw_iron': 5, 'stone_pickaxe': 1}
54 |
55 | Task: Mine 5 iron_ore
56 |
57 | RESPONSE:
58 | {
59 | "reasoning": "Mining iron_ore in Minecraft will get raw_iron. You have 5 raw_iron in your inventory.",
60 | "success": true,
61 | "critique": ""
62 | }
63 |
64 | INPUT:
65 | Biome: plains
66 |
67 | Nearby blocks: stone, dirt, grass_block, grass, farmland, wheat
68 |
69 | Inventory (26/36): ...
70 |
71 | Task: Plant 1 wheat seed.
72 |
73 | RESPONSE:
74 | {
75 | "reasoning": "For planting tasks, inventory information is useless. In nearby blocks, there is farmland and wheat, which means you succeed to plant the wheat seed.",
76 | "success": true,
77 | "critique": ""
78 | }
79 |
80 | INPUT:
81 | Inventory (11/36): {... ,'rotten_flesh': 1}
82 |
83 | Task: Kill 1 zombie
84 |
85 | Context: ...
86 |
87 | RESPONSE
88 | {
89 | "reasoning": "You have rotten flesh in your inventory, which means you successfully killed one zombie.",
90 | "success": true,
91 | "critique": ""
92 | }
93 |
94 | INPUT:
95 | Hunger: 20.0/20.0
96 |
97 | Inventory (11/36): ...
98 |
99 | Task: Eat 1 ...
100 |
101 | Context: ...
102 |
103 | RESPONSE
104 | {
105 | "reasoning": "For all eating task, if the player's hunger is 20.0, then the player successfully ate the food.",
106 | "success": true,
107 | "critique": ""
108 | }
109 |
110 | INPUT:
111 | Nearby blocks: chest
112 |
113 | Inventory (28/36): {'rail': 1, 'coal': 2, 'oak_planks': 13, 'copper_block': 1, 'diorite': 7, 'cooked_beef': 4, 'granite': 22, 'cobbled_deepslate': 23, 'feather': 4, 'leather': 2, 'cooked_chicken': 3, 'white_wool': 2, 'stick': 3, 'black_wool': 1, 'stone_sword': 2, 'stone_hoe': 1, 'stone_axe': 2, 'stone_shovel': 2, 'cooked_mutton': 4, 'cobblestone_wall': 18, 'crafting_table': 1, 'furnace': 1, 'iron_pickaxe': 1, 'stone_pickaxe': 1, 'raw_copper': 12}
114 |
115 | Chests:
116 | (81, 131, 16): {'andesite': 2, 'dirt': 2, 'cobblestone': 75, 'wooden_pickaxe': 1, 'wooden_sword': 1}
117 |
118 | Task: Deposit useless items into the chest at (81, 131, 16)
119 |
120 | Context: ...
121 |
122 | RESPONSE
123 | {
124 | "reasoning": "You have 28 items in your inventory after depositing, which is more than 20. You need to deposit more items from your inventory to the chest.",
125 | "success": false,
126 | "critique": "Deposit more useless items such as copper_block, diorite, granite, cobbled_deepslate, feather, and leather to meet the requirement of having only 20 occupied slots in your inventory."
127 | }
--------------------------------------------------------------------------------
/voyager/prompts/curriculum.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that tells me the next immediate task to do in Minecraft. My ultimate goal is to discover as many diverse things as possible, accomplish as many diverse tasks as possible and become the best Minecraft player in the world.
2 |
3 | I will give you the following information:
4 | Question 1: ...
5 | Answer: ...
6 | Question 2: ...
7 | Answer: ...
8 | Question 3: ...
9 | Answer: ...
10 | ...
11 | Biome: ...
12 | Time: ...
13 | Nearby blocks: ...
14 | Other blocks that are recently seen: ...
15 | Nearby entities (nearest to farthest): ...
16 | Health: Higher than 15 means I'm healthy.
17 | Hunger: Higher than 15 means I'm not hungry.
18 | Position: ...
19 | Equipment: If I have better armor in my inventory, you should ask me to equip it.
20 | Inventory (xx/36): ...
21 | Chests: You can ask me to deposit or take items from these chests. There also might be some unknown chest, you should ask me to open and check items inside the unknown chest.
22 | Completed tasks so far: ...
23 | Failed tasks that are too hard: ...
24 |
25 | You must follow the following criteria:
26 | 1) You should act as a mentor and guide me to the next task based on my current learning progress.
27 | 2) Please be very specific about what resources I need to collect, what I need to craft, or what mobs I need to kill.
28 | 3) The next task should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]" etc. It should be a single phrase. Do not propose multiple tasks at the same time. Do not mention anything else.
29 | 4) The next task should not be too hard since I may not have the necessary resources or have learned enough skills to complete it yet.
30 | 5) The next task should be novel and interesting. I should look for rare resources, upgrade my equipment and tools using better materials, and discover new things. I should not be doing the same thing over and over again.
31 | 6) I may sometimes need to repeat some tasks if I need to collect more resources to complete more difficult tasks. Only repeat tasks if necessary.
32 | 7) Do not ask me to build or dig shelter even if it's at night. I want to explore the world and discover new things. I don't want to stay in one place.
33 | 8) Tasks that require information beyond the player's status to verify should be avoided. For instance, "Placing 4 torches" and "Dig a 2x1x2 hole" are not ideal since they require visual confirmation from the screen. All the placing, building, planting, and trading tasks should be avoided. Do not propose task starting with these keywords.
34 |
35 | You should only respond in the format as described below:
36 | RESPONSE FORMAT:
37 | Reasoning: Based on the information I listed above, do reasoning about what the next task should be.
38 | Task: The next task.
39 |
40 | Here's an example response:
41 | Reasoning: The inventory is empty now, chop down a tree to get some wood.
42 | Task: Obtain a wood log.
--------------------------------------------------------------------------------
/voyager/prompts/curriculum_qa_step1_ask_questions.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that asks questions to help me decide the next immediate task to do in Minecraft. My ultimate goal is to discover as many things as possible, accomplish as many tasks as possible and become the best Minecraft player in the world.
2 |
3 | I will give you the following information:
4 | Biome: ...
5 | Time: ...
6 | Nearby blocks: ...
7 | Other blocks that are recently seen: ...
8 | Nearby entities (nearest to farthest): ...
9 | Health: ...
10 | Hunger: ...
11 | Position: ...
12 | Equipment: ...
13 | Inventory (xx/36): ...
14 | Chests: ...
15 | Completed tasks so far: ...
16 | Failed tasks that are too hard: ...
17 |
18 | You must follow the following criteria:
19 | 1) You should ask at least 5 questions (but no more than 10 questions) to help me decide the next immediate task to do. Each question should be followed by the concept that the question is about.
20 | 2) Your question should be specific to a concept in Minecraft.
21 | Bad example (the question is too general):
22 | Question: What is the best way to play Minecraft?
23 | Concept: unknown
24 | Bad example (axe is still general, you should specify the type of axe such as wooden axe):
25 | What are the benefits of using an axe to gather resources?
26 | Concept: axe
27 | Good example:
28 | Question: How to make a wooden pickaxe?
29 | Concept: wooden pickaxe
30 | 3) Your questions should be self-contained and not require any context.
31 | Bad example (the question requires the context of my current biome):
32 | Question: What are the blocks that I can find in my current biome?
33 | Concept: unknown
34 | Bad example (the question requires the context of my current inventory):
35 | Question: What are the resources you need the most currently?
36 | Concept: unknown
37 | Bad example (the question requires the context of my current inventory):
38 | Question: Do you have any gold or emerald resources?
39 | Concept: gold
40 | Bad example (the question requires the context of my nearby entities):
41 | Question: Can you see any animals nearby that you can kill for food?
42 | Concept: food
43 | Bad example (the question requires the context of my nearby blocks):
44 | Question: Is there any water source nearby?
45 | Concept: water
46 | Good example:
47 | Question: What are the blocks that I can find in the sparse jungle?
48 | Concept: sparse jungle
49 | 4) Do not ask questions about building tasks (such as building a shelter) since they are too hard for me to do.
50 |
51 | Let's say your current biome is sparse jungle. You can ask questions like:
52 | Question: What are the items that I can find in the sparse jungle?
53 | Concept: sparse jungle
54 | Question: What are the mobs that I can find in the sparse jungle?
55 | Concept: sparse jungle
56 |
57 | Let's say you see a creeper nearby, and you have not defeated a creeper before. You can ask a question like:
58 | Question: How to defeat the creeper?
59 | Concept: creeper
60 |
61 | Let's say your last completed task is "Craft a wooden pickaxe". You can ask a question like:
62 | Question: What are the suggested tasks that I can do after crafting a wooden pickaxe?
63 | Concept: wooden pickaxe
64 |
65 | Here are some more question and concept examples:
66 | Question: What are the ores that I can find in the sparse jungle?
67 | Concept: sparse jungle
68 | (the above concept should not be "ore" because I need to look up the page of "sparse jungle" to find out what ores I can find in the sparse jungle)
69 | Question: How can you obtain food in the sparse jungle?
70 | Concept: sparse jungle
71 | (the above concept should not be "food" because I need to look up the page of "sparse jungle" to find out what food I can obtain in the sparse jungle)
72 | Question: How can you use the furnace to upgrade your equipment and make useful items?
73 | Concept: furnace
74 | Question: How to obtain a diamond ore?
75 | Concept: diamond ore
76 | Question: What are the benefits of using a stone pickaxe over a wooden pickaxe?
77 | Concept: stone pickaxe
78 | Question: What are the tools that you can craft using wood planks and sticks?
79 | Concept: wood planks
80 |
81 | You should only respond in the format as described below:
82 | RESPONSE FORMAT:
83 | Reasoning: ...
84 | Question 1: ...
85 | Concept 1: ...
86 | Question 2: ...
87 | Concept 2: ...
88 | Question 3: ...
89 | Concept 3: ...
90 | Question 4: ...
91 | Concept 4: ...
92 | Question 5: ...
93 | Concept 5: ...
94 | ...
95 |
--------------------------------------------------------------------------------
/voyager/prompts/curriculum_qa_step2_answer_questions.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that answer my question about Minecraft.
2 |
3 | I will give you the following information:
4 | Question: ...
5 |
6 | You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
7 | 1) Start your answer with "Answer: ".
8 | 2) Answer "Answer: Unknown" if you don't know the answer.
--------------------------------------------------------------------------------
/voyager/prompts/curriculum_task_decomposition.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that generates a curriculum of subgoals to complete any Minecraft task specified by me.
2 |
3 | I'll give you a final task and my current inventory, you need to decompose the task into a list of subgoals based on my inventory.
4 |
5 | You must follow the following criteria:
6 | 1) Return a Python list of subgoals that can be completed in order to complete the specified task.
7 | 2) Each subgoal should follow a concise format, such as "Mine [quantity] [block]", "Craft [quantity] [item]", "Smelt [quantity] [item]", "Kill [quantity] [mob]", "Cook [quantity] [food]", "Equip [item]".
8 | 3) Include each level of necessary tools as a subgoal, such as wooden, stone, iron, diamond, etc.
9 |
10 | You should only respond in JSON format as described below:
11 | ["subgoal1", "subgoal2", "subgoal3", ...]
12 | Ensure the response can be parsed by Python `json.loads`, e.g.: no trailing commas, no single quotes, etc.
--------------------------------------------------------------------------------
/voyager/prompts/skill.txt:
--------------------------------------------------------------------------------
1 | You are a helpful assistant that writes a description of the given function written in Mineflayer javascript code.
2 |
3 | 1) Do not mention the function name.
4 | 2) Do not mention anything about `bot.chat` or helper functions.
5 | 3) There might be some helper functions before the main function, but you only need to describe the main function.
6 | 4) Try to summarize the function in no more than 6 sentences.
7 | 5) Your response should be a single line of text.
8 |
9 | For example, if the function is:
10 |
11 | async function mineCobblestone(bot) {
12 | // Check if the wooden pickaxe is in the inventory, if not, craft one
13 | let woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
14 | if (!woodenPickaxe) {
15 | bot.chat("Crafting a wooden pickaxe.");
16 | await craftWoodenPickaxe(bot);
17 | woodenPickaxe = bot.inventory.findInventoryItem(mcData.itemsByName["wooden_pickaxe"].id);
18 | }
19 |
20 | // Equip the wooden pickaxe if it exists
21 | if (woodenPickaxe) {
22 | await bot.equip(woodenPickaxe, "hand");
23 |
24 | // Explore until we find a stone block
25 | await exploreUntil(bot, new Vec3(1, -1, 1), 60, () => {
26 | const stone = bot.findBlock({
27 | matching: mcData.blocksByName["stone"].id,
28 | maxDistance: 32
29 | });
30 | if (stone) {
31 | return true;
32 | }
33 | });
34 |
35 | // Mine 8 cobblestone blocks using the wooden pickaxe
36 | bot.chat("Found a stone block. Mining 8 cobblestone blocks.");
37 | await mineBlock(bot, "stone", 8);
38 | bot.chat("Successfully mined 8 cobblestone blocks.");
39 |
40 | // Save the event of mining 8 cobblestone
41 | bot.save("cobblestone_mined");
42 | } else {
43 | bot.chat("Failed to craft a wooden pickaxe. Cannot mine cobblestone.");
44 | }
45 | }
46 |
47 | The main function is `mineCobblestone`.
48 |
49 | Then you would write:
50 |
51 | The function is about mining 8 cobblestones using a wooden pickaxe. First check if a wooden pickaxe is in the inventory. If not, craft one. If the wooden pickaxe is available, equip the wooden pickaxe in the hand. Next, explore the environment until finding a stone block. Once a stone block is found, mine a total of 8 cobblestone blocks using the wooden pickaxe.
--------------------------------------------------------------------------------
/voyager/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .file_utils import *
2 | from .json_utils import *
3 | from .record_utils import EventRecorder
4 |
--------------------------------------------------------------------------------
/voyager/utils/file_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | File system utils.
3 | """
4 | import collections
5 | import os
6 | import pickle
7 | import sys
8 | import errno
9 | import shutil
10 | import glob
11 |
12 | # import pwd
13 | import codecs
14 | import hashlib
15 | import tarfile
16 | import fnmatch
17 | import tempfile
18 | from datetime import datetime
19 | from socket import gethostname
20 | import logging
21 |
22 |
23 | f_ext = os.path.splitext
24 |
25 | f_size = os.path.getsize
26 |
27 | is_file = os.path.isfile
28 |
29 | is_dir = os.path.isdir
30 |
31 | get_dir = os.path.dirname
32 |
33 |
34 | def host_name():
35 | "Get host name, alias with ``socket.gethostname()``"
36 | return gethostname()
37 |
38 |
39 | def host_id():
40 | """
41 | Returns: first part of hostname up to '.'
42 | """
43 | return host_name().split(".")[0]
44 |
45 |
46 | def utf_open(fname, mode):
47 | """
48 | Wrapper for codecs.open
49 | """
50 | return codecs.open(fname, mode=mode, encoding="utf-8")
51 |
52 |
53 | def is_sequence(obj):
54 | """
55 | Returns:
56 | True if the sequence is a collections.Sequence and not a string.
57 | """
58 | return isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str)
59 |
60 |
61 | def pack_varargs(args):
62 | """
63 | Pack *args or a single list arg as list
64 |
65 | def f(*args):
66 | arg_list = pack_varargs(args)
67 | # arg_list is now packed as a list
68 | """
69 | assert isinstance(args, tuple), "please input the tuple `args` as in *args"
70 | if len(args) == 1 and is_sequence(args[0]):
71 | return args[0]
72 | else:
73 | return args
74 |
75 |
76 | def f_not_empty(*fpaths):
77 | """
78 | Returns:
79 | True if and only if the file exists and file size > 0
80 | if fpath is a dir, if and only if dir exists and has at least 1 file
81 | """
82 | fpath = f_join(*fpaths)
83 | if not os.path.exists(fpath):
84 | return False
85 |
86 | if os.path.isdir(fpath):
87 | return len(os.listdir(fpath)) > 0
88 | else:
89 | return os.path.getsize(fpath) > 0
90 |
91 |
92 | def f_expand(fpath):
93 | return os.path.expandvars(os.path.expanduser(fpath))
94 |
95 |
96 | def f_exists(*fpaths):
97 | return os.path.exists(f_join(*fpaths))
98 |
99 |
100 | def f_join(*fpaths):
101 | """
102 | join file paths and expand special symbols like `~` for home dir
103 | """
104 | fpaths = pack_varargs(fpaths)
105 | fpath = f_expand(os.path.join(*fpaths))
106 | if isinstance(fpath, str):
107 | fpath = fpath.strip()
108 | return fpath
109 |
110 |
111 | def f_listdir(
112 | *fpaths,
113 | filter_ext=None,
114 | filter=None,
115 | sort=True,
116 | full_path=False,
117 | nonexist_ok=True,
118 | recursive=False,
119 | ):
120 | """
121 | Args:
122 | full_path: True to return full paths to the dir contents
123 | filter: function that takes in file name and returns True to include
124 | nonexist_ok: True to return [] if the dir is non-existent, False to raise
125 | sort: sort the file names by alphabetical
126 | recursive: True to use os.walk to recursively list files. Note that `filter`
127 | will be applied to the relative path string to the root dir.
128 | e.g. filter will take "a/data1.txt" and "a/b/data3.txt" as input, instead of
129 | just the base file names "data1.txt" and "data3.txt".
130 | if False, will simply call os.listdir()
131 | """
132 | assert not (filter_ext and filter), "filter_ext and filter are mutually exclusive"
133 | dir_path = f_join(*fpaths)
134 | if not os.path.exists(dir_path) and nonexist_ok:
135 | return []
136 | if recursive:
137 | files = [
138 | os.path.join(os.path.relpath(root, dir_path), file)
139 | for root, _, files in os.walk(dir_path)
140 | for file in files
141 | ]
142 | else:
143 | files = os.listdir(dir_path)
144 | if filter is not None:
145 | files = [f for f in files if filter(f)]
146 | elif filter_ext is not None:
147 | files = [f for f in files if f.endswith(filter_ext)]
148 | if sort:
149 | files.sort()
150 | if full_path:
151 | return [os.path.join(dir_path, f) for f in files]
152 | else:
153 | return files
154 |
155 |
156 | def f_mkdir(*fpaths):
157 | """
158 | Recursively creates all the subdirs
159 | If exist, do nothing.
160 | """
161 | fpath = f_join(*fpaths)
162 | os.makedirs(fpath, exist_ok=True)
163 | return fpath
164 |
165 |
166 | def f_mkdir_in_path(*fpaths):
167 | """
168 | fpath is a file,
169 | recursively creates all the parent dirs that lead to the file
170 | If exist, do nothing.
171 | """
172 | os.makedirs(get_dir(f_join(*fpaths)), exist_ok=True)
173 |
174 |
175 | def last_part_in_path(fpath):
176 | """
177 | https://stackoverflow.com/questions/3925096/how-to-get-only-the-last-part-of-a-path-in-python
178 | """
179 | return os.path.basename(os.path.normpath(f_expand(fpath)))
180 |
181 |
182 | def is_abs_path(*fpath):
183 | return os.path.isabs(f_join(*fpath))
184 |
185 |
186 | def is_relative_path(*fpath):
187 | return not is_abs_path(f_join(*fpath))
188 |
189 |
190 | def f_time(*fpath):
191 | "File modification time"
192 | return str(os.path.getctime(f_join(*fpath)))
193 |
194 |
195 | def f_append_before_ext(fpath, suffix):
196 | """
197 | Append a suffix to file name and retain its extension
198 | """
199 | name, ext = f_ext(fpath)
200 | return name + suffix + ext
201 |
202 |
203 | def f_add_ext(fpath, ext):
204 | """
205 | Append an extension if not already there
206 | Args:
207 | ext: will add a preceding `.` if doesn't exist
208 | """
209 | if not ext.startswith("."):
210 | ext = "." + ext
211 | if fpath.endswith(ext):
212 | return fpath
213 | else:
214 | return fpath + ext
215 |
216 |
217 | def f_has_ext(fpath, ext):
218 | "Test if file path is a text file"
219 | _, actual_ext = f_ext(fpath)
220 | return actual_ext == "." + ext.lstrip(".")
221 |
222 |
223 | def f_glob(*fpath):
224 | return glob.glob(f_join(*fpath), recursive=True)
225 |
226 |
227 | def f_remove(*fpath, verbose=False, dry_run=False):
228 | """
229 | If exist, remove. Supports both dir and file. Supports glob wildcard.
230 | """
231 | assert isinstance(verbose, bool)
232 | fpath = f_join(fpath)
233 | if dry_run:
234 | print("Dry run, delete:", fpath)
235 | return
236 | for f in glob.glob(fpath):
237 | try:
238 | shutil.rmtree(f)
239 | except OSError as e:
240 | if e.errno == errno.ENOTDIR:
241 | try:
242 | os.remove(f)
243 | except: # final resort safeguard
244 | pass
245 | if verbose:
246 | print(f'Deleted "{fpath}"')
247 |
248 |
249 | def f_copy(fsrc, fdst, ignore=None, include=None, exists_ok=True, verbose=False):
250 | """
251 | Supports both dir and file. Supports glob wildcard.
252 | """
253 | fsrc, fdst = f_expand(fsrc), f_expand(fdst)
254 | for f in glob.glob(fsrc):
255 | try:
256 | f_copytree(f, fdst, ignore=ignore, include=include, exist_ok=exists_ok)
257 | except OSError as e:
258 | if e.errno == errno.ENOTDIR:
259 | shutil.copy(f, fdst)
260 | else:
261 | raise
262 | if verbose:
263 | print(f'Copied "{fsrc}" to "{fdst}"')
264 |
265 |
266 | def _f_copytree(
267 | src,
268 | dst,
269 | symlinks=False,
270 | ignore=None,
271 | exist_ok=True,
272 | copy_function=shutil.copy2,
273 | ignore_dangling_symlinks=False,
274 | ):
275 | """Copied from python standard lib shutil.copytree
276 | except that we allow exist_ok
277 | Use f_copytree as entry
278 | """
279 | names = os.listdir(src)
280 | if ignore is not None:
281 | ignored_names = ignore(src, names)
282 | else:
283 | ignored_names = set()
284 |
285 | os.makedirs(dst, exist_ok=exist_ok)
286 | errors = []
287 | for name in names:
288 | if name in ignored_names:
289 | continue
290 | srcname = os.path.join(src, name)
291 | dstname = os.path.join(dst, name)
292 | try:
293 | if os.path.islink(srcname):
294 | linkto = os.readlink(srcname)
295 | if symlinks:
296 | # We can't just leave it to `copy_function` because legacy
297 | # code with a custom `copy_function` may rely on copytree
298 | # doing the right thing.
299 | os.symlink(linkto, dstname)
300 | shutil.copystat(srcname, dstname, follow_symlinks=not symlinks)
301 | else:
302 | # ignore dangling symlink if the flag is on
303 | if not os.path.exists(linkto) and ignore_dangling_symlinks:
304 | continue
305 | # otherwise let the copy occurs. copy2 will raise an error
306 | if os.path.isdir(srcname):
307 | _f_copytree(
308 | srcname, dstname, symlinks, ignore, exist_ok, copy_function
309 | )
310 | else:
311 | copy_function(srcname, dstname)
312 | elif os.path.isdir(srcname):
313 | _f_copytree(srcname, dstname, symlinks, ignore, exist_ok, copy_function)
314 | else:
315 | # Will raise a SpecialFileError for unsupported file types
316 | copy_function(srcname, dstname)
317 | # catch the Error from the recursive copytree so that we can
318 | # continue with other files
319 | except shutil.Error as err:
320 | errors.extend(err.args[0])
321 | except OSError as why:
322 | errors.append((srcname, dstname, str(why)))
323 | try:
324 | shutil.copystat(src, dst)
325 | except OSError as why:
326 | # Copying file access times may fail on Windows
327 | if getattr(why, "winerror", None) is None:
328 | errors.append((src, dst, str(why)))
329 | if errors:
330 | raise shutil.Error(errors)
331 | return dst
332 |
333 |
334 | def _include_patterns(*patterns):
335 | """Factory function that can be used with copytree() ignore parameter.
336 |
337 | Arguments define a sequence of glob-style patterns
338 | that are used to specify what files to NOT ignore.
339 | Creates and returns a function that determines this for each directory
340 | in the file hierarchy rooted at the source directory when used with
341 | shutil.copytree().
342 | """
343 |
344 | def _ignore_patterns(path, names):
345 | keep = set(
346 | name for pattern in patterns for name in fnmatch.filter(names, pattern)
347 | )
348 | ignore = set(
349 | name
350 | for name in names
351 | if name not in keep and not os.path.isdir(os.path.join(path, name))
352 | )
353 | return ignore
354 |
355 | return _ignore_patterns
356 |
357 |
358 | def f_copytree(fsrc, fdst, symlinks=False, ignore=None, include=None, exist_ok=True):
359 | fsrc, fdst = f_expand(fsrc), f_expand(fdst)
360 | assert (ignore is None) or (
361 | include is None
362 | ), "ignore= and include= are mutually exclusive"
363 | if ignore:
364 | ignore = shutil.ignore_patterns(*ignore)
365 | elif include:
366 | ignore = _include_patterns(*include)
367 | _f_copytree(fsrc, fdst, ignore=ignore, symlinks=symlinks, exist_ok=exist_ok)
368 |
369 |
370 | def f_move(fsrc, fdst):
371 | fsrc, fdst = f_expand(fsrc), f_expand(fdst)
372 | for f in glob.glob(fsrc):
373 | shutil.move(f, fdst)
374 |
375 |
376 | def f_split_path(fpath, normpath=True):
377 | """
378 | Splits path into a list of its component folders
379 |
380 | Args:
381 | normpath: call os.path.normpath to remove redundant '/' and
382 | up-level references like ".."
383 | """
384 | if normpath:
385 | fpath = os.path.normpath(fpath)
386 | allparts = []
387 | while 1:
388 | parts = os.path.split(fpath)
389 | if parts[0] == fpath: # sentinel for absolute paths
390 | allparts.insert(0, parts[0])
391 | break
392 | elif parts[1] == fpath: # sentinel for relative paths
393 | allparts.insert(0, parts[1])
394 | break
395 | else:
396 | fpath = parts[0]
397 | allparts.insert(0, parts[1])
398 | return allparts
399 |
400 |
401 | def get_script_dir():
402 | """
403 | Returns: the dir of current script
404 | """
405 | return os.path.dirname(os.path.realpath(sys.argv[0]))
406 |
407 |
408 | def get_script_file_name():
409 | """
410 | Returns: the dir of current script
411 | """
412 | return os.path.basename(sys.argv[0])
413 |
414 |
415 | def get_script_self_path():
416 | """
417 | Returns: the dir of current script
418 | """
419 | return os.path.realpath(sys.argv[0])
420 |
421 |
422 | def get_parent_dir(location, abspath=False):
423 | """
424 | Args:
425 | location: current directory or file
426 |
427 | Returns:
428 | parent directory absolute or relative path
429 | """
430 | _path = os.path.abspath if abspath else os.path.relpath
431 | return _path(f_join(location, os.pardir))
432 |
433 |
434 | def md5_checksum(*fpath):
435 | """
436 | File md5 signature
437 | """
438 | hash_md5 = hashlib.md5()
439 | with open(f_join(*fpath), "rb") as f:
440 | for chunk in iter(lambda: f.read(65536), b""):
441 | hash_md5.update(chunk)
442 | return hash_md5.hexdigest()
443 |
444 |
445 | def create_tar(fsrc, output_tarball, include=None, ignore=None, compress_mode="gz"):
446 | """
447 | Args:
448 | fsrc: source file or folder
449 | output_tarball: output tar file name
450 | compress_mode: "gz", "bz2", "xz" or "" (empty for uncompressed write)
451 | include: include pattern, will trigger copy to temp directory
452 | ignore: ignore pattern, will trigger copy to temp directory
453 | """
454 | fsrc, output_tarball = f_expand(fsrc), f_expand(output_tarball)
455 | assert compress_mode in ["gz", "bz2", "xz", ""]
456 | src_base = os.path.basename(fsrc)
457 |
458 | tempdir = None
459 | if include or ignore:
460 | tempdir = tempfile.mkdtemp()
461 | tempdest = f_join(tempdir, src_base)
462 | f_copy(fsrc, tempdest, include=include, ignore=ignore)
463 | fsrc = tempdest
464 |
465 | with tarfile.open(output_tarball, "w:" + compress_mode) as tar:
466 | tar.add(fsrc, arcname=src_base)
467 |
468 | if tempdir:
469 | f_remove(tempdir)
470 |
471 |
472 | def extract_tar(source_tarball, output_dir=".", members=None):
473 | """
474 | Args:
475 | source_tarball: extract members from archive
476 | output_dir: default to current working dir
477 | members: must be a subset of the list returned by getmembers()
478 | """
479 | source_tarball, output_dir = f_expand(source_tarball), f_expand(output_dir)
480 | with tarfile.open(source_tarball, "r:*") as tar:
481 | tar.extractall(output_dir, members=members)
482 |
483 |
484 | def move_with_backup(*fpath, suffix=".bak"):
485 | """
486 | Ensures that a path is not occupied. If there is a file, rename it by
487 | adding @suffix. Resursively backs up everything.
488 |
489 | Args:
490 | fpath: file path to clear
491 | suffix: Add to backed up files (default: {'.bak'})
492 | """
493 | fpath = str(f_join(*fpath))
494 | if os.path.exists(fpath):
495 | move_with_backup(fpath + suffix)
496 | shutil.move(fpath, fpath + suffix)
497 |
498 |
499 | def insert_before_ext(name, insert):
500 | """
501 | log.txt -> log.ep50.txt
502 | """
503 | name, ext = os.path.splitext(name)
504 | return name + insert + ext
505 |
506 |
507 | def timestamp_file_name(fname):
508 | timestr = datetime.now().strftime("_%H-%M-%S_%m-%d-%y")
509 | return insert_before_ext(fname, timestr)
510 |
511 |
512 | def get_file_lock(*fpath, timeout: int = 15, logging_level="critical"):
513 | """
514 | NFS-safe filesystem-backed lock. `pip install flufl.lock`
515 | https://flufllock.readthedocs.io/en/stable/apiref.html
516 |
517 | Args:
518 | fpath: should be a path on NFS so that every process can see it
519 | timeout: seconds
520 | """
521 | from flufl.lock import Lock
522 |
523 | logging.getLogger("flufl.lock").setLevel(logging_level.upper())
524 | return Lock(f_join(*fpath), lifetime=timeout)
525 |
526 |
527 | def load_pickle(*fpaths):
528 | with open(f_join(*fpaths), "rb") as fp:
529 | return pickle.load(fp)
530 |
531 |
532 | def dump_pickle(data, *fpaths):
533 | with open(f_join(*fpaths), "wb") as fp:
534 | pickle.dump(data, fp)
535 |
536 |
537 | def load_text(*fpaths, by_lines=False):
538 | with open(f_join(*fpaths), "r") as fp:
539 | if by_lines:
540 | return fp.readlines()
541 | else:
542 | return fp.read()
543 |
544 |
545 | def load_text_lines(*fpaths):
546 | return load_text(*fpaths, by_lines=True)
547 |
548 |
549 | def dump_text(s, *fpaths):
550 | with open(f_join(*fpaths), "w") as fp:
551 | fp.write(s)
552 |
553 |
554 | def dump_text_lines(lines: list[str], *fpaths, add_newline=True):
555 | with open(f_join(*fpaths), "w") as fp:
556 | for line in lines:
557 | print(line, file=fp, end="\n" if add_newline else "")
558 |
559 |
560 | # aliases to be consistent with other load_* and dump_*
561 | pickle_load = load_pickle
562 | pickle_dump = dump_pickle
563 | text_load = load_text
564 | read_text = load_text
565 | read_text_lines = load_text_lines
566 | write_text = dump_text
567 | write_text_lines = dump_text_lines
568 | text_dump = dump_text
569 |
--------------------------------------------------------------------------------
/voyager/utils/json_utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | from typing import Any, Dict, Union
4 | from .file_utils import f_join
5 |
6 |
7 | def json_load(*file_path, **kwargs):
8 | file_path = f_join(file_path)
9 | with open(file_path, "r") as fp:
10 | return json.load(fp, **kwargs)
11 |
12 |
13 | def json_loads(string, **kwargs):
14 | return json.loads(string, **kwargs)
15 |
16 |
17 | def json_dump(data, *file_path, **kwargs):
18 | file_path = f_join(file_path)
19 | with open(file_path, "w") as fp:
20 | json.dump(data, fp, **kwargs)
21 |
22 |
23 | def json_dumps(data, **kwargs):
24 | """
25 | Returns: string
26 | """
27 | return json.dumps(data, **kwargs)
28 |
29 |
30 | # ---------------- Aliases -----------------
31 | # add aliases where verb goes first, json_load -> load_json
32 | load_json = json_load
33 | loads_json = json_loads
34 | dump_json = json_dump
35 | dumps_json = json_dumps
36 |
37 |
38 | def extract_char_position(error_message: str) -> int:
39 | """Extract the character position from the JSONDecodeError message.
40 | Args:
41 | error_message (str): The error message from the JSONDecodeError
42 | exception.
43 | Returns:
44 | int: The character position.
45 | """
46 | import re
47 |
48 | char_pattern = re.compile(r"\(char (\d+)\)")
49 | if match := char_pattern.search(error_message):
50 | return int(match[1])
51 | else:
52 | raise ValueError("Character position not found in the error message.")
53 |
54 |
55 | def add_quotes_to_property_names(json_string: str) -> str:
56 | """
57 | Add quotes to property names in a JSON string.
58 | Args:
59 | json_string (str): The JSON string.
60 | Returns:
61 | str: The JSON string with quotes added to property names.
62 | """
63 |
64 | def replace_func(match):
65 | return f'"{match.group(1)}":'
66 |
67 | property_name_pattern = re.compile(r"(\w+):")
68 | corrected_json_string = property_name_pattern.sub(replace_func, json_string)
69 |
70 | try:
71 | json.loads(corrected_json_string)
72 | return corrected_json_string
73 | except json.JSONDecodeError as e:
74 | raise e
75 |
76 |
77 | def balance_braces(json_string: str) -> str:
78 | """
79 | Balance the braces in a JSON string.
80 | Args:
81 | json_string (str): The JSON string.
82 | Returns:
83 | str: The JSON string with braces balanced.
84 | """
85 |
86 | open_braces_count = json_string.count("{")
87 | close_braces_count = json_string.count("}")
88 |
89 | while open_braces_count > close_braces_count:
90 | json_string += "}"
91 | close_braces_count += 1
92 |
93 | while close_braces_count > open_braces_count:
94 | json_string = json_string.rstrip("}")
95 | close_braces_count -= 1
96 |
97 | try:
98 | json.loads(json_string)
99 | return json_string
100 | except json.JSONDecodeError as e:
101 | raise e
102 |
103 |
104 | def fix_invalid_escape(json_str: str, error_message: str) -> str:
105 | while error_message.startswith("Invalid \\escape"):
106 | bad_escape_location = extract_char_position(error_message)
107 | json_str = json_str[:bad_escape_location] + json_str[bad_escape_location + 1 :]
108 | try:
109 | json.loads(json_str)
110 | return json_str
111 | except json.JSONDecodeError as e:
112 | error_message = str(e)
113 | return json_str
114 |
115 |
116 | def correct_json(json_str: str) -> str:
117 | """
118 | Correct common JSON errors.
119 | Args:
120 | json_str (str): The JSON string.
121 | """
122 |
123 | try:
124 | json.loads(json_str)
125 | return json_str
126 | except json.JSONDecodeError as e:
127 | error_message = str(e)
128 | if error_message.startswith("Invalid \\escape"):
129 | json_str = fix_invalid_escape(json_str, error_message)
130 | if error_message.startswith(
131 | "Expecting property name enclosed in double quotes"
132 | ):
133 | json_str = add_quotes_to_property_names(json_str)
134 | try:
135 | json.loads(json_str)
136 | return json_str
137 | except json.JSONDecodeError as e:
138 | error_message = str(e)
139 | if balanced_str := balance_braces(json_str):
140 | return balanced_str
141 | return json_str
142 |
143 |
144 | def fix_and_parse_json(
145 | json_str: str, try_to_fix_with_gpt: bool = True
146 | ) -> Union[str, Dict[Any, Any]]:
147 | """Fix and parse JSON string"""
148 | try:
149 | json_str = json_str.replace("\t", "")
150 | return json.loads(json_str)
151 | except json.JSONDecodeError as _: # noqa: F841
152 | json_str = correct_json(json_str)
153 | try:
154 | return json.loads(json_str)
155 | except json.JSONDecodeError as _: # noqa: F841
156 | pass
157 | # Let's do something manually:
158 | # sometimes GPT responds with something BEFORE the braces:
159 | # "I'm sorry, I don't understand. Please try again."
160 | # {"text": "I'm sorry, I don't understand. Please try again.",
161 | # "confidence": 0.0}
162 | # So let's try to find the first brace and then parse the rest
163 | # of the string
164 | try:
165 | brace_index = json_str.index("{")
166 | json_str = json_str[brace_index:]
167 | last_brace_index = json_str.rindex("}")
168 | json_str = json_str[: last_brace_index + 1]
169 | return json.loads(json_str)
170 | except json.JSONDecodeError as e: # noqa: F841
171 | # if try_to_fix_with_gpt:
172 | # print(
173 | # "Warning: Failed to parse AI output, attempting to fix."
174 | # "\n If you see this warning frequently, it's likely that"
175 | # " your prompt is confusing the AI. Try changing it up"
176 | # " slightly."
177 | # )
178 | # # Now try to fix this up using the ai_functions
179 | # ai_fixed_json = fix_json(json_str, JSON_SCHEMA)
180 | #
181 | # if ai_fixed_json != "failed":
182 | # return json.loads(ai_fixed_json)
183 | # else:
184 | # # This allows the AI to react to the error message,
185 | # # which usually results in it correcting its ways.
186 | # print("Failed to fix ai output, telling the AI.")
187 | # return json_str
188 | # else:
189 | raise e
190 |
191 |
192 | # def fix_json(json_str: str, schema: str) -> str:
193 | # """Fix the given JSON string to make it parseable and fully complient with the provided schema."""
194 | #
195 | # # Try to fix the JSON using gpt:
196 | # function_string = "def fix_json(json_str: str, schema:str=None) -> str:"
197 | # args = [f"'''{json_str}'''", f"'''{schema}'''"]
198 | # description_string = (
199 | # "Fixes the provided JSON string to make it parseable"
200 | # " and fully complient with the provided schema.\n If an object or"
201 | # " field specified in the schema isn't contained within the correct"
202 | # " JSON, it is ommited.\n This function is brilliant at guessing"
203 | # " when the format is incorrect."
204 | # )
205 | #
206 | # # If it doesn't already start with a "`", add one:
207 | # if not json_str.startswith("`"):
208 | # json_str = "```json\n" + json_str + "\n```"
209 | # result_string = call_ai_function(
210 | # function_string, args, description_string, model=cfg.fast_llm_model
211 | # )
212 | # if cfg.debug:
213 | # print("------------ JSON FIX ATTEMPT ---------------")
214 | # print(f"Original JSON: {json_str}")
215 | # print("-----------")
216 | # print(f"Fixed JSON: {result_string}")
217 | # print("----------- END OF FIX ATTEMPT ----------------")
218 | #
219 | # try:
220 | # json.loads(result_string) # just check the validity
221 | # return result_string
222 | # except: # noqa: E722
223 | # # Get the call stack:
224 | # # import traceback
225 | # # call_stack = traceback.format_exc()
226 | # # print(f"Failed to fix JSON: '{json_str}' "+call_stack)
227 | # return "failed"
228 |
--------------------------------------------------------------------------------
/voyager/utils/record_utils.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from .file_utils import *
4 | from .json_utils import *
5 |
6 |
7 | class EventRecorder:
8 | def __init__(
9 | self,
10 | ckpt_dir="ckpt",
11 | resume=False,
12 | init_position=None,
13 | ):
14 | self.ckpt_dir = ckpt_dir
15 | self.item_history = set()
16 | self.item_vs_time = {}
17 | self.item_vs_iter = {}
18 | self.biome_history = set()
19 | self.init_position = init_position
20 | self.position_history = [[0, 0]]
21 | self.elapsed_time = 0
22 | self.iteration = 0
23 | f_mkdir(self.ckpt_dir, "events")
24 | if resume:
25 | self.resume()
26 |
27 | def record(self, events, task):
28 | task = re.sub(r'[\\/:"*?<>| ]', "_", task)
29 | task = task.replace(" ", "_") + time.strftime(
30 | "_%Y%m%d_%H%M%S", time.localtime()
31 | )
32 | self.iteration += 1
33 | if not self.init_position:
34 | self.init_position = [
35 | events[0][1]["status"]["position"]["x"],
36 | events[0][1]["status"]["position"]["z"],
37 | ]
38 | for event_type, event in events:
39 | self.update_items(event)
40 | if event_type == "observe":
41 | self.update_elapsed_time(event)
42 | print(
43 | f"\033[96m****Recorder message: {self.elapsed_time} ticks have elapsed****\033[0m\n"
44 | f"\033[96m****Recorder message: {self.iteration} iteration passed****\033[0m"
45 | )
46 | dump_json(events, f_join(self.ckpt_dir, "events", task))
47 |
48 | def resume(self, cutoff=None):
49 | self.item_history = set()
50 | self.item_vs_time = {}
51 | self.item_vs_iter = {}
52 | self.elapsed_time = 0
53 | self.position_history = [[0, 0]]
54 |
55 | def get_timestamp(string):
56 | timestamp = "_".join(string.split("_")[-2:])
57 | return time.mktime(time.strptime(timestamp, "%Y%m%d_%H%M%S"))
58 |
59 | records = f_listdir(self.ckpt_dir, "events")
60 | sorted_records = sorted(records, key=get_timestamp)
61 | for record in sorted_records:
62 | self.iteration += 1
63 | if cutoff and self.iteration > cutoff:
64 | break
65 | events = load_json(f_join(self.ckpt_dir, "events", record))
66 | if not self.init_position:
67 | self.init_position = (
68 | events[0][1]["status"]["position"]["x"],
69 | events[0][1]["status"]["position"]["z"],
70 | )
71 | for event_type, event in events:
72 | self.update_items(event)
73 | self.update_position(event)
74 | if event_type == "observe":
75 | self.update_elapsed_time(event)
76 |
77 | def update_items(self, event):
78 | inventory = event["inventory"]
79 | elapsed_time = event["status"]["elapsedTime"]
80 | biome = event["status"]["biome"]
81 | items = set(inventory.keys())
82 | new_items = items - self.item_history
83 | self.item_history.update(items)
84 | self.biome_history.add(biome)
85 | if new_items:
86 | if self.elapsed_time + elapsed_time not in self.item_vs_time:
87 | self.item_vs_time[self.elapsed_time + elapsed_time] = []
88 | self.item_vs_time[self.elapsed_time + elapsed_time].extend(new_items)
89 | if self.iteration not in self.item_vs_iter:
90 | self.item_vs_iter[self.iteration] = []
91 | self.item_vs_iter[self.iteration].extend(new_items)
92 |
93 | def update_elapsed_time(self, event):
94 | self.elapsed_time += event["status"]["elapsedTime"]
95 |
96 | def update_position(self, event):
97 | position = [
98 | event["status"]["position"]["x"] - self.init_position[0],
99 | event["status"]["position"]["z"] - self.init_position[1],
100 | ]
101 | if self.position_history[-1] != position:
102 | self.position_history.append(position)
103 |
--------------------------------------------------------------------------------