diff --git a/.gitignore b/.gitignore index 5e34b04..17be9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ build/ dist/ pyncraft.egg-info -build.txt \ No newline at end of file +build.txt +*.pyc \ No newline at end of file diff --git a/README.md b/README.md index 7b78d95..bcac2f6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,152 @@ -A Python API to Minecraft that works with the [FruitJuice](https://github.com/jdeast/FruitJuice) Bukkit plugin to enable a python and/or scratch programming interface. +# PynCraft -See [FruitJuice/README_server_setup.md](https://github.com/jdeast/FruitJuice/blob/master/README_server_setup.md) for instructions to set up your own python/scratch server that works on java or bedrock. \ No newline at end of file +A Python API to Minecraft that works with the +[FruitJuice](https://github.com/jdeast/FruitJuice) +PaperMC plugin to enable a python and/or scratch programming interface. It can +also use the `RCON` protocol to issue Minecraft commands from python. + +## Server Setup + +See [FruitJuice/server_setup/README_ssetup.md](https://github.com/jdeast/FruitJuice/blob/master/server_setup/README_setup.md) +for instructions for setting up a python/scratch server that works on java +or bedrock using Docker. It runs on Minecraft version 1.20.6, but can +be accessed from newer clients (1.21 as of May 2025) using the +[ViaVersion](https://viaversion.com/index.html) plugin. In addition to +`FruitJuice`, the server supports the `RCON` (remote console) protocol. + +Don't worry-- it's pretty easy. You can skip this step if someone has +already configured a FruitJuice server for you. In that case, you just +need to get its IP address or URL. + +## Development Environment + +You will need to set up some software for coding. + +### Python + +You will need a recent (3.9 or higher) version of python running on your +computer. One easy way to install it is from this link: + +https://www.python.org/downloads/ + +### Visual Studio Code (VSCode) + +VSCode is a free Integrated Development Environment (IDE) application that +may be used for developing code. It has a lot of nice features, like +an editor that colors your python code and checks for errors, a debugger, +an integrated terminal, a Docker plugin and tools for connecting to Docker +containers so you can write code for them while they're running. It even +has AI built in to help you write code! It's free-- give it a try! + +Download VSCode for your operating system +[from here](https://code.visualstudio.com/download). +Launch VSCode, then "Open Directory" and pick (or create) `minecraft/code` +in your home directory. You will then need to install extensions to make +it work with python and Jupyter notebooks. + + +On the left edge, you will see a column of icons. From the top, they are: +
+
+ VSCode icons +
+
+ +
+
+ +Click on the File Extensions icon (four squares with the upper right one offset), +then search for "python". Choose the "Python" package from Microsoft, and +it will install the tools you need. + +Then search for "Jupyter" and select the "Jupyter" extension from +Microsoft. This will install support for Jupyter notebooks, a popular way of +interacting with python where you can combine nicely-formatted documentation +with "cells" where you run code. + +Then click on the **File Explorer** icon at the top. The panel next to these +icons will show you the files in your project. If you click on one of them, +it will open in the editor to the right. This is where you will work on your +code. You can have multiple editor windows open at once; they will appear +as different tabs. You can also split the editor panels horizontally or +vertically so you can see multiple files, or multiple parts of the same file +at once. + +At the top right of the VSCode screen, you will see these icons: + +![alt text](assets/vscode-panel-icons.png) + +The one with the horizontal split that is highlighted on the bottom (second +from right) will toggle the a panel at the bottom. This is how you can +access a terminal. By default, it will show whatever your default Terminal +type is-- `bash`, `zsh`, `Powershell`, etc. You can use this to interact +with the file system, or to run operating system commands. + +### Git + +This is used to download open source code from the GitHub and other +repositories. + +## Getting Started + +To get started, install `pyncraft` into your python environment: +``` +pip install pyncraft +``` +In your python script or Jupyter notebook, import `pyncraft`, then create +a `Minecraft` instance: +``` +from pyncraft import Minecraft + +# replace 'localhost' with the server IP address if the server +# is running on a different computer +address = 'localhost' + +# put your player name in the quotes +player_name = "your-minecraft-player-name" + +# this is the default unless you changed it +juice_port = 4711 + +# mc will be your connection to Minecraft +mc = Minecraft.create(address=address, port=juice_port, playerName=player_name) +``` + +You can use help to see what commands are available: +``` +help(mc) +``` + +## History + +- The Minecraft: Pi edition Python library was originally created by Mojang +and released with Minecraft: Pi edition. Initial supported was provided for +Python 2 only, but during a sprint at PyconUK 2014 it was migrated to Python 3 +and [py3minepi](https://github.com/py3minepi/py3minepi) was created. + +- The [RaspberryJam](https://github.com/arpruss/raspberryjammod) mod was +developed by arpruss for Minecraft 1.8 on the Raspberry Pi. + +- The [RaspberryJuice](https://github.com/zhuowei/RaspberryJuice) plugin +supported Minecraft Java Edition. Martin O'Hanlon developed it and maintained +it through 2019. + +- After that the [MinecraftDawn fork](https://github.com/zhuowei/RaspberryJuice). +was maintained for a few years, but has not been very active. + +- The present repo is a fork of that. The `FruitJuice` plugin was originally +developed for Bukkit/Spiget, which are no longer maintained. This fork uses +PaperMC instead. It also supports Bedrock Edition. + +- Martin O'Hanlon wrote the python package +[mcpi](https://github.com/martinohanlon/mcpi), as well as the book +"Adventures in Minecraft" (with David Whale, 2014). This is the predecessor of +`PynCraft`. diff --git a/assets/vscode-panel-icons.png b/assets/vscode-panel-icons.png new file mode 100644 index 0000000..de49d56 Binary files /dev/null and b/assets/vscode-panel-icons.png differ diff --git a/assets/vscode-sidebar-icons.png b/assets/vscode-sidebar-icons.png new file mode 100644 index 0000000..afee647 Binary files /dev/null and b/assets/vscode-sidebar-icons.png differ diff --git a/examples/pyncraft-notebook.ipynb b/examples/pyncraft-notebook.ipynb new file mode 100644 index 0000000..6886860 --- /dev/null +++ b/examples/pyncraft-notebook.ipynb @@ -0,0 +1,3583 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1f4bcb92", + "metadata": {}, + "source": [ + "# PynCraft Development and Testing\n", + "\n", + "Notebook for testing changes to the `FruitJuice` (java), `pyncraft`, which \n", + "includes using `rcon` and `mcipc` packages as dependencies. Also played\n", + "around with the `mciwb` and `mcwb` packages. You probably want to step \n", + "through it and run individual cells, not just run all.\n", + "\n", + "The notebook has been run in a python 3.11.12 virtual environment. Note that \n", + "you probably don't want to create the environment within this repo. But if\n", + "you're using VSCode, you should be able to access it from whereever it lives.\n", + "```\n", + "python -m venv venv-pycraft\n", + "source venv-pycraft/bin/activate\n", + "# note that mciwb installs rcon, mcipc and mcwb as dependencies\n", + "pip install \\\n", + " html_table_parser \\\n", + " ipykernel \\\n", + " mciwb \\\n", + " \"numpy<2.0\" \\\n", + " \"pandas<2.0\"\n", + "# add pyncraft in editable mode (adjust the path to local pyncraft below)\n", + "pip install -e pyncraft\n", + "```\n", + "\n", + "A Minecraft Java Edition server is being run from a local docker container.\n", + "Connection information is provided below. The server is being launched using\n", + "`docker compose up`. Ensure that the configuration in the `docker-compose.yml`\n", + "is consistent with the values in this notebook. It is currently using the\n", + "`FruitJuice_0.4.0b.jar` (2025-05-23) plugin. The world SEED is set to \n", + "`9064150133272194`, so if you step through the notebook, you shouldn't \n", + "unexpectedly teleport into a mountain or something.\n", + "\n", + "You may want to adjust video settings for your client to make the world\n", + "render faster-- set \"Graphics: Fast\" is the simplest way." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d5073dce", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "import json\n", + "import math\n", + "import os\n", + "from pathlib import Path\n", + "import re\n", + "import time\n", + "from typing import Callable, Iterable, Union, Optional\n", + "\n", + "import altair as alt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "# from mcipc.rcon.je.client import Client\n", + "from mcipc.rcon.je import Biome, Structure, Client\n", + "# from mcipc.rcon.types import Vec3\n", + "from mciwb.server import MinecraftServer\n", + "# from mcwb import Vec3\n", + "from mciwb.nbt import parse_nbt\n", + "\n", + "from mcipc.rcon.item import Item\n", + "from pyncraft.minecraft import Minecraft \n", + "from pyncraft.vec3 import Vec3\n", + "from pyncraft.coord import Coord, Vec3D\n", + "\n", + "\n", + "# notebook configuration options\n", + "alt.data_transformers.disable_max_rows()\n", + "\n", + "pd.set_option('display.max_columns', None) # or 1000\n", + "pd.set_option('display.max_rows', None) # or 1000\n", + "pd.set_option('display.max_colwidth', None) # or 199\n", + "\n", + "# block materials (types) from mcipc as a dictionary\n", + "ITEMS = {n: m for n, m in Item.__members__.items()}" + ] + }, + { + "cell_type": "markdown", + "id": "f75a363e", + "metadata": {}, + "source": [ + "## Server connection parameters\n", + "- These must be consistent with server config in the `docker-compose.yml` file." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6afefd98", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: you must add the player to the OPS list to use many RCON commands!\n", + "player_name = 'Acaimo'\n", + "juice_port = 4711\n", + "\n", + "# these only matter if you are using the RCON client\n", + "server_name = 'minecraft-server'\n", + "server_port = 25565\n", + "rcon_pw = 'aspergers-with-cheese'\n", + "rcon_port = 25575\n", + "\n", + "# NOTE: you must set the environment variables to values in docker-compose.yml\n", + "os.environ['RCON_PASSWORD'] = rcon_pw\n", + "os.environ['RCON_PORT'] = str(rcon_port)" + ] + }, + { + "cell_type": "markdown", + "id": "81b6ccf1", + "metadata": {}, + "source": [ + "## Coord and Vec3D Classes\n", + "- Leverage Numpy to avoid reinventing the wheel.\n", + "- Treat Vectors as separate objects from Coordinates" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3f391e5f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
anglexyzstringrotationdegreesradianscoord.xcoord.ycoord.z
00.00.00-10000.0north00.00.0000000.00.0-1.0
17.51305.00-9914.0north00.00.0000000.00.0-1.0
215.02588.00-9659.0north00.00.0000000.00.0-1.0
322.53827.00-9239.0north00.00.0000000.00.0-1.0
430.05000.00-8660.0north00.00.0000000.00.0-1.0
537.56088.00-7934.0north00.00.0000000.00.0-1.0
645.07071.00-7071.0north00.00.0000000.00.0-1.0
752.57934.00-6088.0east490.01.5707961.00.00.0
860.08660.00-5000.0east490.01.5707961.00.00.0
967.59239.00-3827.0east490.01.5707961.00.00.0
1075.09659.00-2588.0east490.01.5707961.00.00.0
1182.59914.00-1305.0east490.01.5707961.00.00.0
\n", + "
" + ], + "text/plain": [ + " angle x y z string rotation degrees radians coord.x \\\n", + "0 0.0 0.0 0 -10000.0 north 0 0.0 0.000000 0.0 \n", + "1 7.5 1305.0 0 -9914.0 north 0 0.0 0.000000 0.0 \n", + "2 15.0 2588.0 0 -9659.0 north 0 0.0 0.000000 0.0 \n", + "3 22.5 3827.0 0 -9239.0 north 0 0.0 0.000000 0.0 \n", + "4 30.0 5000.0 0 -8660.0 north 0 0.0 0.000000 0.0 \n", + "5 37.5 6088.0 0 -7934.0 north 0 0.0 0.000000 0.0 \n", + "6 45.0 7071.0 0 -7071.0 north 0 0.0 0.000000 0.0 \n", + "7 52.5 7934.0 0 -6088.0 east 4 90.0 1.570796 1.0 \n", + "8 60.0 8660.0 0 -5000.0 east 4 90.0 1.570796 1.0 \n", + "9 67.5 9239.0 0 -3827.0 east 4 90.0 1.570796 1.0 \n", + "10 75.0 9659.0 0 -2588.0 east 4 90.0 1.570796 1.0 \n", + "11 82.5 9914.0 0 -1305.0 east 4 90.0 1.570796 1.0 \n", + "\n", + " coord.y coord.z \n", + "0 0.0 -1.0 \n", + "1 0.0 -1.0 \n", + "2 0.0 -1.0 \n", + "3 0.0 -1.0 \n", + "4 0.0 -1.0 \n", + "5 0.0 -1.0 \n", + "6 0.0 -1.0 \n", + "7 0.0 0.0 \n", + "8 0.0 0.0 \n", + "9 0.0 0.0 \n", + "10 0.0 0.0 \n", + "11 0.0 0.0 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# some testing of Vec3.cardinal_direction() method\n", + "r = 10000.0\n", + "angles = np.linspace(0, 2 * np.pi / 4, 12, endpoint=False)\n", + "df = pd.DataFrame({\n", + " 'angle': 180 * angles / np.pi,\n", + " 'x': np.round(r * np.sin(angles)),\n", + " 'y': [0] * len(angles),\n", + " 'z': np.round(-r * np.cos(angles)),\n", + " 'string': [''] * len(angles),\n", + " 'rotation': [0] * len(angles),\n", + " 'degrees': [0.0] * len(angles),\n", + " 'radians': [0.0] * len(angles),\n", + " 'coord.x': [0.0] * len(angles),\n", + " 'coord.y': [0.0] * len(angles),\n", + " 'coord.z': [0.0] * len(angles),\n", + "})\n", + "\n", + "cp = 4\n", + "for i in df.index:\n", + " # v = Vec3D(Coord(df.x[i], df.y[i], df.z[i]))\n", + " v = Vec3D(Coord(df.loc[i, ['x', 'y', 'z']]))\n", + " df.loc[i, 'string'] = v.cardinal_direction(returns='string', compass_points=cp)\n", + " df.loc[i, 'rotation'] = v.cardinal_direction(returns='rotation', compass_points=cp)\n", + " df.loc[i, 'degrees'] = v.cardinal_direction(returns='degrees', compass_points=cp)\n", + " df.loc[i, 'radians'] = v.cardinal_direction(returns='radians', compass_points=cp)\n", + " df.loc[i, [f'coord.{x}' for x in 'xyz']] = \\\n", + " v.cardinal_direction(returns='coord', compass_points=cp).round(3)\n", + "\n", + "display(df)" + ] + }, + { + "cell_type": "markdown", + "id": "9fbf5f4b", + "metadata": {}, + "source": [ + "## Connect to Minecraft Server with FruitJuice plugin\n", + "\n", + "#### note that player must be connected to the server to connect" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "51e7bffc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[96m ('Running Python version: 3.11.12 (main, Apr 21 2025, 17:46:22) [Clang 17.0.0 (clang-1700.0.13.3)]',) \u001b[0m\n", + "\u001b[96m ('get Acaimo playerid=36',) \u001b[0m\n", + "['Seed: [9064150133272194]']\n" + ] + } + ], + "source": [ + "mc = Minecraft.create('localhost', port=juice_port, playerName=player_name)\n", + "mc.postToChat('Hello World from Python!')\n", + "\n", + "# assuming seed is `9064150133272194`\n", + "print(mc.runCommands('seed'))\n", + "\n", + "spawn_point = (-4.5, 109, 5.5)" + ] + }, + { + "cell_type": "markdown", + "id": "b692e3d1", + "metadata": {}, + "source": [ + "### Test out some of the PynCraft `Minecraft` methods\n", + "#### you can use help to see methods available" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1231885a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on CmdPlayer in module pyncraft.minecraft object:\n", + "\n", + "class CmdPlayer(CmdPositioner)\n", + " | CmdPlayer(connection, playerId)\n", + " | \n", + " | Methods for the host player\n", + " | \n", + " | Method resolution order:\n", + " | CmdPlayer\n", + " | CmdPositioner\n", + " | builtins.object\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, connection, playerId)\n", + " | Initialize self. See help(type(self)) for accurate signature.\n", + " | \n", + " | getDirection(self) -> pyncraft.vec3.Vec3\n", + " | Get direction of the entity\n", + " | \n", + " | getFoodLevel(self) -> int\n", + " | \n", + " | getHealth(self) -> float\n", + " | \n", + " | getPitch(self) -> float\n", + " | Get pitch if the entity\n", + " | \n", + " | getPos(self) -> pyncraft.vec3.Vec3\n", + " | Get entity position (entityId:int) => Vec3\n", + " | \n", + " | getRotation(self) -> float\n", + " | Get rotation if the entity\n", + " | \n", + " | getTilePos(self) -> pyncraft.vec3.Vec3\n", + " | Get entity tile position (entityId:int) => Vec3\n", + " | \n", + " | sendTitle(self, title: str, subTitle: str = '', fadeIn: int = 10, stay: int = 70, fadeOut: int = 20) -> None\n", + " | \n", + " | setDirection(self, x: float, y: float, z: float) -> None\n", + " | Set direction of the entity\n", + " | \n", + " | setFoodLevel(self, foodLevel: int) -> None\n", + " | \n", + " | setHealth(self, health: float) -> None\n", + " | \n", + " | setPitch(self, pitch) -> None\n", + " | Set pitch if the entity\n", + " | \n", + " | setPos(self, x: float, y: float, z: float) -> None\n", + " | Set entity position (entityId:int, x,y,z)\n", + " | \n", + " | setRotation(self, yaw) -> None\n", + " | Set rotation if the entity\n", + " | \n", + " | setTilePos(self, x: int, y: int, z: int) -> None\n", + " | Set entity tile position (entityId:int) => Vec3\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Methods inherited from CmdPositioner:\n", + " | \n", + " | setting(self, setting, status) -> None\n", + " | Set a player setting (setting, status). keys: autojump\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors inherited from CmdPositioner:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object\n", + "\n" + ] + } + ], + "source": [ + "help(mc.player)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "62a57777", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "XYZ: [ -0.5 104. -1.5]\n", + "Block: [ -1 104 -2]\n", + "Facing: east (115.8 / 23.9)\n" + ] + } + ], + "source": [ + "facing = Vec3D(mc.player.getDirection()).cardinal_direction()\n", + "pitch = np.round(mc.player.getPitch(), 1)\n", + "rotation = np.round(mc.player.getRotation(), 1)\n", + "print(f'XYZ: {Vec3D(mc.player.getPos()).round(1)}')\n", + "print(f'Block: {Vec3D(mc.player.getTilePos()).round(1)}')\n", + "print(f'Facing: {facing} ({rotation} / {pitch})')" + ] + }, + { + "cell_type": "markdown", + "id": "48087705", + "metadata": {}, + "source": [ + "### `mc.player` methods\n", + "\n", + "these work:\n", + "- getFoodLevel\n", + "- getHealth\n", + "- setFoodLevel\n", + "- setHealth\n", + "- getDirection\n", + "- getPitch\n", + "- getPos\n", + "- getRotation\n", + "- getTilePos\n", + "\n", + "don't work:\n", + "- setDirection (always sets to east, pitch=0)\n", + "- setPitch (always sets to directly down, pitch=90)\n", + "- setPos\n", + "- setRotation (always sets to same value, -134.4, no effect on pitch)\n", + "- setTilePos\n", + "\n", + "huh? - not sure what these are supposed to do:\n", + "- sendTitle\n", + "- setting\n", + "\n", + "Try to fix these in next commit (update Bukkit in FruitJuice?)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b10f615b", + "metadata": {}, + "outputs": [], + "source": [ + "# mc.player.setPos(*spawn_point)\n", + "# teleport(*spawn_point)\n", + "\n", + "# mc.player.setDirection(0.0, 0.0, 1.0)\n", + "# mc.player.setRotation(0. * np.pi / 180.)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2d4def3c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mc:\n", + "--------------------------\n", + "attributes:\n", + "--------------------\n", + "camera\n", + "cmdplayer\n", + "conn\n", + "entity\n", + "events\n", + "player\n", + "playerId\n", + "settings\n", + "\n", + "methods:\n", + "--------------------\n", + "create\n", + "createExplosion\n", + "getBlock\n", + "getBlockWithData\n", + "getBlocks\n", + "getHeight\n", + "getPlayerEntityId\n", + "getPlayerEntityIds\n", + "postToChat\n", + "runCommands\n", + "setBlock\n", + "setBlocks\n", + "setSign\n", + "setWallSign\n", + "setting\n", + "spawnEntity\n", + "mc.settings:\n", + "--------------------------\n", + "attributes:\n", + "--------------------\n", + "SHOW_DEBUG\n", + "SHOW_Log\n", + "SYS_SPEED\n", + "\n", + "methods:\n", + "--------------------\n", + "Speed\n", + "\n", + "mc.player:\n", + "--------------------------\n", + "attributes:\n", + "--------------------\n", + "conn\n", + "pkg\n", + "playerId\n", + "\n", + "methods:\n", + "--------------------\n", + "getDirection\n", + "getFoodLevel\n", + "getHealth\n", + "getPitch\n", + "getPos\n", + "getRotation\n", + "getTilePos\n", + "sendTitle\n", + "setDirection\n", + "setFoodLevel\n", + "setHealth\n", + "setPitch\n", + "setPos\n", + "setRotation\n", + "setTilePos\n", + "setting\n" + ] + } + ], + "source": [ + "def object_info(obj):\n", + " \"\"\"list attributes and methods of an object\"\"\"\n", + " print('attributes:\\n' + '-' * 20)\n", + " print('\\n'.join(\n", + " sorted([x for x in dir(obj) if not x.startswith('_') \n", + " and not isinstance(getattr(obj, x), Callable)])))\n", + "\n", + " print('\\nmethods:\\n' + '-' * 20)\n", + " print('\\n'.join(\n", + " sorted([x for x in dir(obj) if not x.startswith('_') \n", + " and isinstance(getattr(obj, x), Callable)])))\n", + " \n", + "\n", + "# check out Minecraft object's attributes and methods\n", + "print('mc:\\n--------------------------')\n", + "object_info(mc)\n", + "\n", + "print('mc.settings:\\n--------------------------')\n", + "object_info(mc.settings)\n", + "\n", + "print('\\nmc.player:\\n--------------------------')\n", + "object_info(mc.player)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b1c08907", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "world height at x=50, z=-160: 62\n", + "\n" + ] + } + ], + "source": [ + "print(f'world height at x=50, z=-160: {mc.getHeight(50, -160)}')\n", + "print(mc.settings)" + ] + }, + { + "cell_type": "markdown", + "id": "d18999da", + "metadata": {}, + "source": [ + "### Test whether using `runCommands()` via the RCON protocol works" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1489dbe1", + "metadata": {}, + "outputs": [], + "source": [ + "def teleport(pos: Iterable):\n", + " \"\"\"\n", + " Teleport the player to a given position.\n", + "\n", + " Note: mc.player.setTilePos() doesn't seem to work correctly.\n", + " \n", + " Parameters\n", + " ----------\n", + " pos : Iterable\n", + " An iterable of 3 floats representing the x, y, and z coordinates.\n", + " \"\"\"\n", + " \n", + " pos = Coord(pos)\n", + " here = Coord(mc.player.getTilePos()).midblock()\n", + "\n", + " there = [f'{float(x)}' for x in pos]\n", + " there = ' '.join(there)\n", + " result = mc.runCommands(f'tp {player_name} {there}')[0]\n", + "\n", + " if result.startswith('Teleported'):\n", + " msg = f'Teleported {player_name} from {here} to {pos}'\n", + " print(msg.replace(' ', ' ').replace('[ ', '['))\n", + " return " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6e54bb2b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-0.5 104. -1.5] to [-4.5 109. 5.5]\n" + ] + } + ], + "source": [ + "teleport(spawn_point)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "05b0dfa7", + "metadata": {}, + "outputs": [], + "source": [ + "# stop sending command output to the player's chat\n", + "mc.runCommands('gamerule sendCommandFeedback false')\n", + "for i in range(20):\n", + " mc.postToChat('')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e1bf5acd", + "metadata": {}, + "outputs": [], + "source": [ + "def dawn(self):\n", + " self.runCommands(['time set 2350s', 'weather clear'])\n", + " \n", + "dawn(mc)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "141f9fa6", + "metadata": {}, + "outputs": [], + "source": [ + "# cycle through the hours of the day, 1 hr per second\n", + "results = mc.runCommands(\n", + " [f'time set {1000 * ((x + 6) % 24)}' for x in range(0, 24)], 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "887ad253", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[\"Target doesn't have the requested effect\",\n", + " 'Applied effect Night Vision to Acaimo']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# give yourself night vision\n", + "mc.runCommands([\n", + " 'effect clear @p minecraft:night_vision',\n", + " 'effect give @p minecraft:night_vision 1000000 128 true'\n", + " ])" + ] + }, + { + "cell_type": "markdown", + "id": "99b114c4", + "metadata": {}, + "source": [ + "### Test my extensions of the Fruit Juice plugins\n", + "- `world.getBlockData`: returns BlockState as a string with material and \n", + " all other attributes (which vary by material)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "351fd85b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-4.5 109. 5.5] to [11.5 91. -10.5]\n", + "warning: unknown material: minecraft:pink_petals\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xyzmaterialstate
01192-11AIR
11191-11PINK_PETALSfacing=south,flower_amount=1
21190-11GRASS_BLOCKsnowy=false
31189-11DIRT
41188-11DIRT
51187-11DIORITE
61186-11DIORITE
71185-11DIRT
81184-11DIRT
91183-11DIRT
101182-11DIRT
111181-11STONE
121180-11STONE
\n", + "
" + ], + "text/plain": [ + " x y z material state\n", + "0 11 92 -11 AIR \n", + "1 11 91 -11 PINK_PETALS facing=south,flower_amount=1\n", + "2 11 90 -11 GRASS_BLOCK snowy=false\n", + "3 11 89 -11 DIRT \n", + "4 11 88 -11 DIRT \n", + "5 11 87 -11 DIORITE \n", + "6 11 86 -11 DIORITE \n", + "7 11 85 -11 DIRT \n", + "8 11 84 -11 DIRT \n", + "9 11 83 -11 DIRT \n", + "10 11 82 -11 DIRT \n", + "11 11 81 -11 STONE \n", + "12 11 80 -11 STONE " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "teleport(Coord(11.5, 91, -10.5))\n", + "pos = mc.player.getTilePos()\n", + "\n", + "blockdata = []\n", + "for dy in range(1, -12, -1):\n", + " # pos.y -= 1\n", + " blockdata.append(mc.getBlockWithData(pos.x, pos.y + dy, pos.z, parse=False))\n", + "\n", + "\n", + "blockdata = pd.DataFrame(blockdata)\n", + "blockdata#.fillna('')\n", + "\n", + "# mc.getBlockWithData(mc, pos)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "6466f551", + "metadata": {}, + "outputs": [], + "source": [ + "def getBlocksDf(\n", + " self, \n", + " x1: int, \n", + " y1: int, \n", + " z1: int, \n", + " x2: int, \n", + " y2: int, \n", + " z2: int,\n", + " with_state: bool = False\n", + " ) -> pd.DataFrame:\n", + " \"\"\"\n", + " Get materials of a block cuboid as a Pandas DataFrame, specified by corners.\n", + "\n", + " Parameters\n", + " ----------\n", + " x1, y1, z1 : int\n", + " Coordinates of one corner of the cuboid.\n", + " x2, y2, z2 : int\n", + " Coordinates of the opposite corner of the cuboid (inclusive).\n", + " with_state : bool, optional\n", + " If True, also get the block state as a string (default is False).\n", + " Note that this will slow down the function significantly.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " A DataFrame with columns 'x', 'y', 'z', and 'material'.\n", + " The material is a string representation of the block.\n", + " \"\"\"\n", + "\n", + " start, stop = zip(sorted([x2, x1]), sorted([y1, y2]), sorted([z2, z1]))\n", + " blocks = self.conn.sendReceive(b\"world.getBlocks\", *start, *stop).split(',')\n", + " \n", + " x = np.arange(start[0], stop[0]+1)\n", + " y = np.arange(start[1], stop[1]+1)\n", + " z = np.arange(start[2], stop[2]+1)\n", + " x1, y1, z1 = np.meshgrid(x, y, z)\n", + " \n", + " df = pd.DataFrame({\n", + " 'x': x1.flatten(), \n", + " 'y': y1.flatten(), \n", + " 'z': z1.flatten(),\n", + " 'material': blocks})\n", + " \n", + " if with_state:\n", + " df['state'] = ''\n", + " df = df.set_index(['x', 'y', 'z']).sort_index()\n", + " # get the block data for each block\n", + " for v3 in df.index:\n", + " if df.loc[v3, 'material'] != 'AIR':\n", + " state = self.getBlockWithData(*v3, parse=False)['state']\n", + " df.loc[v3, 'state'] = state\n", + " \n", + " df.reset_index(inplace=True)\n", + "\n", + " return df\n", + "\n", + "\n", + "# add this to the Minecraft object (should add it to the class)\n", + "mc.getBlocksDf = getBlocksDf" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "85230dae", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 11 91 -11]\n" + ] + }, + { + "data": { + "text/plain": [ + "[[['GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK'],\n", + " ['GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK', 'GRASS_BLOCK']],\n", + " [['OXEYE_DAISY', 'AIR', 'PINK_PETALS', 'SHORT_GRASS', 'AIR'],\n", + " ['AIR', 'AIR', 'DANDELION', 'DANDELION', 'AIR']],\n", + " [['AIR', 'AIR', 'DANDELION', 'PINK_PETALS', 'SHORT_GRASS'],\n", + " ['SHORT_GRASS', 'AIR', 'PINK_PETALS', 'AIR', 'PINK_PETALS']],\n", + " [['AIR', 'AIR', 'PINK_PETALS', 'PINK_PETALS', 'PINK_PETALS'],\n", + " ['AIR', 'AIR', 'AIR', 'AIR', 'AIR']],\n", + " [['AIR', 'AIR', 'AIR', 'AIR', 'AIR'], ['AIR', 'AIR', 'AIR', 'AIR', 'AIR']]]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# use juice getBlocks to get materials of a cuboid as an array of strings\n", + "pos = Coord(mc.player.getTilePos()).block()\n", + "print(pos)\n", + "here = (pos.x-2, pos.y, pos.z-2)\n", + "there = (pos.x+2, pos.y+1, pos.z+2)\n", + "\n", + "mc.getBlocks(*here, *there)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "b8e5a675", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9 91 -13 13 92 -9\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:pink_petals\n", + "warning: unknown material: minecraft:short_grass\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xyzmaterialstate
0991-13GRASS_BLOCKsnowy=false
1991-12GRASS_BLOCKsnowy=false
2991-11GRASS_BLOCKsnowy=false
3991-10GRASS_BLOCKsnowy=false
4991-9GRASS_BLOCKsnowy=false
5992-13SHORT_GRASS
7992-11PINK_PETALSfacing=south,flower_amount=2
9992-9PINK_PETALSfacing=west,flower_amount=3
101091-13GRASS_BLOCKsnowy=false
111091-12GRASS_BLOCKsnowy=false
121091-11GRASS_BLOCKsnowy=false
131091-10GRASS_BLOCKsnowy=false
141091-9GRASS_BLOCKsnowy=false
171092-11PINK_PETALSfacing=east,flower_amount=4
181092-10PINK_PETALSfacing=east,flower_amount=1
191092-9PINK_PETALSfacing=south,flower_amount=1
201191-13OXEYE_DAISY
221191-11PINK_PETALSfacing=south,flower_amount=1
231191-10SHORT_GRASS
321291-11DANDELION
331291-10DANDELION
421391-11DANDELION
431391-10PINK_PETALSfacing=north,flower_amount=3
441391-9SHORT_GRASS
\n", + "
" + ], + "text/plain": [ + " x y z material state\n", + "0 9 91 -13 GRASS_BLOCK snowy=false\n", + "1 9 91 -12 GRASS_BLOCK snowy=false\n", + "2 9 91 -11 GRASS_BLOCK snowy=false\n", + "3 9 91 -10 GRASS_BLOCK snowy=false\n", + "4 9 91 -9 GRASS_BLOCK snowy=false\n", + "5 9 92 -13 SHORT_GRASS \n", + "7 9 92 -11 PINK_PETALS facing=south,flower_amount=2\n", + "9 9 92 -9 PINK_PETALS facing=west,flower_amount=3\n", + "10 10 91 -13 GRASS_BLOCK snowy=false\n", + "11 10 91 -12 GRASS_BLOCK snowy=false\n", + "12 10 91 -11 GRASS_BLOCK snowy=false\n", + "13 10 91 -10 GRASS_BLOCK snowy=false\n", + "14 10 91 -9 GRASS_BLOCK snowy=false\n", + "17 10 92 -11 PINK_PETALS facing=east,flower_amount=4\n", + "18 10 92 -10 PINK_PETALS facing=east,flower_amount=1\n", + "19 10 92 -9 PINK_PETALS facing=south,flower_amount=1\n", + "20 11 91 -13 OXEYE_DAISY \n", + "22 11 91 -11 PINK_PETALS facing=south,flower_amount=1\n", + "23 11 91 -10 SHORT_GRASS \n", + "32 12 91 -11 DANDELION \n", + "33 12 91 -10 DANDELION \n", + "42 13 91 -11 DANDELION \n", + "43 13 91 -10 PINK_PETALS facing=north,flower_amount=3\n", + "44 13 91 -9 SHORT_GRASS " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "50\n" + ] + } + ], + "source": [ + "print(*here, *there)\n", + "cuboid = getBlocksDf(mc, *here, *there, with_state=True)\n", + "\n", + "# # sanity check - this is slow, but one-off to check that coords are correct\n", + "# for row in cuboid.itertuples(): \n", + "# checktype = mc.getBlock(row.x, row.y, row.z)\n", + "# assert checktype == row.material, \\\n", + "# f\"Block type mismatch at ({row.x}, {row.y}, {row.z}): {row.material} != {checktype}\"\n", + "\n", + "display(cuboid[cuboid['material'].ne('AIR')])\n", + "print(len(cuboid))" + ] + }, + { + "cell_type": "markdown", + "id": "3b68dc34", + "metadata": {}, + "source": [ + "### function to render many blocks, with specified state" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "3a5e8480", + "metadata": {}, + "outputs": [], + "source": [ + "def place_blocks_with_state(\n", + " self, \n", + " df: pd.DataFrame, \n", + " check: bool=True\n", + " ) -> pd.DataFrame:\n", + " \"\"\"\n", + " Given a DataFrame of block coordinates, materials and state, place blocks\n", + "\n", + " Parameters\n", + " ----------\n", + " df : pd.DataFrame\n", + " Must have columns 'x', 'y', 'z', 'material' and optionally 'state'.\n", + " The coordinates 'x', 'y', 'z' should be integers (float will be rounded).\n", + " Material may be a string with prefix 'minecraft:' or just the name,\n", + " case independent. State is a string with comma-separated key=value\n", + " pairs, or an empty string, without surrounding brackets.\n", + " check : bool, optional\n", + " If True, check if the blocks would be changed before placing them.\n", + " Default is True. If False, will always attempt to place the blocks.\n", + " If True, only blocks that would be changed will be placed.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " A copy of the input DataFrame with an additional 'command' column\n", + " containing the setblock command for each block, and a 'success' column\n", + " indicating whether the command was successful (True) or not (False).\n", + " \"\"\"\n", + "\n", + " df = df.copy()\n", + " for col in ['x', 'y', 'z']:\n", + " if df.dtypes[col].__str__() == 'float64':\n", + " df[col] = df[col].round().astype(int)\n", + "\n", + " missing = list({'x', 'y', 'z', 'material'} - set(df.columns))\n", + " if missing:\n", + " raise ValueError(f\"Missing columns: {missing}\")\n", + " \n", + " if 'state' not in df.columns:\n", + " df['state'] = ''\n", + " else:\n", + " df['state'] = df['state'].fillna('')\n", + "\n", + " # first check to see which blocks are already set\n", + " df['result'] = \"failed\"\n", + " if check:\n", + " df = df.set_index(['x', 'y', 'z']).sort_index()\n", + " # get the block data for each block\n", + " for v3 in df.index:\n", + " old = self.getBlockWithData(*v3, parse=False)\n", + " changed = (old['material'] != df.loc[v3, 'material']) \\\n", + " or (old['state'] != df.loc[v3, 'state'])\n", + " if not changed:\n", + " df.loc[v3, 'result'] = 'unchanged'\n", + " df = df.reset_index()\n", + " \n", + " df['command'] = ''\n", + " df['command'] = np.where(\n", + " df['state'].gt(''), '[' + df['state'].copy() + ']', '') \n", + " df['command'] = 'setblock ' \\\n", + " + df[['x', 'y', 'z']].astype(str).apply(lambda x: ' '.join(list(x)), axis=1) \\\n", + " + ' minecraft:' + df['material'].str.lower() + df['command']\n", + " update = df['result'].eq('failed')\n", + " commands = df.loc[update, 'command'].tolist()\n", + "\n", + " # should maybe chunk the commands and pause between them to avoid overload\n", + " results = self.runCommands(commands, 0.1)\n", + " df.loc[update, 'result'] = \\\n", + " ['success' if x.startswith('Changed the block at') else 'failed' \n", + " for x in results]\n", + "\n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "10560a91", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [11.5 91. -10.5] to [-189.5 138. -271.5]\n" + ] + }, + { + "data": { + "text/plain": [ + "Coord([-201, 47, -261])" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# copy the cuboid above to a new location\n", + "teleport(Coord(-189.5, 138, -271.5))\n", + "new_pos = Coord(mc.player.getTilePos()).block()\n", + "\n", + "move = new_pos - pos\n", + "move" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "53003799", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n", + "warning: unknown material: minecraft:short_grass\n" + ] + }, + { + "data": { + "text/plain": [ + "result\n", + "success 27\n", + "unchanged 23\n", + "dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xyzmaterialstateresultcommand
0-192138-274GRASS_BLOCKsnowy=falsesuccesssetblock -192 138 -274 minecraft:grass_block[snowy=false]
1-192138-273GRASS_BLOCKsnowy=falsesuccesssetblock -192 138 -273 minecraft:grass_block[snowy=false]
2-192138-272GRASS_BLOCKsnowy=falsesuccesssetblock -192 138 -272 minecraft:grass_block[snowy=false]
3-192138-271GRASS_BLOCKsnowy=falsesuccesssetblock -192 138 -271 minecraft:grass_block[snowy=false]
4-192138-270GRASS_BLOCKsnowy=falsesuccesssetblock -192 138 -270 minecraft:grass_block[snowy=false]
5-192139-274SHORT_GRASSsuccesssetblock -192 139 -274 minecraft:short_grass
7-192139-272PINK_PETALSfacing=south,flower_amount=2successsetblock -192 139 -272 minecraft:pink_petals[facing=south,flower_amount=2]
9-192139-270PINK_PETALSfacing=west,flower_amount=3successsetblock -192 139 -270 minecraft:pink_petals[facing=west,flower_amount=3]
10-191138-274GRASS_BLOCKsnowy=falsesuccesssetblock -191 138 -274 minecraft:grass_block[snowy=false]
11-191138-273GRASS_BLOCKsnowy=falsesuccesssetblock -191 138 -273 minecraft:grass_block[snowy=false]
12-191138-272GRASS_BLOCKsnowy=falsesuccesssetblock -191 138 -272 minecraft:grass_block[snowy=false]
13-191138-271GRASS_BLOCKsnowy=falsesuccesssetblock -191 138 -271 minecraft:grass_block[snowy=false]
14-191138-270GRASS_BLOCKsnowy=falsesuccesssetblock -191 138 -270 minecraft:grass_block[snowy=false]
17-191139-272PINK_PETALSfacing=east,flower_amount=4successsetblock -191 139 -272 minecraft:pink_petals[facing=east,flower_amount=4]
18-191139-271PINK_PETALSfacing=east,flower_amount=1successsetblock -191 139 -271 minecraft:pink_petals[facing=east,flower_amount=1]
19-191139-270PINK_PETALSfacing=south,flower_amount=1successsetblock -191 139 -270 minecraft:pink_petals[facing=south,flower_amount=1]
20-190138-274OXEYE_DAISYsuccesssetblock -190 138 -274 minecraft:oxeye_daisy
21-190138-273AIRsuccesssetblock -190 138 -273 minecraft:air
22-190138-272PINK_PETALSfacing=south,flower_amount=1successsetblock -190 138 -272 minecraft:pink_petals[facing=south,flower_amount=1]
23-190138-271SHORT_GRASSsuccesssetblock -190 138 -271 minecraft:short_grass
24-190138-270AIRsuccesssetblock -190 138 -270 minecraft:air
32-189138-272DANDELIONsuccesssetblock -189 138 -272 minecraft:dandelion
33-189138-271DANDELIONsuccesssetblock -189 138 -271 minecraft:dandelion
40-188138-274AIRsuccesssetblock -188 138 -274 minecraft:air
41-188138-273AIRsuccesssetblock -188 138 -273 minecraft:air
42-188138-272DANDELIONsuccesssetblock -188 138 -272 minecraft:dandelion
43-188138-271PINK_PETALSfacing=north,flower_amount=3successsetblock -188 138 -271 minecraft:pink_petals[facing=north,flower_amount=3]
\n", + "
" + ], + "text/plain": [ + " x y z material state result \\\n", + "0 -192 138 -274 GRASS_BLOCK snowy=false success \n", + "1 -192 138 -273 GRASS_BLOCK snowy=false success \n", + "2 -192 138 -272 GRASS_BLOCK snowy=false success \n", + "3 -192 138 -271 GRASS_BLOCK snowy=false success \n", + "4 -192 138 -270 GRASS_BLOCK snowy=false success \n", + "5 -192 139 -274 SHORT_GRASS success \n", + "7 -192 139 -272 PINK_PETALS facing=south,flower_amount=2 success \n", + "9 -192 139 -270 PINK_PETALS facing=west,flower_amount=3 success \n", + "10 -191 138 -274 GRASS_BLOCK snowy=false success \n", + "11 -191 138 -273 GRASS_BLOCK snowy=false success \n", + "12 -191 138 -272 GRASS_BLOCK snowy=false success \n", + "13 -191 138 -271 GRASS_BLOCK snowy=false success \n", + "14 -191 138 -270 GRASS_BLOCK snowy=false success \n", + "17 -191 139 -272 PINK_PETALS facing=east,flower_amount=4 success \n", + "18 -191 139 -271 PINK_PETALS facing=east,flower_amount=1 success \n", + "19 -191 139 -270 PINK_PETALS facing=south,flower_amount=1 success \n", + "20 -190 138 -274 OXEYE_DAISY success \n", + "21 -190 138 -273 AIR success \n", + "22 -190 138 -272 PINK_PETALS facing=south,flower_amount=1 success \n", + "23 -190 138 -271 SHORT_GRASS success \n", + "24 -190 138 -270 AIR success \n", + "32 -189 138 -272 DANDELION success \n", + "33 -189 138 -271 DANDELION success \n", + "40 -188 138 -274 AIR success \n", + "41 -188 138 -273 AIR success \n", + "42 -188 138 -272 DANDELION success \n", + "43 -188 138 -271 PINK_PETALS facing=north,flower_amount=3 success \n", + "\n", + " command \n", + "0 setblock -192 138 -274 minecraft:grass_block[snowy=false] \n", + "1 setblock -192 138 -273 minecraft:grass_block[snowy=false] \n", + "2 setblock -192 138 -272 minecraft:grass_block[snowy=false] \n", + "3 setblock -192 138 -271 minecraft:grass_block[snowy=false] \n", + "4 setblock -192 138 -270 minecraft:grass_block[snowy=false] \n", + "5 setblock -192 139 -274 minecraft:short_grass \n", + "7 setblock -192 139 -272 minecraft:pink_petals[facing=south,flower_amount=2] \n", + "9 setblock -192 139 -270 minecraft:pink_petals[facing=west,flower_amount=3] \n", + "10 setblock -191 138 -274 minecraft:grass_block[snowy=false] \n", + "11 setblock -191 138 -273 minecraft:grass_block[snowy=false] \n", + "12 setblock -191 138 -272 minecraft:grass_block[snowy=false] \n", + "13 setblock -191 138 -271 minecraft:grass_block[snowy=false] \n", + "14 setblock -191 138 -270 minecraft:grass_block[snowy=false] \n", + "17 setblock -191 139 -272 minecraft:pink_petals[facing=east,flower_amount=4] \n", + "18 setblock -191 139 -271 minecraft:pink_petals[facing=east,flower_amount=1] \n", + "19 setblock -191 139 -270 minecraft:pink_petals[facing=south,flower_amount=1] \n", + "20 setblock -190 138 -274 minecraft:oxeye_daisy \n", + "21 setblock -190 138 -273 minecraft:air \n", + "22 setblock -190 138 -272 minecraft:pink_petals[facing=south,flower_amount=1] \n", + "23 setblock -190 138 -271 minecraft:short_grass \n", + "24 setblock -190 138 -270 minecraft:air \n", + "32 setblock -189 138 -272 minecraft:dandelion \n", + "33 setblock -189 138 -271 minecraft:dandelion \n", + "40 setblock -188 138 -274 minecraft:air \n", + "41 setblock -188 138 -273 minecraft:air \n", + "42 setblock -188 138 -272 minecraft:dandelion \n", + "43 setblock -188 138 -271 minecraft:pink_petals[facing=north,flower_amount=3] " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "moved = cuboid.copy()\n", + "moved['x'] += move.x\n", + "moved['y'] += move.y\n", + "moved['z'] += move.z\n", + "\n", + "results = place_blocks_with_state(mc, moved)\n", + "display(results.groupby(['result']).size())\n", + "display(results[results['result'].ne('unchanged')])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "cc25bc52", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-189.5 138. -271.5] to [-29 70 -185]\n" + ] + } + ], + "source": [ + "# just for backwards compatibility with older version of the subway code\n", + "blocktypes = {\n", + " 0: 'AIR',\n", + " 1: 'STONE',\n", + " 2: 'GRASS_BLOCK', #\n", + " 3: 'DIRT',\n", + " 5: 'DARK_OAK_WOOD', #\n", + " 9: 'BLUE_ICE', \n", + " 20: 'GLASS',\n", + " 27: 'POWERED_RAIL',\n", + " 76: 'REDSTONE_TORCH',\n", + " 89: 'GLOWSTONE',\n", + " 98: 'STONE_BRICKS', #\n", + " 107: 'BIRCH_FENCE_GATE' #\n", + "}\n", + "\n", + "\n", + "def display_block_types(distance: int = 2):\n", + " \"\"\"\n", + " Generate each block type with a sign indicating ID and name on top\n", + " \n", + " Blocks are placed in a line in front of the player, with signs above,\n", + " facing the player. Note that this is using the Juice plugin's\n", + " `setBlock` command.\n", + " \"\"\"\n", + "\n", + " pos = Coord(*mc.player.getTilePos()).astype(int)\n", + " pointing = Vec3D(mc.player.getDirection())\n", + " \n", + " facing = pointing.cardinal_direction().upper()\n", + " pointing = pointing.cardinal_direction('coord')\n", + "\n", + " # print(f'Player position: {pos}, pointing: {pointing} ({facing})')\n", + "\n", + " # place the blocks in a line in front of the player\n", + " start = pos + distance * pointing\n", + "\n", + " # center left to right from their perspective\n", + " if pointing.x == 0:\n", + " start.x += pointing.z * -len(blocktypes)\n", + " translate = Coord(pointing.z, 0, 0)\n", + " else:\n", + " start.z += pointing.x * -len(blocktypes)\n", + " translate = Coord(0, 0, pointing.x)\n", + "\n", + " # print(start)\n", + " flip_face = {\n", + " 'NORTH': 'SOUTH', 'SOUTH': 'NORTH', 'EAST': 'WEST', 'WEST': 'EAST'\n", + " }\n", + " for i, k in enumerate(blocktypes.keys()):\n", + " here = start + 2 * i * translate\n", + " # print(f'Placing {here} ({blocktypes[k]})')\n", + " mc.setBlock(*here, blocktypes[k], 'NORTH')\n", + " here.y += 1\n", + " mc.setSign(*here, 'BIRCH_SIGN', flip_face[facing], \n", + " f'{k} =', *(blocktypes[k]).split('_'))\n", + " \n", + "\n", + "# field with big flat area of grass\n", + "teleport(Coord(-29, 70, -185))\n", + "display_block_types(distance=3)" + ] + }, + { + "cell_type": "markdown", + "id": "3933fb47", + "metadata": {}, + "source": [ + "### Some general building functions, to be added to `Builder` or `Unit` class\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "ada7ef84", + "metadata": {}, + "outputs": [], + "source": [ + "def np_rep(\n", + " x: np.array, \n", + " reps: int=1, \n", + " each: bool=False, \n", + " length: int=0\n", + " ) -> np.array:\n", + " \"\"\"\n", + " Replicate the values in x, similar to R's rep() and rep_len() functions.\n", + " \n", + " Parameters\n", + " ----------\n", + " x : numpy array\n", + " The array to be replicated.\n", + " reps : int, optional\n", + " The number of times to replicate the values in x. Default is 1.\n", + " each : bool, optional\n", + " If True, each element of x is repeated reps times before the next\n", + " element. Default is False.\n", + " length : int, optional\n", + " The desired length of the output array. If greater than 0, this \n", + " overrides the reps argument. Default is 0.\n", + "\n", + " Returns\n", + " -------\n", + " numpy array\n", + " The replicated array, with length equal to len(x) * reps or the \n", + " length if specified.\n", + " \"\"\"\n", + "\n", + " if length > 0:\n", + " reps = np.int(np.ceil(length / x.size))\n", + " x = np.repeat(x, reps)\n", + " if not each:\n", + " x = x.reshape(-1, reps).T.ravel()\n", + " if length > 0:\n", + " x = x[0:length]\n", + " return x\n", + "\n", + "\n", + "def extruder(\n", + " df: pd.DataFrame, \n", + " startpos: tuple, \n", + " dir: str=\"y\", \n", + " distance: int=1,\n", + " rcon: bool=False,\n", + " pause: float=0,\n", + " ) -> int:\n", + " \"\"\"\n", + " Given A DataFrame of 2D coordinates, extrude in 3D From Start Position\n", + "\n", + " Note: this is a stupid implementation.\n", + "\n", + " Parameters\n", + " ----------\n", + " df : pd.DataFrame\n", + " A DataFrame of 2D coordinates with columns 'x', 'y', and 'blocktype'.\n", + " Positions will be instantiated relative to the start position.\n", + " startpos : tuple\n", + " A tuple of 3 integers representing the starting position in 3D space.\n", + " The coordinates are in the order (x, y, z).\n", + " dir : str, optional\n", + " The direction to extrude in. Can be 'x', 'y', or 'z'. Default is 'y'.\n", + " Positive x is east, positive y is up, and positive z is south.\n", + " distance : int, optional\n", + " Distance to extrude in the direction specified, by default 1\n", + " rcon: bool, optional\n", + " If True, use the RCON client to place blocks, otherwise use the \n", + " FruitJuice setBlock() method. Default is False. RCON can set block\n", + " state, but seems to have more issues with server overload. Probably\n", + " should use RCON only for blocks with non-default states?\n", + " pause: float, optional\n", + " Pause in seconds between placing each block, by default 0.\n", + " This is useful to avoid overloading the server with too many commands\n", + " at once, especially when using RCON.\n", + "\n", + " Returns\n", + " -------\n", + " int\n", + " The total number of blocks placed.\n", + " \"\"\"\n", + "\n", + " n = 0\n", + " if rcon:\n", + " # use the RCON client to place blocks; seems to have more lag issues\n", + " if(dir[0]=='-'):\n", + " offset = -1\n", + " dir = dir[1]\n", + " else:\n", + " offset = 1 \n", + "\n", + " segment = df.copy().rename(columns={'blocktype': 'material'})\n", + " segment['x'] = startpos[0]\n", + " segment['y'] = startpos[1]\n", + " segment['z'] = startpos[2]\n", + " segment['stats'] = ''\n", + " if dir == \"x\":\n", + " segment['y'] += df['y']\n", + " segment['z'] += df['x']\n", + " elif dir == \"y\":\n", + " segment['x'] += df['x']\n", + " segment['z'] += df['y']\n", + " elif dir == \"z\":\n", + " segment['x'] += df['x']\n", + " segment['y'] += df['y']\n", + "\n", + " for i in range(distance):\n", + " segment[dir] += offset\n", + " results = place_blocks_with_state(mc, segment)\n", + " n += len(segment)\n", + "\n", + " else:\n", + " # use the FruitJuice setBlock() method\n", + " if(dir[0]=='-'):\n", + " offsets = range(0, -distance, -1)\n", + " dir = dir[1]\n", + " else:\n", + " offsets = range(0, distance)\n", + "\n", + " for offset in offsets:\n", + " time.sleep(pause)\n", + " for index, row in df.iterrows():\n", + " if dir == \"x\":\n", + " d = [offset, row.y, row.x]\n", + " elif dir == \"y\":\n", + " d = [row.x, offset, row.y]\n", + " elif dir == \"z\":\n", + " d = [row.x, row.y, offset]\n", + " mc.setBlock(\n", + " startpos[0] + d[0], \n", + " startpos[1] + d[1], \n", + " startpos[2] + d[2],\n", + " row.blocktype)\n", + " n += 1\n", + "\n", + " return n\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "8e48a36a", + "metadata": {}, + "outputs": [], + "source": [ + "def circle_of_blocks(\n", + " d: int, \n", + " cutoff: float = 0.45, \n", + " ) -> pd.DataFrame:\n", + " \"\"\"\n", + " Generate a DataFrame of block positions in a circle with a given radius.\n", + "\n", + " Uses a heuristic to determine positions of blocks that are fairly round.\n", + " May have gaps in the perimeter for very large diameters (like >100?)\n", + "\n", + " The center of the circle is positioned at x=0, y=0; block positions are\n", + " specified relative to that value.\n", + "\n", + " For odd diameters, the center block position should be specified as\n", + " intergers, which will place the center of the circle at the center of the\n", + " center block. \n", + " \n", + " For even diameters, the block positions are half-block centered. If\n", + " you specify the center block position as integers, the center will \n", + " be at the southwest corner of the center block. If the center block \n", + " is specified as half-block centered, the center will be at the southeast \n", + " corner of the center block.\n", + "\n", + " Parameters\n", + " ----------\n", + " d : int\n", + " The diameter of the circle; integer > 2\n", + " cutoff : float, optional\n", + " Cutoff that impacts width and roundness. Default 0.45 gives good \n", + " results for most diameters. Larger values exclude blocks with \n", + " a smaller proportion of the circle's perimeter inside them.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " DataFrame with columns 'x', 'y', 'n', and 'theta'. Theta is in radians.\n", + " \"\"\"\n", + "\n", + " if d < 2:\n", + " raise ValueError(f\"Diameter must be greater than 2, got {d}\")\n", + " \n", + " fun = np.floor if d % 2 == 0 else np.round\n", + " theta = np.linspace(0.0, 2 * np.pi, 10000, endpoint=False)\n", + " r = (d / 2) - 0.5\n", + " df = pd.DataFrame({\n", + " 'x': fun(r * np.cos(theta)).astype(int),\n", + " 'y': fun(r * np.sin(theta)).astype(int),\n", + " })\n", + " df = df.groupby(['x', 'y']).size().reset_index().rename(columns={0: 'n'})\n", + " df['n'] /= df['n'].max() \n", + " df = df[df['n'].gt(cutoff)]\n", + " if d % 2 == 0:\n", + " df['x'] += 0.5\n", + " df['y'] += 0.5\n", + " df['theta'] = np.arctan2(df['y'], df['x'])\n", + " df = df.sort_values('theta').reset_index()\n", + "\n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "51fded25", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
x-2-1012
y
-29109
-199
01010
199
29109
\n", + "
" + ], + "text/plain": [ + "x -2 -1 0 1 2\n", + "y \n", + "-2 9 10 9 \n", + "-1 9 9\n", + " 0 10 10\n", + " 1 9 9\n", + " 2 9 10 9 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
x-2.5-1.5-0.50.51.52.5
y
-2.5610106
-1.56776
-0.51010
0.51010
1.56776
2.5610106
\n", + "
" + ], + "text/plain": [ + "x -2.5 -1.5 -0.5 0.5 1.5 2.5\n", + "y \n", + "-2.5 6 10 10 6 \n", + "-1.5 6 7 7 6\n", + "-0.5 10 10\n", + " 0.5 10 10\n", + " 1.5 6 7 7 6\n", + " 2.5 6 10 10 6 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "\n", + "
\n", + "" + ], + "text/plain": [ + "alt.VConcatChart(...)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show output of circle_of_blocks() for various diameters\n", + "charts = []\n", + "axes = alt.Axis(labels=False, title='', grid=False, gridOpacity=0.5, ticks=False)\n", + "for i, d in enumerate(range(3, 27)):\n", + " if i % 6 == 0:\n", + " row = []\n", + "\n", + " r = np.ceil(d / 2.0)\n", + " circle_df = circle_of_blocks(d, 0.45) \n", + "\n", + " if d in (5, 6):\n", + " circle_df['n'] = (10 * circle_df['n'].round(1)).astype(int).astype(str)\n", + " display(circle_df.pivot(index='y', columns='x', values='n').fillna(''))\n", + "\n", + " circle_df['n'] = 1.0\n", + " chart = alt.Chart(circle_df).mark_rect(stroke='gray20', strokeWidth=1).encode(\n", + " x=alt.X('x:O', axis=axes),\n", + " y=alt.Y('y:O', axis=axes),\n", + " color=alt.Color('n:N', \n", + " scale=alt.Scale(scheme='greys'), \n", + " bin=alt.Bin(maxbins=10),\n", + " ),\n", + " tooltip = ['n', 'theta:Q'],\n", + " ).properties(\n", + " width=100, \n", + " height=100,\n", + " title=f'{d}',\n", + " )\n", + " row.append(chart)\n", + " if i % 6 == 5:\n", + " charts.append(\n", + " alt.hconcat(*row).resolve_scale(x='independent', y='independent'))\n", + " \n", + " \n", + "charts = alt.vconcat(*charts).resolve_scale(x='independent', y='independent')\n", + "display(charts.configure_legend(disable=True))" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "97984829", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-28.5 70. -184.5] to [17. 91. -13.]\n" + ] + } + ], + "source": [ + "# test the circle_of_blocks + extruder functions for making cylinders\n", + "pos = Coord(17.0, 91, -13.0) # another flattish area\n", + "teleport(pos)\n", + "time.sleep(1)\n", + "\n", + "pos = Coord(mc.player.getTilePos())\n", + "for ring in range(3):\n", + " d = 7 + 4 * ring\n", + " center = pos.block() if d % 2 == 1 else pos.midblock()\n", + " df = circle_of_blocks(d)\n", + " df['blocktype'] = 'BLUE_WOOL'\n", + " extruder(df, center, \"y\", 1) #int(7 - (ring * 2)))\n", + " time.sleep(0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "0fc9f7c5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.897, 0.063, 0.437])" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "'east'" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[1, 0, 0]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# setDirection doesn't work-- always sets direction to 1,0,0\n", + "pointing = Vec3D(mc.player.getDirection())\n", + "display(pointing.round(3))\n", + "display(pointing.cardinal_direction())\n", + "\n", + "mc.player.setDirection(0.0, 0.0, 1.0) # FAILS!\n", + "pointing = mc.player.getDirection()\n", + "[round(float(x)) for x in pointing]" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "3c8df2be", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'x': 20.87604761388642,\n", + " 'y': 91.95292452735033,\n", + " 'z': -12.271120580154287,\n", + " 'material': 'BLUE_WOOL',\n", + " 'distance': 4}" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def set_in_front(mc, blocktype, distance=2):\n", + " \"\"\"\n", + " Get player's current position and direction; set block in front of player.\n", + " \"\"\"\n", + " pointing = Vec3D(mc.player.getDirection())\n", + " facing = pointing.cardinal_direction()\n", + " pointing = pointing.cardinal_direction('coord')\n", + " pos = Coord(mc.player.getTilePos()).astype(int)\n", + "\n", + " print(f'pointing: {pointing} ({facing})')\n", + " print(f'position: {pos}')\n", + "\n", + " pos += pointing * distance\n", + " print(f' target: {pos}')\n", + " mc.setBlock(*pos, blocktype)\n", + "\n", + "\n", + "\n", + "def whats_that(mc):\n", + " \"\"\"\n", + " Get material of block the player is looking at (approximately).\n", + "\n", + " This doesn't work that well-- not how it should handle partially \n", + " transparent blocks like short grass or slabs, and camera is offset by 0.05 \n", + " blocks in the direction the player is looking. But can be used to \n", + " judge distances to distant blocks.\n", + " \"\"\"\n", + "\n", + " pos = Coord(mc.player.getPos())\n", + " pos.y += 1.62 # camera height\n", + " pointing = Vec3D(mc.player.getDirection())\n", + "\n", + " for d in range(255):\n", + " that = pos + d * pointing\n", + " block = mc.getBlockWithData(*that)\n", + " if block['material'] not in ['AIR', 'WATER']:\n", + " block.update({'distance': d})\n", + " break\n", + " \n", + " return block\n", + "\n", + "\n", + "# set_in_front(mc, 'WHITE_BED')\n", + "# set_in_front(mc, 'WHITE_BED', 3)\n", + "whats_that(mc)\n" + ] + }, + { + "cell_type": "markdown", + "id": "91332bb6", + "metadata": {}, + "source": [ + "### Functions to create a \"Subway\" Section on Support Columns" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "221317b7", + "metadata": {}, + "outputs": [], + "source": [ + "def subway_section(radius: int) -> pd.DataFrame:\n", + " \"\"\"\n", + " Creates A Cross-Section of a Subway Tube As A DataFrame\n", + "\n", + " Parameters\n", + " ----------\n", + " radius : int\n", + " Radius of the tube section in blocks. Optimized for 4.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " A DataFrame with columns 'x', 'y', 'radius', 'theta', and 'blocktype'.\n", + " \"\"\"\n", + "\n", + " section = np.arange(-(radius + 1), radius + 2)\n", + " section = pd.DataFrame({\n", + " 'x': np_rep(section, section.size),\n", + " 'y': np_rep(section, section.size, each=section.size)\n", + " })\n", + " section['radius'] = np.sqrt(section.x**2 + section.y**2)\n", + " section['theta'] = np.arctan(section.y / section.x) * 180 / math.pi\n", + " section['blocktype'] = np_rep(0, section.shape[0])\n", + " \n", + " # build up the platform\n", + " section.loc[(section.x==0) & (section.y==-2), 'blocktype'] = 98 # stone brick\n", + " section.loc[(section.x==0) & (section.y==-1), 'blocktype'] = 27 # powered rail\n", + " section.loc[(np.abs(section.x)==1) & (section.y==-3), 'blocktype'] = 89 # glowstone\n", + " section.loc[(np.abs(section.x)==1) & (section.y==-2), 'blocktype'] = 2 # grass\n", + " # set the tube to glass\n", + " section.loc[round(section.radius) == radius, 'blocktype'] = 20\n", + " # wooden supports\n", + " section.loc[(round(section.radius) == radius)\n", + " & (np.abs((section.theta % 60) - 30) < 4), 'blocktype'] = 5\n", + " section.loc[(section.x==0) & (section.y==-3), 'blocktype'] = 76 # redstone torch\n", + "\n", + " section.loc[(np.abs(section.x)==2) & (section.y==-2), 'blocktype'] = 9 # water\n", + " # external bottom supports\n", + " section.loc[(np.abs(section.x)==3) & (section.y==-4), 'blocktype'] = 98 # stone brick\n", + " section.loc[(np.abs(section.x)==4) & (section.y==-4), 'blocktype'] = 98 # stone brick\n", + " section.loc[(np.abs(section.x)==1) & (section.y==-5), 'blocktype'] = 98 # stone brick\n", + " # drop surrounding part\n", + " section = section.loc[(np.round(section.radius) <= radius) | (section.blocktype!=0)]\n", + " section['blocktype'] = section['blocktype'].map(blocktypes)\n", + " # switch out the BLUE_ICE for WATER (lights up due to glowstone)\n", + " section.loc[section.blocktype == 'BLUE_ICE', 'blocktype'] = 'WATER'\n", + " \n", + " return section\n", + "\n", + "\n", + "def support(across: int=4, blocktype: int=1) -> pd.DataFrame:\n", + " \"\"\"\n", + " Square Section of a Subway Tube Support, Dimension 2 * across + 1 \n", + "\n", + " Parameters\n", + " ----------\n", + " across : int, optional\n", + " Half (width -1), by default 4\n", + " blocktype : int, optional\n", + " Material as a number, by default 1\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " A DataFrame with columns 'x', 'y', and 'blocktype' (as string).\n", + " \"\"\"\n", + "\n", + " section = np.arange(-across, across + 1)\n", + " section = pd.DataFrame({\n", + " 'x': np_rep(section, section.size),\n", + " 'y': np_rep(section, section.size, each=section.size),\n", + " 'blocktype': np_rep(blocktype, section.size ** 2)\n", + " })\n", + " section.loc[(np.abs(section.x) < 3) & (section.y == 0), 'blocktype'] = 0\n", + " section.loc[(section.x == 0) & (np.abs(section.y) < 3), 'blocktype'] = 0\n", + " section = section.loc[section.blocktype != 0]\n", + " section['blocktype'] = section['blocktype'].map(blocktypes)\n", + " \n", + " return section\n", + "\n", + "\n", + "def arch_df(section_length: int, material: str, dir: str='z') -> pd.DataFrame:\n", + "\n", + " arch = circle_of_blocks(section_length - 5)\n", + " arch['blocktype'] = material\n", + " arch.drop('theta', axis=1, inplace=True)\n", + " arch = arch[arch['y'].ge(0)]\n", + " arch['y'] = arch['y'] - arch['y'].max()\n", + " # arch = arch[arch['x'].lt((section_length - 2) / 2)]\n", + "\n", + " sign = -1 if dir[0] == '-' else 1\n", + " arch['x'] = arch['x'] + sign * section_length / 2\n", + "\n", + " return arch" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "a156ec1a", + "metadata": {}, + "outputs": [], + "source": [ + "def build_subway_line(\n", + " here: Coord,\n", + " there: Coord,\n", + " section_lengths: int = 24,\n", + " radius: int = 4,\n", + " column_radius: int = 4,\n", + " max_support_height: int = 40,\n", + " delay_factor: float = 2.0,\n", + " calcs_only: bool = False\n", + " ) -> int:\n", + " \"\"\"\n", + " Build A Subway Line Between Two Coordinates\n", + "\n", + " The subway line must currently be on a straight path between two points \n", + " in the same plane (xy or yz). The line is built in sections of a given\n", + " length, with any extra blocks placed at the ends, as on and off-ramps.\n", + " There are junctions between sections with two rings of stone and a gap,\n", + " and a support column under each junction. Under each section (but not\n", + " the ramps) there is a support arch. At each end of the powewered rail\n", + " way, there is an unpowered rail and a stone block, to keep the minecart\n", + " from going off the ends of the line.\n", + "\n", + " Parameters\n", + " ----------\n", + " here : Coord\n", + " Starting coordinate of the subway line, as a Coord object.\n", + " there : Coord\n", + " Ending coordinate of the subway line, as a Coord object.\n", + " section_lengths : int, optional\n", + " Lengths of sections in the middle, by default 24.\n", + " radius : int, optional\n", + " Radius of the tube, by default 4 (build has been optimized for 4).\n", + " column_radius : int, optional\n", + " Specifies column size, where it will be twice this value + 1 at the\n", + " top. By default 4; should be at least 3.\n", + " max_support_height : int, optional\n", + " Maximum distance support columns are expected to be above the surface\n", + " below, by default 40. Currently the colums will extend this far down\n", + " regardless of the height of the surface below (stupid).\n", + " delay_factor : float, optional\n", + " Controls how long to wait between placing chunks of the build.\n", + " Increase this if the server is getting overwhelmed, by default 2.0\n", + " calcs_only : bool, optional\n", + " If True, just print out info about what will be built. By default False.\n", + "\n", + " Returns\n", + " -------\n", + " int\n", + " Total number of block placements (including air).\n", + " \"\"\"\n", + "\n", + " here = here.block()\n", + " there = there.block()\n", + "\n", + " # check that the ends are in the same plane\n", + " plane = here.coplane(there)\n", + " if plane not in {'xy', 'yz'}:\n", + " raise ValueError(\n", + " f'Coordinates {here} and {there} are not in the xy or yz plane.')\n", + " pointing = here.cardinal_direction(there, 'coord')\n", + " facing = here.cardinal_direction(there)\n", + " directions = {'north': '-z', 'south': 'z', 'east': 'x', 'west': '-x'}\n", + " direction = directions[facing]\n", + " axis = direction[-1]\n", + " across = 'x' if axis == 'z' else 'z'\n", + "\n", + " # calculate the number of sections, and leftover for the ends\n", + " dist = here.distance(there) + 1 # endpoint inclusive\n", + " n_sections = int(dist // section_lengths)\n", + " leftover = int(dist % section_lengths)\n", + "\n", + " # top of the support column as a Coord centered on the start position\n", + " column_top = here.copy()\n", + " column_top.y -= (radius + 1)\n", + "\n", + " print(f'Will build subway line from {here} to {there}:')\n", + " print(f' traveling {dist} blocks {facing}, in {n_sections} sections '\n", + " f'of {section_lengths} blocks,\\n'\n", + " f' with {leftover} blocks split between the ends.')\n", + " if calcs_only:\n", + " return plane, pointing, facing, dist, leftover, n_sections\n", + " \n", + "\n", + " # define the different cross-sections of the subway tube\n", + " cross_section = subway_section(radius)\n", + " # add junction between sections\n", + " junction = cross_section.copy()\n", + " # add stone platform and add the gap section one beyond the end\n", + " junction.loc[\n", + " (np.abs(junction.x)>0) & (junction.y==-2), 'blocktype'] = blocktypes[98]\n", + " # replace glass with stone and add ring/s before and after the gap\n", + " ring = junction.copy()\n", + " ring.loc[round(ring.radius) == radius, 'blocktype'] = blocktypes[1]\n", + " # arch to place between support columns under each section\n", + " arch = arch_df(section_lengths, 'STONE', dir=direction)\n", + "\n", + " on_ramp = int(leftover // 2)\n", + " off_ramp = leftover - on_ramp\n", + "\n", + " # loop over subsections, sandwiched by on-ramp and off-ramp sections\n", + " n_blocks = 0\n", + " for section in range(n_sections + 2):\n", + "\n", + " midsection = False\n", + " if section == 0:\n", + " # first section is the leftover at the start\n", + " section_length = on_ramp\n", + " along = 0\n", + "\n", + " elif section == n_sections + 1:\n", + " # last section is the leftover at the end\n", + " section_length = off_ramp\n", + " along = on_ramp + n_sections * section_lengths\n", + " else:\n", + " # regular section, with supports underneath\n", + " midsection = True\n", + " section_length = section_lengths\n", + " along = on_ramp + (section - 1) * section_lengths\n", + "\n", + " if section_length == 0:\n", + " continue # no leftover section, skip\n", + "\n", + " # offset from the start position in the build direction\n", + " if direction[0] == '-':\n", + " along = -along\n", + " along = Coord(0, 0, along) if axis == 'z' else Coord(along, 0, 0)\n", + "\n", + " # extrude the tube for this section\n", + " added = extruder(cross_section, here + along, direction, section_length)\n", + " n_blocks += added\n", + "\n", + " if section == 0:\n", + " # add a ring at the entrance to hold the water in\n", + " added = extruder(ring, here, axis, 1)\n", + " n_blocks += added\n", + " \n", + " time.sleep(5 * delay_factor)\n", + "\n", + " if midsection or (n_sections > 0 and section == n_sections + 1):\n", + "\n", + " n_chunk = 0\n", + " # add junction rings and support column at the start\n", + " added = extruder(junction, here + along, axis, 1)\n", + " n_chunk += added\n", + " # replace glass with stone and add rings before and after the gap\n", + " offset1 = Coord(0, 0, 1) if axis == 'z' else Coord(1, 0, 0)\n", + " added = extruder(ring, here + along - offset1, axis, 1)\n", + " n_chunk += added\n", + " added = extruder(ring, here + along + offset1, axis, 1)\n", + " n_chunk += added\n", + "\n", + " # add the support column at the start\n", + " column = support(column_radius)\n", + " column_pos = column_top + along\n", + " # square slab at top, 2 * column_radius + 1 wide\n", + " added = extruder(column, column_pos, \"-y\", 1)\n", + " n_chunk += added\n", + " \n", + " # square below that, 2 * column_radius - 1 wide\n", + " column = column[(np.abs(column.x) < column_radius) \n", + " & (np.abs(column.y) < column_radius)]\n", + " added = extruder(column, column_pos, \"-y\", 1)\n", + " n_chunk += added\n", + " # 4 pillars below, each column_radius - 2 wide, 1 block gap in middle\n", + " column_pos.y -= 1\n", + " column = column[(np.abs(column.x) < column_radius-1)\n", + " & (np.abs(column.y) < column_radius-1)]\n", + " # should be smarter about this by checking height above the surface\n", + " added = extruder(column, column_pos, \"-y\", max_support_height)\n", + " n_chunk += added\n", + "\n", + " time.sleep(4 * delay_factor)\n", + " n_blocks += n_chunk\n", + "\n", + " if midsection and n_sections > 0:\n", + " # add the arches under this midsection, matched to the 4 columns\n", + " transverse = sorted(column['x'].drop_duplicates())\n", + " n_chunk = 0\n", + " for x_offset in transverse:\n", + " if x_offset == 0:\n", + " continue\n", + " arch_pos = Coord(x_offset, 0, 0) if axis == 'z' \\\n", + " else Coord(0, 0, x_offset)\n", + " added = extruder(arch, column_top + along + arch_pos, across, 1)\n", + " n_chunk += added\n", + "\n", + " time.sleep(3 * delay_factor)\n", + " n_blocks += n_chunk\n", + " \n", + " # report progress\n", + " if section == 0:\n", + " print('Completed the on-ramp section')\n", + " elif section < n_sections + 1:\n", + " print(f'Finished midsection {section} of {n_sections}')\n", + " else:\n", + " print('Completed the off-ramp section')\n", + "\n", + " # add a ring at the end to hold the water in\n", + " added = extruder(ring, there, axis, 1)\n", + " n_blocks += added\n", + "\n", + " # put a minecart at the start of the subway section, on unpowered rail\n", + " cart_pos = here + pointing + Coord(0, -1, 0)\n", + " mc.setBlock(*(cart_pos - pointing), 'STONE', 'NORTH')\n", + " mc.setBlock(*cart_pos, 'RAIL', 'NORTH')\n", + " mc.runCommands('summon minecart ' + str(cart_pos)[1:-1])\n", + "\n", + " # also add block and unpowered rail at the end\n", + " cart_pos = there - pointing + Coord(0, -1, 0)\n", + " mc.setBlock(*cart_pos, 'RAIL', 'NORTH')\n", + " mc.setBlock(*(cart_pos + pointing), 'STONE', 'NORTH')\n", + " n_blocks += 4\n", + "\n", + " print(f'\\nSubway complete! Placed {n_blocks} blocks in total.')\n", + "\n", + " return n_blocks" + ] + }, + { + "cell_type": "markdown", + "id": "ec94d70a", + "metadata": {}, + "source": [ + "### Actually lay \"subway\" track.\n", + "- Will start at your position, to a specified endpoint (`there`)\n", + "- Works best with `radius = 4`\n", + "- There are `time.sleep()` calls to avoid server overload, but may be \n", + " insufficient if you haven't passed through the area it is building.\n", + "- Should be based on numbers of block placed, but... not yet\n", + "\n", + "#### Teleport player to a cool starting point abd build\n", + "#### Take several minutes to complete; please be patient." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "765af0ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-45.5 104. 567.5] to [-162.5 87. 1095.5]\n", + "Will build subway line from [-163 89 1092] to [-163 89 212]:\n", + " traveling 881.0 blocks north, in 29 sections of 30 blocks,\n", + " with 11 blocks split between the ends.\n", + "Completed the on-ramp section\n", + "Finished midsection 1 of 29\n", + "Finished midsection 2 of 29\n", + "Finished midsection 3 of 29\n", + "Finished midsection 4 of 29\n", + "Finished midsection 5 of 29\n", + "Finished midsection 6 of 29\n", + "Finished midsection 7 of 29\n", + "Finished midsection 8 of 29\n", + "Finished midsection 9 of 29\n", + "Finished midsection 10 of 29\n", + "Finished midsection 11 of 29\n", + "Finished midsection 12 of 29\n", + "Finished midsection 13 of 29\n", + "Finished midsection 14 of 29\n", + "Finished midsection 15 of 29\n", + "Finished midsection 16 of 29\n", + "Finished midsection 17 of 29\n", + "Finished midsection 18 of 29\n", + "Finished midsection 19 of 29\n", + "Finished midsection 20 of 29\n", + "Finished midsection 21 of 29\n", + "Finished midsection 22 of 29\n", + "Finished midsection 23 of 29\n", + "Finished midsection 24 of 29\n", + "Finished midsection 25 of 29\n", + "Finished midsection 26 of 29\n", + "Finished midsection 27 of 29\n", + "Finished midsection 28 of 29\n", + "Finished midsection 29 of 29\n", + "Completed the off-ramp section\n", + "\n", + "Subway complete! Placed 99599 blocks in total.\n" + ] + } + ], + "source": [ + "# # random hillside to cave (takes ~ 5 minutes)\n", + "# south_end = 567\n", + "# north_end = 207\n", + "# xy = (-52, 103)\n", + "\n", + "# # random hillside to village\n", + "# south_end = 1257.5\n", + "# north_end = 141.5\n", + "# xy = (-178.5, 69)\n", + "\n", + "# village to hillside above village (~ 12 minutes! ~ 100K blocks placed)\n", + "xy = (-162.5, 87)\n", + "south_end = 1095.5\n", + "north_end = 212.5\n", + "\n", + "\n", + "# teleport to the start and adjust the start position\n", + "observer = Coord(*xy, south_end).midblock()\n", + "teleport(observer)\n", + "time.sleep(10) # give the server time to render the world after teleporting\n", + "\n", + "pos = observer.copy()\n", + "pos.y += 2 # track at the same block level as the player\n", + "pos.z -= 3 # two blocks north of the player (note that junction will be closer)\n", + "\n", + "_ = build_subway_line(\n", + " here=pos.midblock(), \n", + " there=Coord(pos.x, pos.y, north_end).midblock(),\n", + " section_lengths=30,\n", + " calcs_only=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "529c953f", + "metadata": {}, + "source": [ + "### Build From Wherever!\n", + "#### build subway line from player's position, in direction they are facing" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "f602abb4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Teleported Acaimo from [-34.5 118. 532.5] to [-35 118 533]\n" + ] + } + ], + "source": [ + "pos = Coord(-35, 118, 533)\n", + "teleport(pos)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "f4ca4cc8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "warning: unknown material: minecraft:short_grass\n", + "123\n" + ] + } + ], + "source": [ + "# in game, point player east at cherry trees across the river to get distance\n", + "dist = whats_that(mc)['distance'] - 4\n", + "print(dist)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "100b68a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-35 118 533]\n", + "Will build subway line from [-32 119 533] to [ 91 119 533]:\n", + " traveling 124.0 blocks east, in 3 sections of 32 blocks,\n", + " with 28 blocks split between the ends.\n", + "Completed the on-ramp section\n", + "Finished midsection 1 of 3\n", + "Finished midsection 2 of 3\n", + "Finished midsection 3 of 3\n", + "Completed the off-ramp section\n", + "\n", + "Subway complete! Placed 15086 blocks in total.\n" + ] + } + ], + "source": [ + "\n", + "observer = Coord(mc.player.getTilePos()).block()\n", + "pointing = Vec3D(mc.player.getDirection()).cardinal_direction('coord')\n", + "print(observer)\n", + " \n", + "section_length = 32\n", + "n_sections = dist // section_length\n", + "on_off_ramps = dist % section_length\n", + "pos = observer.copy()\n", + "pos.y += 1 # track at the same block level as the player\n", + "pos += pointing * 3 # two blocks in front of the player\n", + "\n", + "there = pos + pointing * (n_sections * section_length + on_off_ramps)\n", + "_ = build_subway_line(\n", + " here=pos, \n", + " there=there,\n", + " section_lengths=section_length,\n", + " delay_factor=2,\n", + " calcs_only=False,\n", + " max_support_height=60,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "2a39243d", + "metadata": {}, + "source": [ + "### Search for nearby biomes\n", + "I was thinking of teleporting to each biome and trying to randomly sample\n", + "all the blocktypes in the game... but I think this would take a long time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e7c31d1", + "metadata": {}, + "outputs": [], + "source": [ + "biomes = sorted({m.value for _, m in Biome.__members__.items()})\n", + "biomes_at = mc.runCommands([f'locate biome {b}' for b in biomes], 1.0)\n", + "biomes_at = {b: v for b, v in zip(biomes, biomes_at)}\n", + "biomes_at" + ] + }, + { + "cell_type": "markdown", + "id": "fefdcdbd", + "metadata": {}, + "source": [ + "------------------\n", + "# The `rcon`, `mcipc`, `mcwb` and `mciwb` Libraries\n", + "\n", + "This is just some stuff I messed around with before modifying `FruitJuice` \n", + "and `pyncraft` modules.\n", + "\n", + "\n", + "------------------\n", + "\n", + "## Use RCON For Commands Via The mcipc Client\n", + "- Allows you to run any command you can do from the command line\n", + "- Powerful, but limited-- can't get entity UIDs or Block state (data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75c278d9", + "metadata": {}, + "outputs": [], + "source": [ + "def nbt_to_dict(nbt: str) -> dict:\n", + " \"\"\"\n", + " Convert NBT data to a dictionary using json.loads.\n", + "\n", + " This mostly works, but not always.\n", + " \"\"\"\n", + " if nbt.endswith('...'):\n", + " raise ValueError(\n", + " f\"NBT data is truncated (ends with '...'); can't parse:\\n{nbt}\")\n", + "\n", + " match = re.search(r'^([A-Za-z0-9]+) has the following ([A-Za-z_]+) data: (.*)$', nbt)\n", + " if not match:\n", + " raise ValueError(f\"Failed to parse NBT string:\\n{nbt}\")\n", + " \n", + " what = match.group(1)\n", + " data_type = match.group(2)\n", + " data = match.group(3)\n", + "\n", + " pre = '([ {\\[])'\n", + " term = '([ ,}\\]])'\n", + " # convert 0b and 1b to False and True\n", + " data = re.sub(f'{pre}0b{term}', r'\\1false\\2', data)\n", + " data = re.sub(f'{pre}1b{term}', r'\\1true\\2', data)\n", + " # numeric types : remove \"s\", \"l\", \"d\" and \"f\" suffixes\n", + " data = re.sub(f'{pre}(-?[0-9]+)[sl]?{term}', r'\\1\\2\\3', data)\n", + " data = re.sub(f'{pre}(-?[0-9\\.]+)[df]{term}', r'\\1\\2\\3', data)\n", + " # convert other strings ending in a letter to string\n", + " data = re.sub(f'{pre}([0-9]+[a-z]){term}', r'\\1\"\\2\"\\3', data)\n", + " # double quotes around the strings\n", + " prefix = '([\\[{ ])'\n", + " data = re.sub(f'{prefix}([A-Z_a-z]+): ', r'\\1\"\\2\": ', data)\n", + " # remove type indicator prefix from lists\n", + " data = re.sub(f'\\[[ILFDB]; ', r'[', data)\n", + "\n", + " # to avoid hitting string length limit, extract embedded lists first\n", + " lists = re.findall('\"([A-Z_a-z]+)\": (\\[[^\\]]+\\])', data)\n", + " list_dict = {k: v for k, v in lists}\n", + " updater = {}\n", + " for k, v in list_dict.items():\n", + " data = data.replace(v, '[]')\n", + " d = json.loads('{\"' + k + '\": ' + v + '}')\n", + " updater[k] = d[k]\n", + " \n", + " d = json.loads(data)\n", + " d.update(updater)\n", + "\n", + " return what, data_type, {k: d[k] for k in sorted(d.keys())}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2ef1865", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# interact via the RCON port using the mcipc Client\n", + "# I've been running this in the debugger with a breakpoint set a few lines in.\n", + "# The client seems to only work in a context manager\n", + "with Client('localhost', rcon_port, passwd=rcon_pw) as client:\n", + " seed = client.seed\n", + " players = dict(client.list())\n", + "\n", + " here = (442, 73, -1060)\n", + " client.setblock(pos=here, block='stone_stairs[facing=north,half=top]')\n", + " \n", + " client.run('setblock', '442', '73', '-1062', 'stone_stairs[facing=north,half=top]')#, 'half', 'top')\n", + " # # these examples from the docs don't work:\n", + " # mansion = client.locate('mansion')\n", + " # badlands = client.locatebiome('badlands')\n", + " client.run('tp', 'Acaimo', *here)\n", + " client.run('effect', 'give', 'Acaimo', 'night_vision', 'infinite', 255, True)\n", + "\n", + " # but these will:\n", + " mansion = client.run('locate', 'structure', 'mansion')\n", + " badlands = client.run('locate', 'biome', 'badlands')\n", + "\n", + " # when run with Paper/FruitJuice, output is truncated to 200 characters\n", + " nbt = client.data.get(entity='Acaimo') \n", + " # nbt = client.data.get(block=Vec3(193, 71, 61)) # blocks have states, not NBT data\n", + " what, dtype, data = nbt_to_dict(nbt)\n", + " print(f'{dtype}: \"{what}\"') \n", + "\n", + "\n", + " # print(client.xp()('Acaimo', 1000))\n", + "\n", + " \n", + "print(seed) \n", + "print(players)\n", + "display({k: v for k, v in data.items() if k in ['pos', 'Pos', 'Rotation', 'UUID']}) " + ] + }, + { + "cell_type": "markdown", + "id": "989481d3", + "metadata": {}, + "source": [ + "## Playing with `mciwb` MineCraftServer\n", + "- I got this to work sometimes, but then it wouldn't reconnect.\n", + "- The package has a bunch of code to download and control the docker server\n", + " as a class. But you still have to install docker first.\n", + "- I think it is easier to just specify the server config in `docker-compose.yml`\n", + "- You can then start and stop the server like:\n", + "```\n", + "# to start the server:\n", + "> docker compose up # add -d flag for detached mode\n", + "\n", + "# if not detached, use cntl-C to stop the server\n", + "# to remove the server (if you want to reconfigure it)\n", + "> docker compose down\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21e40e09", + "metadata": {}, + "outputs": [], + "source": [ + "# # attempt to get the mciwb MineCraftServer to start up; then connect with Client\n", + "# pw = 'aspergers-with-cheese'\n", + "# server_name = 'mciwb-server'\n", + "# server_port = 20101\n", + "# rcon_port = server_port - 1\n", + "# server_folder = Path.home() / 'minecraft/mciwb-server'\n", + "# backup_folder = Path.home() / 'minecraft/mciwb-backup'\n", + "# world_type = 'normal'\n", + "# player_name = 'Acaimo'\n", + "\n", + "# mc = MinecraftServer(\n", + "# name=server_name,\n", + "# rcon=rcon_port,\n", + "# password=pw,\n", + "# server_folder=server_folder,\n", + "# world_type=world_type,\n", + "# backup_folder=backup_folder,\n", + "# )\n", + "\n", + "# print('\\n '.join(['MinecraftServer methods:'] + [\n", + "# m for m in dir(mc) if not m.startswith('_') and isinstance(getattr(mc, m), Callable)]))\n", + "\n", + "# print('\\n '.join(['MinecraftServer attributes:'] + [\n", + "# m for m in dir(mc) if not m.startswith('_') and not isinstance(getattr(mc, m), Callable)]))\n", + "\n", + "# mc.create()" + ] + }, + { + "cell_type": "markdown", + "id": "6c4ef18a", + "metadata": {}, + "source": [ + "## Try using the MCIWB client\n", + "- this works, but no real advantage over mcipc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53e6f9dc", + "metadata": {}, + "outputs": [], + "source": [ + "from mciwb.threads import get_client, set_client, get_thread_name\n", + "\n", + " \n", + "set_client(Client('localhost', rcon_port, passwd=pw))\n", + "# client = get_client()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8646580", + "metadata": {}, + "outputs": [], + "source": [ + "# as with the MCIPC Client, must use the context manager\n", + "with get_client() as client:\n", + " here = (50, 76, -160)\n", + " client.setblock(Vec3(*here), 'glass')\n", + "\n", + "# client is essentially the same as the MCIPC Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e92df51", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv-juice", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyncraft/connection.py b/pyncraft/connection.py index c42add6..90375d1 100644 --- a/pyncraft/connection.py +++ b/pyncraft/connection.py @@ -9,7 +9,7 @@ class RequestError(Exception): pass class Connection: - """Connection to a Minecraft Pi game""" + """Connection to a Minecraft game""" RequestFailed = "Fail" def __init__(self, address, port): @@ -24,8 +24,8 @@ def drain(self): if not readable: break data = self.socket.recv(1500) - e = "Drained Data: <%s>\n"%data.strip() - e += "Last Message: <%s>\n"%self.lastSent.strip() + e = "Drained Data: <%s>\n"%data.strip().decode('cp437') + e += "Last Message: <%s>\n"%self.lastSent.strip().decode('cp437') sys.stderr.write(e) def send(self, f, *data): @@ -53,7 +53,9 @@ def receive(self): s = self.socket.makefile("r").readline().rstrip("\n") checkFail = s.split(",") if checkFail[0] == Connection.RequestFailed: - raise RequestError("%s failed! Cause: %s" % (self.lastSent.strip(),checkFail[-1])) + self.drain() + raise RequestError( + f"{self.lastSent.strip()} failed! Cause: {checkFail[-1]}") return s def sendReceive(self, *data): diff --git a/pyncraft/coord.py b/pyncraft/coord.py new file mode 100644 index 0000000..461e15c --- /dev/null +++ b/pyncraft/coord.py @@ -0,0 +1,279 @@ +from typing import Callable, Iterable, Union, Optional + +import numpy as np + + + +class Coord(np.ndarray): + """ + Implement 3D coordinates as a 3-element (x, y, z) numpy array subclass. + + This avoids having to reinvent the wheel for basic vector operations. + You can pass in a Vec3, list, tuple, pd.Series or individual x, y, z values. + They must be numeric (int or float). + + Parameters + ---------- + x : int, float, Iterable + The x coordinate or a 3-element iterable containing x, y, z coordinates. + Positive x is east, negative x is west. + y : int, float, None + The y coordinate (vertical position). Positive y is up, negative y is + down. Unused if `x` is an iterable. + z : int, float, None + The z coordinate. Positive z is south, negative z is north. Unused if + `x` is an iterable. + """ + + __int_types__ = (int, np.int32, np.int64) + __float_types__ = (float, np.float32, np.float64) + __numeric__ = __int_types__ + __float_types__ + + def __new__(cls, + x: Union[int, float, Iterable], + y: Union[int, float, None] = None, + z: Union[int, float, None] = None + ): + if isinstance(x, Iterable): + x = [v for i, v in enumerate(x) if i < 4] + if len(x) != 3: + raise ValueError(f"Expected 3 coordinates, got {len(x)}: {x}") + x, y, z = tuple(x) + else: + if not isinstance(x, cls.__numeric__): + raise TypeError(f"x must be int or float, got {type(x)}: {x}") + if not isinstance(y, cls.__numeric__): + raise TypeError(f"y must be int or float, got {type(y)}: {y}") + if not isinstance(z, cls.__numeric__): + raise TypeError(f"z must be int or float, got {type(z)}: {z}") + + obj = np.asarray([x, y, z]).view(cls) + return obj + + # accessor methods convert numpy int/float to Python int/float + @property + def x(self): + return int(self[0]) if isinstance(self[0], self.__int_types__) else float(self[0]) + + @x.setter + def x(self, value): + self[0] = value + + @property + def y(self): + return int(self[1]) if isinstance(self[1], self.__int_types__) else float(self[1]) + + @y.setter + def y(self, value): + self[1] = value + + @property + def z(self): + return int(self[2]) if isinstance(self[2], self.__int_types__) else float(self[2]) + + @z.setter + def z(self, value): + self[2] = value + + + def adjust(self, axis: str, delta: Union[int, float]) -> 'Coord': + """ + Copy Coord and adjust one axis (x, y or z) by delta + """ + axis = {'x': 0, 'y': 1, 'z': 2}.get(axis.lower()) + coord = self.copy() + coord[axis] += delta + return coord + + + def block(self, as_int: bool = True) -> 'Coord': + """ + Get the whole-number block coordinates as Coord object By Flooring. + + If as_int is True, returns the block coordinate as an integer. + Otherwise, float. + """ + block = np.floor(self) + if as_int: + block = block.astype(int) + return block + + + def midblock(self) -> 'Coord': + return self.block(as_int=False) + Coord(0.5, 0.0, 0.5) + + + def coplane(self, c2: 'Coord') -> bool: + """ + Check if second coordinate is in the same plane. + """ + + return Vec3D(self, c2).coplane() + + + def direction(self, c2: 'Coord') -> 'Vec3D': + """ + Get the unit vector from c1 to c2 (normalized to length 1). + """ + return Vec3D(self, c2).direction() + + + def cardinal_direction(self, c2: 'Coord', *args, **kwargs) -> Union[str, 'Coord']: + """ + Get the Cardinal Direction of a Second Coordinate. + + See help for Vec3D.cardinal_direction() for details. + """ + return Vec3D(self, c2).cardinal_direction(*args, **kwargs) + + + def distance(self, c2: 'Coord') -> float: + """ + Calculate the distance between two coordinates. + """ + return float(np.sqrt(((self - c2) ** 2).sum())) + + + +class Vec3D(Coord): + """ + A 3D vector class that takes one or two Coord objects as input. + """ + + def __init__(self, c1: Coord, c2: Coord = None): + + v = c1 if c2 is None else c2 - c1 + + self.x = v.x + self.y = v.y + self.z = v.z + + + def rotateLeft(self): self.x, self.z = self.z, -self.x + + def rotateRight(self): self.x, self.z = -self.z, self.x + + + def is_unit(self): + """Test if the vector is a unit vector (normalized to length 1).""" + return np.isclose(np.linalg.norm(self), 1.0) + + + def direction(self) -> Coord: + """ + Get the unit vector from c1 to c2 (normalized to length 1). + """ + return (self) / np.linalg.norm(self) + + + def coplane(self, by_block: bool = True) -> bool: + """ + Check if second coordinate is in the same plane. + """ + + if by_block: + pass + + plane = '' + if np.isclose(self.x, 0.0): + plane += 'x' + if np.isclose(self.y, 0.0): + plane += 'y' + if np.isclose(self.z, 0.0): + plane += 'z' + + return plane + + + def cardinal_direction( + self, + returns: str = 'string', + compass_points: int = 4 + ) -> Union[str, int, Coord]: + """ + Get the Cardinal Direction Of The Vector In The XY Plane. + + Uses the x and z components only, and returns the closest direction + based on the number fo compass points. Direction may be returned + as a string ("north"), minecraft rotation value (0-15), degrees (0-360), + radians (0-2pi), or a Vec3D/Coord unit vector with y==0. + + Note that when there is a tie (like x=1 and z=-1 with 4 compass points), + I'm not doing anything specific to resolve (north or east in this case). + Could return 'indeteminate', None/np.nan or raise an exception. Also + leaving it up to the user whether to convert Coords to integer. + + Parameters + ---------- + returns : str + The type of return value. Can be 'string', 'rotation', 'coord', or + 'degrees'. Default is 'string'. + compass_points : int + The number of compass points to use for the direction. + Default is 4 (north, east, south, west). If set to 8, it will also + return northeast, southeast, southwest, and northwest. If set + to 16, it will return all 16 compass points ('northnortheast', + 'northeast', 'eastnortheast', 'east', etc). + + Returns + ------- + Union[str, int, Coord]: + If returns is string: 'north', 'south', 'east', 'west',. + """ + ok = {'string', 'rotation', 'coord', 'degrees', 'radians'} + invalid = {returns} - ok + if invalid: + raise ValueError( + 'Invalid returns argument "{returns}"; must be one of:\n ' + + ', '.join(list(ok))) + + if self.x == 0 and self.z == 0: + raise ValueError('Vector point straight up or down; indeterminate.') + + # drop the y component and normalize the xz vector (z -> y) + xz = np.delete(self, 1) + xz = xz / np.linalg.norm(xz) + + # atan2 returns angle from x-axis (east), so swap arguments and invert z + # calculate fraction of a turn [0, 1) from North = 0 degrees + frac360 = (np.arctan2(xz.x, -xz.y) / (np.pi * 2)) % 1.0 + + bearings = ['north', 'northnortheast', 'northeast', 'eastnortheast', + 'east', 'eastsoutheast', 'southeast', 'southsoutheast', + 'south', 'southsouthwest', 'southwest', 'westsouthwest', + 'west', 'westnorthwest', 'northwest', 'northnorthwest', + 'north'] + + if compass_points > 0: + # convert to a rotation value, rounded to the nearest compass point + rotation = int( + np.round(frac360 * compass_points) * 16 / compass_points) + + if returns == 'rotation': + return rotation + elif returns == 'degrees': + return rotation * 360 / 16 + elif returns == 'string': + return bearings[rotation] + else: + radians = rotation * 2 * np.pi / 16 + if returns == 'radians': + return radians + else: + # rounded unit vector in the xz plane + coord = Coord(np.sin(radians), 0, -np.cos(radians)) + if compass_points == 4: + coord = coord.astype(int) + return coord + elif returns in ('string', 'rotation'): + raise ValueError( + f'Must set compass_points > 0 for returns "string" or "rotation"') + else: + # return results without rounding + if returns == 'degrees': + return frac360 * 360 + else: + if returns == 'radians': + return radians + else: + return Coord(np.sin(radians), 0,-np.cos(radians)) \ No newline at end of file diff --git a/pyncraft/minecraft.py b/pyncraft/minecraft.py index 10f567d..1fbd81d 100644 --- a/pyncraft/minecraft.py +++ b/pyncraft/minecraft.py @@ -1,12 +1,20 @@ +import os +import re +import sys +from time import sleep +from typing import Optional, Union +from warnings import warn + +from mcipc.rcon.item import Item +from mcipc.rcon.je import Client as RconClient + from .connection import Connection from .vec3 import Vec3 from .event import BlockEvent, ChatEvent, ArrowHitEvent #from .entity import Entity #from .block import Block from .util import flatten -from warnings import warn from .logger import * -import sys, ipdb """ Minecraft PI low level api v0.1_1 @@ -200,7 +208,7 @@ def __init__(self, connection, playerId): self.camera = CmdCamera(connection) self.entity = CmdEntity(connection) - self.cmdplayer = CmdPlayer(connection,playerId) + self.cmdplayer = CmdPlayer(connection, playerId) # not sure why mcpi_e did this, but it doesn't work with empty playerIds #self.player = CmdPlayerEntity(connection,playerId) @@ -209,16 +217,139 @@ def __init__(self, connection, playerId): self.events = CmdEvents(connection) self.playerId = playerId self.settings = settings + + + def runCommands(self, cmds: Union[list, str], pause: float = 0.0) -> str: + """ + Run one or more commands on the Minecraft server and return the result. + + This method uses the RCON protocol to send commands to the server and + receive the output. All commands are executed using the same socket, + so it should be faster than if they were executed separately. + + Parameters + ---------- + cmds : Union[list, str] + A single command as a string, starting with the method name followed + by the arguments separated by spaces, or a list of such commands. + pause : float, optional + Time to pause between commands, to prevent the server from + being overloaded, by default 0. + + Returns + ------- + str + Output of the commands, separated by newlines. + """ + + address = os.getenv('PYNCRAFT_ADDRESS') + if address == '': + raise Exception("PYNCRAFT_ADDRESS environment variable unset.") + rcon_port = os.getenv('RCON_PORT') + rcon_pw = os.getenv('RCON_PASSWORD') + if rcon_port == '' or rcon_pw == '': + raise Exception( + "RCON_PORT and RCON_PASSWORD environment variables unset.") + + if isinstance(cmds, str): + cmds = [cmds] + + with RconClient('localhost', int(rcon_port), passwd=rcon_pw) as client: + + result = [] + for cmd in cmds: + args = cmd.replace(' ', ' ').split(' ') + result.append(client.run(*args)) + if pause > 0: + sleep(pause) + + return result + def getBlock(self, x:int, y:int, z:int) -> str: """Get block (x,y,z) => id:int""" return self.conn.sendReceive(b"world.getBlock", x, y, z) # Not supported by FruitJuice. Why not? - def getBlockWithData(self, x:int, y:int, z:int): - """Get block with data (x,y,z) => Block""" - ans = self.conn.sendReceive(b"world.getBlockWithData", x, y, z) - return Block(*list(map(int, ans.split(",")))) + # def getBlockWithData(self, x:int, y:int, z:int): + # """Get block with data (x,y,z) => Block""" + # ans = self.conn.sendReceive(b"world.getBlockWithData", x, y, z) + # return Block(*list(map(int, ans.split(",")))) + + def getBlockWithData( + self, + x: Union[int, Vec3], + y: Optional[int] = None, + z: Optional[int] = None, + parse: bool=True + ) -> dict: + """ + Return material and state (if any) of a block at the given coordinates. + + Parameters + ---------- + x : Union[int, Vec3] + Vec3 specifying the blocks coordinates, or x coordinate + y : Optional[int], optional + y coordinate, by default None; required if x is an int + z : Optional[int], optional + z coordinate, by default None; required if x is an int + parse : bool, optional + If True (default), parse the block state string into key-value pairs. + If False, return the string as is with key 'state'. + + Returns + ------- + dict + Dictionary with x, y, z coordinates, material, and state (if any). + If parse is True, the state is a dictionary of key-value pairs. + If parse is False, the state is a string; keys is 'state', value is '' + if no state info found. + """ + + if isinstance(x, Vec3): + pos = x + else: + pos = Vec3(x, y, z) + + data = {'x': pos.x, 'y': pos.y, 'z': pos.z} + datastr = self.conn.sendReceive(b"world.getBlockData", pos.x, pos.y, pos.z) + + if not datastr.startswith('CraftBlockData'): + return data + + # parse the string to get the block type, and any other attributes + patt = re.compile(r'^CraftBlockData{' + + r'(?Pminecraft:[a-z_]+)(?P\[[-.,_=A-Za-z0-9]+\])?}') + m = patt.match(datastr) + if not m: + # should this raise an exception? + return data + + material = m.groups()[0] + try: + material = Item(material).name + except Exception: + print(f'warning: unknown material: {material}') + material = material.replace('minecraft:', '').upper() + + data.update({'material': material}) + attrs = m.groups()[1] + if attrs: + if parse: + # extract the other parameters as key-value pairs + attrs = [tuple(a.split('=')) for a in attrs[1:-1].split(',')] + attrs = {k: v for k, v in attrs} + else: + # just return the string + attrs = {'state': attrs[1:-1]} + data.update(attrs) + elif not parse: + # not state was retrieved, so set state to empty string + data.update({'state': ''}) + + return data + def getBlocks(self, x1:int, y1:int, z1:int, x2:int, y2:int, z2:int) -> list: """Get a cuboid of blocks (x0,y0,z0,x1,y1,z1) => [id:int]""" @@ -259,13 +390,14 @@ def getPlayerEntityIds(self) -> list: ids = self.conn.sendReceive(b"world.getPlayerIds") return list(map(int, ids.split("|"))) - def saveCheckpoint(self): - """Save a checkpoint that can be used for restoring the world""" - self.conn.send(b"world.checkpoint.save") + # these methods don't work in FruitJuice, so they are commented out + # def saveCheckpoint(self): + # """Save a checkpoint that can be used for restoring the world""" + # self.conn.sendReceive(b"world.checkpoint.save") - def restoreCheckpoint(self): - """Restore the world state to the checkpoint""" - self.conn.send(b"world.checkpoint.restore") + # def restoreCheckpoint(self): + # """Restore the world state to the checkpoint""" + # self.conn.sendReceive(b"world.checkpoint.restore") def postToChat(self, *msg) -> None: """Post a message to the game chat""" @@ -349,15 +481,17 @@ def setting(self, setting, status): def create(address = "localhost", port = 4711, playerName = ""): #return Minecraft(Connection(address, port)) - - log("Running Python version:"+sys.version) + log(f"Running Python version: {sys.version}") conn=Connection(address, port) playerId = [] if playerName != "": playerId = int(conn.sendReceive(b"world.getPlayerId", playerName)) - log("get {} playerid={}".format(playerName, playerId)) + log(f"get {playerName} playerid={playerId}") - return Minecraft(conn,playerId) + os.environ['PYNCRAFT_ADDRESS'] = address + + return Minecraft(conn, playerId) + if __name__ == "__main__": mc = Minecraft.create() diff --git a/pyncraft/vec3.py b/pyncraft/vec3.py index f0cd42a..c20aaf6 100644 --- a/pyncraft/vec3.py +++ b/pyncraft/vec3.py @@ -1,8 +1,10 @@ class Vec3: def __init__(self, x=0, y=0, z=0): - self.x = x - self.y = y - self.z = z + # this avoids problems when a string is passed in + self.x = float(x) if isinstance(x, str) else x + self.y = float(y) if isinstance(y, str) else y + self.z = float(z) if isinstance(z, str) else z + def __add__(self, rhs): c = self.clone() diff --git a/setup.py b/setup.py index 4c4ea9d..5914bda 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", 'Programming Language :: Python :: 3 :: Only', ], @@ -141,7 +142,7 @@ # # For an analysis of "install_requires" vs pip's requirements files see: # https://packaging.python.org/discussions/install-requires-vs-requirements/ - install_requires=["ipdb","stl-to-voxel"], + install_requires=["ipdb","stl-to-voxel","mcipc"], # List additional groups of dependencies here (e.g. development # dependencies). Users will be able to install these using the "extras"