├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── vox │ │ ├── vxs.vox │ │ ├── alien_engi1a.vox │ │ └── chr_beardo3-default-palette.vox │ ├── schematics │ │ └── alien_engi1a.schematic │ ├── run_tests.py │ ├── test_utils.py │ ├── test_vox.py │ └── test_blocks_memory.py └── integration │ ├── __init__.py │ ├── vox │ ├── vxs.vox │ ├── alien_engi1a.vox │ ├── minecraft_wool.vox │ ├── vxs_glass_ball.vox │ ├── veh_ambulance_mc.vox │ ├── minecraft_wool_palette.png │ └── chr_beardo3-default-palette.vox │ ├── schematics │ ├── scene.schematic │ ├── ship2.schematic │ ├── sphinx.schematic │ ├── zanabot.schematic │ ├── pirate-boat.schematic │ ├── zanabot-tnt.schematic │ ├── alien_engi1a.schematic │ ├── chateau-fairmont.schematic │ ├── veh_ambulance_mc.schematic │ ├── obj_house6-colours.schematic │ └── README.md │ ├── run_tests.py │ ├── test_wall.py │ ├── test_wool.py │ ├── test_blocks_gallery.py │ ├── test_circle.py │ ├── test_scene.py │ ├── test_building.py │ ├── base.py │ ├── test_block.py │ ├── test_river.py │ ├── test_collage.py │ ├── test_house.py │ ├── test_flip_blocks.py │ ├── test_bridge.py │ ├── test_town.py │ ├── test_line.py │ ├── test_sphere.py │ ├── test_platform.py │ ├── test_fence.py │ ├── test_rotate_schematic.py │ ├── test_pyramid.py │ ├── test_border_decorator.py │ ├── test_blocks.py │ ├── test_rotate_blocks.py │ ├── test_schematic.py │ ├── test_lines.py │ └── test_vox.py ├── mcthings ├── __init__.py ├── decorators │ ├── __init__.py │ ├── light_decorator.py │ ├── decorator.py │ └── border_decorator.py ├── renderers │ ├── __init__.py │ ├── renderer.py │ └── raspberry_pi.py ├── _version.py ├── block.py ├── circle.py ├── wall.py ├── blocks.py ├── blocks_gallery.py ├── line.py ├── building.py ├── wool.py ├── collage.py ├── river.py ├── platform.py ├── town.py ├── bridge.py ├── sphere.py ├── world.py ├── pyramid.py ├── house.py ├── fence.py ├── schematic.py ├── scene.py ├── thing.py ├── utils.py ├── blocks_memory.py └── vox.py ├── scenes └── README.md ├── docs ├── img │ ├── twitter.png │ └── ambulance_mc.png ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── .travis.yml ├── release.sh ├── bin ├── README.md └── vox2schematic ├── CONTRIBUTING.md ├── USAGE.md ├── setup.py ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcthings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcthings/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mcthings/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scenes/README.md: -------------------------------------------------------------------------------- 1 | # LEGACY: Go to https://github.com/juntosdesdecasa/mcthings_scenes -------------------------------------------------------------------------------- /docs/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/docs/img/twitter.png -------------------------------------------------------------------------------- /tests/unit/vox/vxs.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/unit/vox/vxs.vox -------------------------------------------------------------------------------- /docs/img/ambulance_mc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/docs/img/ambulance_mc.png -------------------------------------------------------------------------------- /tests/integration/vox/vxs.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/vxs.vox -------------------------------------------------------------------------------- /tests/unit/vox/alien_engi1a.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/unit/vox/alien_engi1a.vox -------------------------------------------------------------------------------- /tests/integration/vox/alien_engi1a.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/alien_engi1a.vox -------------------------------------------------------------------------------- /tests/integration/vox/minecraft_wool.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/minecraft_wool.vox -------------------------------------------------------------------------------- /tests/integration/vox/vxs_glass_ball.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/vxs_glass_ball.vox -------------------------------------------------------------------------------- /tests/integration/vox/veh_ambulance_mc.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/veh_ambulance_mc.vox -------------------------------------------------------------------------------- /tests/integration/schematics/scene.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/scene.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/ship2.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/ship2.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/sphinx.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/sphinx.schematic -------------------------------------------------------------------------------- /tests/unit/schematics/alien_engi1a.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/unit/schematics/alien_engi1a.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/zanabot.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/zanabot.schematic -------------------------------------------------------------------------------- /tests/unit/vox/chr_beardo3-default-palette.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/unit/vox/chr_beardo3-default-palette.vox -------------------------------------------------------------------------------- /tests/integration/schematics/pirate-boat.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/pirate-boat.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/zanabot-tnt.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/zanabot-tnt.schematic -------------------------------------------------------------------------------- /tests/integration/vox/minecraft_wool_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/minecraft_wool_palette.png -------------------------------------------------------------------------------- /tests/integration/schematics/alien_engi1a.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/alien_engi1a.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/chateau-fairmont.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/chateau-fairmont.schematic -------------------------------------------------------------------------------- /tests/integration/schematics/veh_ambulance_mc.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/veh_ambulance_mc.schematic -------------------------------------------------------------------------------- /tests/integration/vox/chr_beardo3-default-palette.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/vox/chr_beardo3-default-palette.vox -------------------------------------------------------------------------------- /tests/integration/schematics/obj_house6-colours.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelers/mcthings/HEAD/tests/integration/schematics/obj_house6-colours.schematic -------------------------------------------------------------------------------- /mcthings/_version.py: -------------------------------------------------------------------------------- 1 | # Versions compliant with PEP 440 https://www.python.org/dev/peps/pep-0440 2 | # Following semver: https://semver.org/ 3 | __version__ = "0.60.0" 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | sudo: false 7 | 8 | before_install: 9 | - pip install --upgrade setuptools 10 | - pip install --upgrade pip 11 | 12 | install: 13 | - pip install . 14 | 15 | script: 16 | - cd tests/unit 17 | - PYTHONPATH=../..:.. ./run_tests.py 18 | -------------------------------------------------------------------------------- /mcthings/block.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | from mcpi.vec3 import Vec3 5 | 6 | from .thing import Thing 7 | 8 | 9 | class Block(Thing): 10 | 11 | def create(self): 12 | self.set_block(Vec3(self.position.x, self.position.y, self.position.z), self.block.id) 13 | self._end_position = self.position 14 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | # Increment version in setup.py 7 | # Commit and tag with the new version 8 | # Push branch with tags 9 | # Generate pip packages and upload to pip repository 10 | cd tests/integration 11 | PYTHONPATH=../..:.. ./run_tests.py 12 | cd ../.. 13 | rm dist/* 14 | python setup.py sdist 15 | python setup.py bdist_wheel 16 | twine upload dist/* 17 | -------------------------------------------------------------------------------- /tests/integration/schematics/README.md: -------------------------------------------------------------------------------- 1 | # Schematics 2 | 3 | The schematics in this folder are samples used to develop the support 4 | of Schematic loading in McThings. Please, read the authors requirements 5 | for reusing them in your own creations: 6 | 7 | * [Pirate Boat](https://www.minecraft-schematics.com/schematic/68/) 8 | * [Chateau Fairmont](https://www.minecraft-schematics.com/schematic/9676/) 9 | * [Sphinix](https://www.planetminecraft.com/project/sphinx/) 10 | * Zanabot: Created in Juntos desde Casa workshops -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. McThings documentation master file, created by 2 | sphinx-quickstart on Mon Apr 13 05:06:29 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to McThings's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /tests/unit/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import os 7 | import sys 8 | import unittest 9 | 10 | 11 | if __name__ == '__main__': 12 | 13 | test_suite = unittest.TestLoader().discover(os.path.join(os.path.dirname(os.path.abspath(__file__)), "."), 14 | pattern='test_*.py') 15 | result = unittest.TextTestRunner(buffer=True).run(test_suite) 16 | sys.exit(not result.wasSuccessful()) 17 | -------------------------------------------------------------------------------- /tests/integration/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import os 7 | import sys 8 | import unittest 9 | 10 | 11 | if __name__ == '__main__': 12 | 13 | test_suite = unittest.TestLoader().discover(os.path.join(os.path.dirname(os.path.abspath(__file__)), "."), 14 | pattern='test_*.py') 15 | result = unittest.TextTestRunner(buffer=True).run(test_suite) 16 | sys.exit(not result.wasSuccessful()) 17 | -------------------------------------------------------------------------------- /mcthings/circle.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | from mcthings.thing import Thing 5 | from mcthings.world import World 6 | 7 | 8 | class Circle(Thing): 9 | 10 | radius = None 11 | """ radius of the Sphere """ 12 | 13 | def build(self): 14 | World.renderer.server.drawing.drawCircle( 15 | self.position.x, 16 | self.position.y, 17 | self.position.z, 18 | self.radius, 19 | self.block) 20 | self._end_position = self.position 21 | -------------------------------------------------------------------------------- /mcthings/wall.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | from mcpi.vec3 import Vec3 4 | 5 | from .thing import Thing 6 | 7 | 8 | class Wall(Thing): 9 | height = 5 10 | length = 10 11 | width = 2 12 | 13 | def create(self): 14 | self.set_blocks( 15 | Vec3(self.position.x, self.position.y, self.position.z), 16 | Vec3(self.position.x + self.length - 1, 17 | self.position.y + self.height - 1, 18 | self.position.z + self.width - 1), 19 | self.block.id) 20 | -------------------------------------------------------------------------------- /mcthings/blocks.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | from mcpi.vec3 import Vec3 5 | 6 | from .thing import Thing 7 | 8 | 9 | class Blocks(Thing): 10 | width = 3 # x 11 | height = 2 # y 12 | length = 4 # z 13 | 14 | def create(self): 15 | p = self.position 16 | self._end_position = Vec3(p.x + self.width - 1, p.y + self.height - 1, p.z + self.length - 1) 17 | # self.set_blocks(Vec3(p.x, p.y, p.z), self._end_position, self.block.id) 18 | self.set_blocks(self._end_position, Vec3(p.x, p.y, p.z), self.block.id) 19 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /mcthings/blocks_gallery.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | 6 | from mcpi.vec3 import Vec3 7 | 8 | from .thing import Thing 9 | 10 | 11 | class BlocksGallery(Thing): 12 | # https://www.minecraftinfo.com/idlist.htm 13 | MAX_BLOCK_NUMBER = 247 14 | 15 | def create(self): 16 | """ 17 | Show all possible block types in a line 18 | :return: 19 | """ 20 | 21 | for i in range(1, self.MAX_BLOCK_NUMBER): 22 | p = self.position 23 | self.set_block(Vec3(p.x + i, p.y, p.z), i) 24 | 25 | self._end_position = Vec3(p.x + self.MAX_BLOCK_NUMBER - 1, p.y, p.z) 26 | -------------------------------------------------------------------------------- /tests/integration/test_wall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.wall import Wall 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestWall(TestBaseThing): 15 | """Test Wall Thing""" 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Building a wall") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 1 23 | 24 | wall = Wall(pos) 25 | wall.build() 26 | 27 | 28 | if __name__ == "__main__": 29 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 30 | unittest.main(warnings='ignore') 31 | -------------------------------------------------------------------------------- /tests/integration/test_wool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.wool import Wool 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestWool(TestBaseThing): 15 | """Test Wool Thing""" 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Building all wool colors") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 1 23 | 24 | wool = Wool(pos) 25 | wool.build() 26 | 27 | 28 | if __name__ == "__main__": 29 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 30 | unittest.main(warnings='ignore') 31 | -------------------------------------------------------------------------------- /tests/integration/test_blocks_gallery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.blocks_gallery import BlocksGallery 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestBlocksGallery(TestBaseThing): 15 | """Test Thing""" 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Building a blocks gallery with all available blocks") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 1 23 | 24 | gallery = BlocksGallery(pos) 25 | gallery.build() 26 | 27 | 28 | if __name__ == "__main__": 29 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 30 | unittest.main(warnings='ignore') 31 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | vox2schematic 2 | -- 3 | 4 | A conversion tool that reads a vox file with one model 5 | in original or current format and converts it voxel by voxel 6 | to blocks in a Minecraft Schematic file. 7 | The voxels are converted to wool blocks, and the color of the voxels 8 | are mapped to one of the 16 possible wools colors in the blocks. 9 | 10 | There is a [MagicaVoxel palette](https://github.com/Voxelers/mcthings/blob/develop/tests/integration/vox/minecraft_wool_palette.png) 11 | with the wool colors. If you use it in your model, the colors in Schematic blocks will be the same than the voxels ones. 12 | 13 | To install it just execute: 14 | 15 | `pip install mcthings` 16 | 17 | To execute it: 18 | 19 | `vox2schematic model.vox` 20 | 21 | and a `model.schematic` file will be created in the same directory. 22 | 23 | [Design and implementation details](https://github.com/Voxelers/mcthings/issues/99) 24 | -------------------------------------------------------------------------------- /tests/integration/test_circle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.circle import Circle 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestCircle(TestBaseThing): 17 | """Test Circle Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building a circle") 21 | 22 | pos = self.pos 23 | 24 | radius = 10 25 | pos.z += 20 26 | 27 | circle = Circle(pos) 28 | circle.radius = radius 29 | circle.block = mcpi.block.BEDROCK 30 | circle.build() 31 | 32 | 33 | if __name__ == "__main__": 34 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 35 | unittest.main(warnings='ignore') 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/integration/test_scene.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.block import Block 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestScene(TestBaseThing): 15 | """Test Scene Thing""" 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Building a scene") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 1 23 | block = Block(pos) 24 | block.build() 25 | 26 | pos.x += 2 27 | block = Block(pos) 28 | block.build() 29 | 30 | pos.y += 1 31 | World.first_scene().move(pos) 32 | 33 | 34 | if __name__ == "__main__": 35 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 36 | unittest.main(warnings='ignore') 37 | -------------------------------------------------------------------------------- /mcthings/line.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi.block 5 | from mcpi.vec3 import Vec3 6 | 7 | from .thing import Thing 8 | from .world import World 9 | 10 | 11 | class Line(Thing): 12 | 13 | width = 3 14 | length = 10 15 | 16 | def create(self): 17 | end_x = self.position.x + self.width - 1 18 | end_y = self.position.y - 1 19 | end_z = self.position.z + self.length - 1 20 | 21 | # Find the type of land block destroyed with the line 22 | self._block_empty = \ 23 | mcpi.block.Block(World.renderer.get_block(Vec3(self.position.x, self.position.y-1, self.position.z)), 0) 24 | 25 | self.set_blocks(Vec3(self.position.x, self.position.y-1, self.position.z), 26 | Vec3(end_x, end_y, end_z), 27 | self.block.id) 28 | 29 | self._end_position = Vec3(end_x, end_y + 1, end_z) 30 | -------------------------------------------------------------------------------- /mcthings/building.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | 6 | from .house import House 7 | from .thing import Thing 8 | 9 | 10 | class Building(Thing): 11 | 12 | floors = 10 13 | width = 10 14 | house_mirror = False 15 | 16 | def create(self): 17 | 18 | init_x = self.position.x 19 | init_y = self.position.y 20 | init_z = self.position.z 21 | 22 | house_pos = mcpi.vec3.Vec3(init_x, init_y, init_z) 23 | init_height = init_y 24 | 25 | for i in range(0, self.floors): 26 | house = House(house_pos, self) 27 | self.add_child(house) 28 | house_pos.y = house.height * i + init_height 29 | house.width = self.width 30 | house.block = self.block 31 | house.mirror = self.house_mirror 32 | house.create() 33 | self._end_position = house.end_position 34 | -------------------------------------------------------------------------------- /tests/integration/test_building.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.building import Building 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestBuilding(TestBaseThing): 17 | """Test Building Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building a building") 21 | 22 | pos = self.pos 23 | 24 | pos.x += 1 25 | 26 | building = Building(pos) 27 | building.block = mcpi.block.BEDROCK 28 | building.house_mirror = True 29 | building.build() 30 | 31 | Building(building.end_position).build() 32 | 33 | 34 | if __name__ == "__main__": 35 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 36 | unittest.main(warnings='ignore') 37 | -------------------------------------------------------------------------------- /tests/integration/base.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import logging 5 | import sys 6 | import unittest 7 | 8 | import mcpi 9 | 10 | from mcthings.renderers.raspberry_pi import _Server 11 | 12 | from mcthings.renderers.raspberry_pi import RaspberryPi 13 | from mcthings.world import World 14 | 15 | 16 | class TestBaseThing(unittest.TestCase): 17 | """ Integration tests for McThings """ 18 | 19 | BUILDER_NAME = "ElasticExplorer" 20 | 21 | MC_SEVER_HOST = "localhost" 22 | MC_SEVER_PORT = 4711 23 | 24 | @classmethod 25 | def setUpClass(cls): 26 | 27 | try: 28 | World.renderer = RaspberryPi(cls.MC_SEVER_HOST, cls.MC_SEVER_PORT) 29 | except mcpi.connection.RequestError: 30 | logging.error("Can't connect to Minecraft server " + cls.MC_SEVER_HOST) 31 | sys.exit(1) 32 | 33 | def setUp(self): 34 | self.pos = World.renderer.get_pos(self.BUILDER_NAME) 35 | 36 | -------------------------------------------------------------------------------- /mcthings/wool.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | 6 | from mcpi.vec3 import Vec3 7 | import mcpi.block 8 | 9 | from .thing import Thing 10 | 11 | 12 | class Wool(Thing): 13 | # Wool colors 14 | COLORS = [ 15 | "White", 16 | "Orange", 17 | "Magenta", 18 | "Light Blue", 19 | "Yellow", 20 | "Lime", 21 | "Pink", 22 | "Grey", 23 | "Light grey", 24 | "Cyan", 25 | "Purple", 26 | "Blue", 27 | "Brown", 28 | "Green", 29 | "Red", 30 | "Black" 31 | ] 32 | 33 | def create(self): 34 | """ 35 | Show all wool colors 36 | :return: 37 | """ 38 | 39 | for i in range(0, len(self.COLORS)): 40 | p = self.position 41 | self.set_block(Vec3(p.x + i, p.y, p.z), mcpi.block.WOOL.id, i) 42 | 43 | self._end_position = Vec3(p.x + len(self.COLORS) - 1, p.y, p.z) 44 | -------------------------------------------------------------------------------- /tests/integration/test_block.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | 11 | from mcthings.block import Block 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestBlock(TestBaseThing): 17 | """Test Block Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building two blocks") 21 | 22 | pos = self.pos 23 | 24 | pos.x += 1 25 | block = Block(pos) 26 | block.build() 27 | 28 | assert len(block._blocks_memory.blocks) == 1 29 | 30 | pos.x += 3 31 | block = Block(pos) 32 | block.build() 33 | block.unbuild() 34 | 35 | block.move(Vec3(pos.x+5, pos.y, pos.z)) 36 | 37 | 38 | if __name__ == "__main__": 39 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 40 | unittest.main(warnings='ignore') 41 | -------------------------------------------------------------------------------- /mcthings/collage.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | from mcpi.vec3 import Vec3 5 | import mcpi.block 6 | 7 | from .thing import Thing 8 | 9 | 10 | class Collage(Thing): 11 | width = 3 # x 12 | height = 2 # y 13 | length = 4 # z 14 | change_blocks = [mcpi.block.BEDROCK, mcpi.block.SAND, mcpi.block.GOLD_BLOCK, mcpi.block.IRON_BLOCK] 15 | 16 | def create(self): 17 | p = self.position 18 | count = 0 19 | for y in range(0, self.height): 20 | for z in range(0, self.length): 21 | for x in range(0, self.width): 22 | block = self.block 23 | if self.block != self._block_empty: 24 | block = self.change_blocks[count % len(self.change_blocks)] 25 | self.set_block(Vec3(p.x + x, p.y + y, p.z + z), block.id) 26 | count += 1 27 | 28 | self._end_position = Vec3(p.x + self.width - 1, p.y + self.height - 1, p.z + self.length - 1) 29 | -------------------------------------------------------------------------------- /mcthings/decorators/light_decorator.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import math 5 | 6 | import mcpi.block 7 | from mcpi.vec3 import Vec3 8 | 9 | from mcthings.block import Block 10 | from .decorator import Decorator 11 | 12 | 13 | class LightDecorator(Decorator): 14 | """ 15 | A Light Decorator to illuminate the Thing. 16 | 17 | Add lights (torches) to Thing so you can see inside it 18 | """ 19 | 20 | def create(self): 21 | """ 22 | Add a torch in the center of the Thing 23 | 24 | :return: 25 | """ 26 | 27 | thing_end = self._thing.end_position 28 | thing_start = self._thing.position 29 | 30 | center_pos_x = thing_start.x + math.floor((thing_end.x - thing_start.x) / 2) 31 | center_pos_y = thing_start.y + math.floor((thing_end.y - thing_start.y) / 2) 32 | center_pos_z = thing_start.z + math.floor((thing_end.z - thing_start.z) / 2) 33 | 34 | self.set_block(Vec3(center_pos_x, center_pos_y, center_pos_z), mcpi.block.TORCH) 35 | -------------------------------------------------------------------------------- /tests/integration/test_river.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | 11 | from mcthings.river import River 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestRiver(TestBaseThing): 17 | """Test River Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building a river") 21 | 22 | pos = self.pos 23 | 24 | pos.x += 1 25 | 26 | river = River(pos) 27 | river.width = 3 28 | river.depth = 3 29 | river.length = 5 30 | river.build() 31 | river.unbuild() 32 | 33 | pos = river.end_position 34 | river = River(Vec3(pos.x, self.pos.y, pos.z)) 35 | river.depth = 3 36 | river.build() 37 | 38 | 39 | if __name__ == "__main__": 40 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 41 | unittest.main(warnings='ignore') 42 | -------------------------------------------------------------------------------- /mcthings/river.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi.block 5 | from mcpi.vec3 import Vec3 6 | 7 | from .thing import Thing 8 | from .world import World 9 | 10 | 11 | class River(Thing): 12 | length = 100 13 | width = 2 14 | depth = 1 15 | block = mcpi.block.WATER_FLOWING 16 | 17 | def create(self): 18 | init_x = self.position.x 19 | init_y = self.position.y - self.depth 20 | init_z = self.position.z 21 | 22 | end_x = init_x + self.width - 1 23 | end_y = self.position.y - 1 24 | end_z = init_z + self.length - 1 25 | 26 | # Find the type of land block destroyed with the river 27 | self._block_empty = \ 28 | mcpi.block.Block(World.renderer.get_block(Vec3(init_x, self.position.y - 1, init_z)), 0) 29 | 30 | self.set_blocks( 31 | Vec3(init_x, init_y, init_z), 32 | Vec3(end_x, end_y, end_z), 33 | self.block.id) 34 | 35 | self._end_position = Vec3(end_x, self.position.y - self.depth, end_z) 36 | -------------------------------------------------------------------------------- /tests/integration/test_collage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | 11 | from mcthings.collage import Collage 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestBlocks(TestBaseThing): 17 | """Test Collage Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building collage") 21 | 22 | self.pos.z += 1 23 | blocks = Collage(self.pos) 24 | blocks.build() 25 | assert len(blocks._blocks_memory.blocks) == 24 26 | assert blocks._blocks_memory.blocks[23].id == 42 27 | 28 | self.pos.z += 10 29 | blocks = Collage(self.pos) 30 | blocks.build() 31 | 32 | blocks.move(Vec3(self.pos.x+5, self.pos.y, self.pos.z)) 33 | 34 | 35 | if __name__ == "__main__": 36 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 37 | unittest.main(warnings='ignore') 38 | -------------------------------------------------------------------------------- /mcthings/platform.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import math 5 | 6 | from mcpi.vec3 import Vec3 7 | 8 | from .thing import Thing 9 | 10 | 11 | class Platform(Thing): 12 | top_size = 3 # square platform at the top 13 | height = 10 # tower height 14 | 15 | def create(self): 16 | p = self.position 17 | 18 | # base of the tower 19 | base_x = p.x + math.floor(self.top_size/2) 20 | base_z = p.z + math.floor(self.top_size/2) 21 | self.set_blocks(Vec3(base_x, p.y, base_z), 22 | Vec3(base_x, p.y + self.height - 1, base_z), 23 | self.block.id) 24 | 25 | # Top 26 | self.set_blocks(Vec3(p.x, p.y + self.height, p.z), 27 | Vec3(p.x + self.top_size - 1, p.y + self.height, p.z + self.top_size - 1), 28 | self.block.id) 29 | 30 | self._end_position = Vec3(p.x + self.top_size - 1, 31 | p.y + self.height, 32 | p.z + self.top_size - 1) 33 | -------------------------------------------------------------------------------- /tests/integration/test_house.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | 10 | from mcthings.house import House 11 | from mcthings.decorators.light_decorator import LightDecorator 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestHouse(TestBaseThing): 17 | """Test House Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building a house") 21 | 22 | pos = self.pos 23 | 24 | pos.x += 1 25 | 26 | house = House(pos) 27 | house.build() 28 | 29 | # Mirror house 30 | pos.x -= 10 # space between both houses 31 | house = House(pos) 32 | house.mirror = True 33 | house.build() 34 | # Add lights 35 | house.add_decorator(LightDecorator) 36 | house.decorate() 37 | 38 | 39 | if __name__ == "__main__": 40 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 41 | unittest.main(warnings='ignore') 42 | -------------------------------------------------------------------------------- /tests/integration/test_flip_blocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.collage import Collage 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestFlipBlock(TestBaseThing): 15 | """ Test to flip Blocks """ 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Building two blocks") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 3 23 | blocks = Collage(pos) 24 | blocks.width = 2 25 | blocks.height = 1 26 | blocks.length = 2 27 | blocks.create() 28 | blocks.render() 29 | 30 | pos.x += 6 31 | blocks = Collage(pos) 32 | blocks.width = 2 33 | blocks.height = 1 34 | blocks.length = 2 35 | blocks.create() 36 | blocks.flip_x() 37 | blocks.render() 38 | 39 | 40 | if __name__ == "__main__": 41 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 42 | unittest.main(warnings='ignore') 43 | -------------------------------------------------------------------------------- /tests/integration/test_bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.bridge import Bridge 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestBridge(TestBaseThing): 17 | """Test Bridge Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building bridges") 21 | 22 | pos = self.pos 23 | 24 | bridge = Bridge(pos) 25 | bridge.large = 10 26 | bridge.height = 3 27 | bridge.width = 3 28 | bridge.block = mcpi.block.WOOD 29 | bridge.build() 30 | 31 | pos = bridge.end_position 32 | pos.x += 1 33 | pos.y = bridge.position.y 34 | bridge1 = Bridge(pos) 35 | bridge1.large = 10 36 | bridge1.block = mcpi.block.BEDROCK 37 | bridge1.build() 38 | 39 | 40 | if __name__ == "__main__": 41 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 42 | unittest.main(warnings='ignore') 43 | -------------------------------------------------------------------------------- /tests/integration/test_town.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | from mcpi.vec3 import Vec3 11 | from mcthings.decorators.light_decorator import LightDecorator 12 | 13 | from mcthings.town import Town 14 | from mcthings.world import World 15 | from integration.base import TestBaseThing 16 | 17 | 18 | class TestTown(TestBaseThing): 19 | """Test Town Thing""" 20 | 21 | def test_build(self): 22 | World.renderer.post_to_chat("Building a town") 23 | 24 | pos = self.pos 25 | 26 | pos.x += 1 27 | 28 | town = Town(pos) 29 | town.block = mcpi.block.BEDROCK 30 | town.build() 31 | 32 | town = Town(Vec3(pos.x-5, pos.y, pos.z)) 33 | town.block = mcpi.block.BEDROCK 34 | town.house_mirror = True 35 | town.build() 36 | 37 | town.add_decorator(LightDecorator) 38 | town.decorate() 39 | 40 | 41 | if __name__ == "__main__": 42 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 43 | unittest.main(warnings='ignore') 44 | -------------------------------------------------------------------------------- /tests/integration/test_line.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.line import Line 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestLine(TestBaseThing): 17 | """Test Line Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building a line") 21 | 22 | pos = self.pos 23 | 24 | pos.x += 1 25 | line = Line(pos) 26 | line.block = mcpi.block.SAND 27 | line.length = 2 28 | line.width = -1 29 | line.build() 30 | 31 | line = Line(line.end_position) 32 | line.width = 2 33 | line.block = mcpi.block.STONE 34 | line.build() 35 | 36 | line = Line(line.end_position) 37 | line.length = 10 38 | line.width = 2 39 | line.block = mcpi.block.SAND 40 | line.build() 41 | 42 | 43 | if __name__ == "__main__": 44 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 45 | unittest.main(warnings='ignore') 46 | -------------------------------------------------------------------------------- /tests/integration/test_sphere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.sphere import Sphere 12 | from mcthings.sphere import SphereHollow 13 | from mcthings.world import World 14 | from integration.base import TestBaseThing 15 | 16 | 17 | class TestSphere(TestBaseThing): 18 | """Test Sphere Thing""" 19 | 20 | def test_build(self): 21 | World.renderer.post_to_chat("Building a sphere") 22 | 23 | pos = self.pos 24 | 25 | radius = 10 26 | pos.z += 20 27 | 28 | sphere = Sphere(pos) 29 | sphere.radius = radius 30 | sphere.block = mcpi.block.IRON_BLOCK 31 | sphere.build() 32 | 33 | World.renderer.post_to_chat("Building a hollow sphere") 34 | pos.x += 20 35 | sphere = SphereHollow(pos) 36 | sphere.radius = radius 37 | sphere.block = mcpi.block.WOOD 38 | sphere.build() 39 | 40 | 41 | if __name__ == "__main__": 42 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 43 | unittest.main(warnings='ignore') 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to McThings 2 | 3 | We'd love for you to contribute to our source code and to make McThings better! 4 | Here are the guidelines we'd like you to follow 5 | 6 | ## Code of Conduct 7 | 8 | Help us keep McThings open and inclusive. Please read and follow our [Code of Conduct][coc]. 9 | 10 | ## Questions, Bugs, Features 11 | 12 | ### Got a Question or Problem? 13 | 14 | McThings is yet a small project so just open a [new issue][github-new-issue] for sending your question. 15 | 16 | ### Do you want to contribute a **Scene** or **Thing**? 17 | 18 | The preferred way is to send it using a pull request. If you don't know howto create 19 | a pull request open a [new issue][github-new-issue] with your code. The license of your 20 | code must be ASL or compatible with it. 21 | 22 | ### Found an Issue or Bug? 23 | 24 | If you find a bug in the source code, you can help us by submitting [new issue][github-new-issue]. 25 | 26 | [coc]: https://github\.com/juntosdesdecasa/mcthings/blob/master/CODE_OF_CONDUCT.md 27 | [github-issues]: https://github\.com/juntosdesdecasa/mcthings/issues 28 | [github-new-issue]: https://github\.com/juntosdesdecasa/mcthings/issues/new 29 | [github]: https://github\.com/juntosdesdecasa/mcthings 30 | -------------------------------------------------------------------------------- /tests/integration/test_platform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi.block 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.block import Block 13 | from mcthings.platform import Platform 14 | from mcthings.world import World 15 | from integration.base import TestBaseThing 16 | 17 | 18 | class TestPlatform(TestBaseThing): 19 | """Test Platform Thing""" 20 | 21 | def test_build(self): 22 | World.renderer.post_to_chat("Building a platform") 23 | 24 | self.pos.z += 1 25 | platform = Platform(self.pos) 26 | platform.top_size = 7 27 | platform.height = 20 28 | platform.build() 29 | 30 | block = Block(platform.end_position) 31 | block.block = mcpi.block.BEDROCK 32 | block.build() 33 | 34 | p = Vec3(platform.end_position.x, platform.end_position.y + 1, platform.end_position.z) 35 | World.renderer.server.mc.entity.setTilePos( 36 | World.renderer.server.mc.getPlayerEntityId(self.BUILDER_NAME), 37 | p) 38 | 39 | 40 | if __name__ == "__main__": 41 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 42 | unittest.main(warnings='ignore') 43 | -------------------------------------------------------------------------------- /mcthings/town.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | 6 | from .thing import Thing 7 | from .house import House 8 | 9 | 10 | class Town(Thing): 11 | 12 | houses = 4 13 | house_width = 5 14 | house_length = 5 15 | house_height = 3 16 | house_mirror = False 17 | space = 3 18 | """space between the town houses""" 19 | 20 | def create(self): 21 | 22 | init_x = self.position.x 23 | init_y = self.position.y 24 | init_z = self.position.z 25 | 26 | house_pos = mcpi.vec3.Vec3(init_x, init_y, init_z) 27 | 28 | for i in range(0, self.houses): 29 | house = House(house_pos, self) 30 | self.add_child(house) 31 | house.width = self.house_width 32 | house.length = self.house_length 33 | house.height = self.house_height 34 | house.block = self.block 35 | house.mirror = self.house_mirror 36 | house.create() 37 | house_pos.z += self.house_width + self.space 38 | 39 | # Fill the end_position 40 | end_x = init_x + self.house_length 41 | end_y = init_y + self.house_height 42 | end_z = house_pos.z - self.space 43 | 44 | self._end_position = mcpi.vec3.Vec3(end_x, end_y, end_z) 45 | -------------------------------------------------------------------------------- /mcthings/bridge.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | from math import floor 5 | 6 | from mcpi.vec3 import Vec3 7 | 8 | from .thing import Thing 9 | 10 | 11 | class Bridge(Thing): 12 | large = 5 13 | height = None 14 | width = 1 15 | 16 | def create(self): 17 | for z in range(0, self.width): 18 | self.build_row(z) 19 | 20 | self._end_position = Vec3(self.position.x + self.large - 1, 21 | self.position.y + self.height, 22 | self.position.z + self.width - 1 23 | ) 24 | 25 | def build_row(self, z): 26 | # large = 2 * height - 1 27 | max_height = floor((self.large + 1) / 2) 28 | if self.height is None: 29 | self.height = max_height 30 | 31 | for x in range(0, self.large): 32 | if x < max_height: 33 | y = x 34 | elif x == max_height and self.large % 2 == 0: 35 | y = max_height - 1 36 | else: 37 | y = y - 1 38 | 39 | final_y = y 40 | 41 | if self.height and y >= self.height - 1: 42 | final_y = self.height - 1 43 | 44 | self.set_block( 45 | Vec3(self.position.x + x, self.position.y + final_y, self.position.z + z), 46 | self.block.id) 47 | -------------------------------------------------------------------------------- /mcthings/sphere.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | from mcpi.vec3 import Vec3 4 | 5 | from mcthings.world import World 6 | from mcthings.thing import Thing 7 | 8 | 9 | class Sphere(Thing): 10 | 11 | radius = 5 12 | """ radius of the Sphere """ 13 | 14 | def build(self): 15 | World.renderer.server.drawing.drawSphere( 16 | self.position.x + self.radius, 17 | self.position.y + self.radius - 1, 18 | self.position.z + self.radius, 19 | self.radius, 20 | self.block) 21 | 22 | end_x = self.position.x + 2 * self.radius 23 | end_y = self.position.y + 2 * self.radius 24 | end_z = self.position.z + 2 * self.radius 25 | 26 | self._end_position = Vec3(end_x, end_y, end_z) 27 | 28 | 29 | class SphereHollow(Thing): 30 | 31 | radius = None 32 | """ radius of the Hollow Sphere """ 33 | height = 0 34 | 35 | def build(self): 36 | World.renderer.server.drawing.drawHollowSphere( 37 | self.position.x + self.radius, 38 | self.position.y + self.radius - 1, 39 | self.position.z + self.radius, 40 | self.radius, 41 | self.block) 42 | 43 | end_x = self.position.x + 2 * self.radius 44 | end_y = self.position.y + 2 * self.radius 45 | end_z = self.position.z + 2 * self.radius 46 | 47 | self._end_position = Vec3(end_x, end_y, end_z) 48 | -------------------------------------------------------------------------------- /tests/integration/test_fence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | import mcpi.block 11 | 12 | from mcthings.fence import Fence 13 | from mcthings.pyramid import Pyramid 14 | from mcthings.town import Town 15 | from mcthings.world import World 16 | from integration.base import TestBaseThing 17 | 18 | 19 | class TestFence(TestBaseThing): 20 | """Test Fence Thing""" 21 | 22 | def test_build(self): 23 | World.renderer.post_to_chat("Building a walled town") 24 | 25 | pos = self.pos 26 | 27 | pos.x += 10 28 | 29 | town = Town(pos) 30 | town.houses = 3 31 | town.block = mcpi.block.WOOD 32 | town.house_width = 10 33 | town.house_length = 10 34 | town.house_height = 10 35 | town.build() 36 | 37 | # Build the wall to round the town 38 | fence = Fence(None) 39 | fence.block = mcpi.block.GOLD_BLOCK 40 | fence.thing = town 41 | fence.thick = 4 42 | fence.height = 5 43 | fence.build() 44 | 45 | pos.x += 30 46 | pyr = Pyramid(pos) 47 | pyr.build() 48 | fence = Fence(None) 49 | fence.thing = pyr 50 | fence.build() 51 | 52 | 53 | if __name__ == "__main__": 54 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 55 | unittest.main(warnings='ignore') 56 | -------------------------------------------------------------------------------- /tests/integration/test_rotate_schematic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.schematic import Schematic 10 | from mcthings.world import World 11 | from integration.base import TestBaseThing 12 | 13 | 14 | class TestRotateSchematic(TestBaseThing): 15 | """Test to rotate an Schematic """ 16 | 17 | def test_build(self): 18 | World.renderer.post_to_chat("Loading and building a schematic") 19 | 20 | pos = self.pos 21 | 22 | pos.x += 3 23 | boat = Schematic(pos) 24 | boat.file_path = "schematics/pirate-boat.schematic" 25 | boat.build() 26 | 27 | rot_boat = Schematic(pos) 28 | rot_boat.file_path = "schematics/pirate-boat.schematic" 29 | rot_boat.create() 30 | rot_boat.rotate(90) 31 | rot_boat.render() 32 | 33 | rot_boat = Schematic(pos) 34 | rot_boat.file_path = "schematics/pirate-boat.schematic" 35 | rot_boat.create() 36 | rot_boat.rotate(180) 37 | rot_boat.render() 38 | 39 | rot_boat = Schematic(pos) 40 | rot_boat.file_path = "schematics/pirate-boat.schematic" 41 | rot_boat.create() 42 | rot_boat.rotate(270) 43 | rot_boat.render() 44 | 45 | 46 | if __name__ == "__main__": 47 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 48 | unittest.main(warnings='ignore') 49 | -------------------------------------------------------------------------------- /tests/integration/test_pyramid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.pyramid import Pyramid, PyramidHollow 13 | from mcthings.world import World 14 | from integration.base import TestBaseThing 15 | 16 | FLAT_WORLD_GROUND_HEIGHT = 0 17 | 18 | 19 | class TestPyramid(TestBaseThing): 20 | """Test Pyramid Thing""" 21 | 22 | def test_build(self): 23 | World.renderer.post_to_chat("Building a pyramid") 24 | 25 | pos = self.pos 26 | 27 | pyramid = Pyramid(pos) 28 | pyramid.height = 5 29 | pyramid.block = mcpi.block.SAND 30 | pyramid.build() 31 | 32 | pyramid = Pyramid(pyramid.end_position) 33 | pyramid.block = mcpi.block.BEDROCK 34 | pyramid.height = 3 35 | pyramid.build() 36 | # Let's move the last pyramid to the ground 37 | pyramid.move(Vec3(pyramid.position.x, FLAT_WORLD_GROUND_HEIGHT, 38 | pyramid.position.z)) 39 | 40 | pyramid = PyramidHollow(Vec3(pos.x + 20, pos.y, pos.z)) 41 | pyramid.block = mcpi.block.WOOD 42 | pyramid.build() 43 | 44 | pyramid.to_schematic("schematics/pyramid_hollow.schematic") 45 | 46 | 47 | if __name__ == "__main__": 48 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 49 | unittest.main(warnings='ignore') 50 | -------------------------------------------------------------------------------- /mcthings/world.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | # TODO: at some point this must be a real Singleton 5 | 6 | 7 | class World: 8 | """ 9 | A world is a container for all the scenes built using McThings. Its mapping 10 | is direct to Minecraft world concept. 11 | 12 | Before adding Things to the World, it must have a renderer 13 | """ 14 | scenes = [] 15 | """ Scenes included in the world """ 16 | renderer = None 17 | """ Render used to render the scenes """ 18 | 19 | @classmethod 20 | def set_renderer(cls, renderer): 21 | cls.renderer = renderer 22 | 23 | # TODO: Hack for Minecraft renderer to use McDrawing 24 | from mcthings.renderers.raspberry_pi import RaspberryPi 25 | if isinstance(renderer, RaspberryPi): 26 | World.drawing = renderer.drawing 27 | 28 | @classmethod 29 | def add_scene(cls, scene): 30 | """ Add a new scene to the world """ 31 | cls.scenes.append(scene) 32 | 33 | @classmethod 34 | def first_scene(cls): 35 | """ Return the first scene used be default """ 36 | return cls.scenes[0] 37 | 38 | @classmethod 39 | def build(cls): 40 | """ Build all the scenes inside the world """ 41 | for scene in cls.scenes: 42 | scene.build() 43 | 44 | @classmethod 45 | def unbuild(cls): 46 | """ Unbuild all the scenes inside the world """ 47 | for scene in cls.scene: 48 | scene.unbuild() 49 | -------------------------------------------------------------------------------- /tests/integration/test_border_decorator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | import mcpi.block 11 | 12 | from mcthings.block import Block 13 | from mcthings.decorators.border_decorator import BorderDecorator 14 | from mcthings.house import House 15 | from mcthings.pyramid import Pyramid 16 | from mcthings.river import River 17 | from mcthings.world import World 18 | from integration.base import TestBaseThing 19 | 20 | 21 | class TestBorderDecorator(TestBaseThing): 22 | """ Test Border Decorator """ 23 | 24 | def test_build(self): 25 | World.renderer.post_to_chat("Building a block with a border") 26 | 27 | pos = self.pos 28 | pos.x += 2 29 | 30 | # Add a border around an scene 31 | Block(pos) 32 | # Block(Vec3(pos.x+1, pos.y, pos.z)) 33 | p = Pyramid(pos) 34 | p.height = 5 35 | House(pos) 36 | river = River(pos) 37 | river.length = 5 38 | init_scene = World.first_scene() 39 | init_scene.build() 40 | 41 | # Add a Railway around the scene 42 | border = BorderDecorator 43 | border.block = mcpi.block.RAIL 44 | border.margin = 5 45 | init_scene.add_decorator(border) 46 | init_scene.decorate() 47 | 48 | 49 | if __name__ == "__main__": 50 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 51 | unittest.main(warnings='ignore') 52 | -------------------------------------------------------------------------------- /tests/integration/test_blocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi.block 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.block import Block 13 | from mcthings.blocks import Blocks 14 | from mcthings.world import World 15 | from integration.base import TestBaseThing 16 | 17 | 18 | class Ve3(object): 19 | pass 20 | 21 | 22 | class TestBlocks(TestBaseThing): 23 | """Test Blocks Thing""" 24 | 25 | def test_build(self): 26 | World.renderer.post_to_chat("Building blocks") 27 | 28 | self.pos.x += 1 29 | blocks = Blocks(self.pos) 30 | blocks.width = 2 31 | blocks.height = 4 32 | blocks.length = 3 33 | blocks.build() 34 | assert len(blocks._blocks_memory.blocks) == blocks.width * blocks.height * blocks.length 35 | 36 | # check the first and last block 37 | init_block = Block(blocks.position) 38 | init_block.block = mcpi.block.GOLD_BLOCK 39 | init_block.build() 40 | end_block = Block(blocks.end_position) 41 | end_block.block = mcpi.block.GOLD_BLOCK 42 | end_block.build() 43 | 44 | self.pos.z += 10 45 | blocks = Blocks(self.pos) 46 | blocks.build() 47 | 48 | blocks.move(Vec3(self.pos.x+5, self.pos.y, self.pos.z)) 49 | 50 | 51 | if __name__ == "__main__": 52 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 53 | unittest.main(warnings='ignore') 54 | -------------------------------------------------------------------------------- /mcthings/decorators/decorator.py: -------------------------------------------------------------------------------- 1 | # TODO: at some point this must be a real Singleton 2 | 3 | from mcpi.minecraft import Minecraft 4 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 5 | # Author (©): Alvaro del Castillo 6 | 7 | import mcpi.block 8 | 9 | from mcthings.blocks_memory import BlocksMemory 10 | from mcthings.world import World 11 | 12 | 13 | class Decorator: 14 | """ 15 | A Decorator is able to decorate a Thing based on its characteristics. 16 | 17 | If a Thing has decorators, they will be called after the build of the Thing. 18 | """ 19 | 20 | block = mcpi.block.AIR 21 | """ Base block for the decorator """ 22 | 23 | def __init__(self, thing): 24 | self._blocks_memory = BlocksMemory() 25 | self._thing = thing 26 | 27 | def create(self): 28 | """ 29 | Create the decorator 30 | 31 | :return: 32 | """ 33 | 34 | def set_block(self, pos, block, data=None): 35 | self._blocks_memory.set_block(pos, block, data) 36 | 37 | def set_blocks(self, init_pos, end_pos, block): 38 | """ Add a cuboid with the same block for all blocks and without specific data""" 39 | self._blocks_memory.set_blocks(init_pos, end_pos, block) 40 | 41 | def render(self): 42 | """ 43 | Renders the decorator 44 | 45 | :return: 46 | """ 47 | World.renderer.render(self._blocks_memory) 48 | 49 | def decorate(self): 50 | """ 51 | Decorate the thing 52 | 53 | :return: 54 | """ 55 | 56 | self.create() 57 | self.render() 58 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # McThings Usage 2 | 3 | In order to use McThings you need Minecraft or Minetest running with Python support 4 | ([Raspberry PI Minecraft](https://www.minecraft.net/en-us/edition/pi/)). Once you have 5 | it you can start executing Python code using McThings to build the scenes in Minecraft/Minetest. 6 | 7 | Jupyter Notebooks are great to start doing it, and the [scenes repository](scenes) uses this option. 8 | 9 | But you can use also other Python environments like PyCharm. 10 | 11 | [This is a research](https://github.com/juntosdesdecasa/mcthings/issues/50) of the current options to use Python in Minecraft and Minetest. 12 | 13 | The options tested are: 14 | 15 | ## Minecraft Spigot server 16 | 17 | This option works with the latest release of Minecraft at this moment: 1.15.2. Install in the Spigot server 18 | [this plugin](https://www.spigotmc.org/resources/raspberryjuice.22724/) and that's all. 19 | 20 | ## Minecraft Forge Client 21 | 22 | In this case you don't need to run a server. Just start the Minecraft Forge client with 23 | [this plugin](https://github.com/arpruss/raspberryjammod) installed and in Single Player mode. 24 | Tested with the version **1.12.2**. The plugin does not work with newer versions. 25 | [Install process](https://github.com/juntosdesdecasa/mcthings/issues/65) 26 | 27 | Probably the Forge server option will work also. 28 | 29 | ## Minetest 30 | 31 | For using Minetest, just install [this plugin](https://github.com/arpruss/raspberryjammod-minetest). 32 | Minetest 5.1 has been tested but probably it will work with newer versions. [Install process](https://github.com/juntosdesdecasa/mcthings/issues/45) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /mcthings/pyramid.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi.block 5 | from mcpi.vec3 import Vec3 6 | 7 | from .thing import Thing 8 | 9 | 10 | class Pyramid(Thing): 11 | height = 10 12 | 13 | def create(self): 14 | length = 2 * self.height - 1 15 | width = length 16 | 17 | for i in range(0, self.height): 18 | level = i 19 | p = self.position 20 | self.set_blocks( 21 | Vec3(p.x + level, p.y + level, p.z + level), 22 | Vec3(p.x + (length - 1) - level, 23 | p.y + level, 24 | p.z + (width - 1) - level), 25 | self.block.id) 26 | 27 | self._end_position = Vec3(p.x + (length - 1), 28 | p.y + self.height - 1, 29 | p.z + (width - 1) 30 | ) 31 | 32 | 33 | class PyramidHollow(Thing): 34 | height = 10 35 | thick = 2 36 | 37 | def create(self): 38 | outer = Pyramid(self.position, self) 39 | outer.height = self.height 40 | outer.block = self.block 41 | self.add_child(outer) 42 | outer.create() 43 | self._end_position = outer.end_position 44 | inner_x = self.position.x + self.thick 45 | inner_y = self.position.y 46 | inner_z = self.position.z + self.thick 47 | inner = Pyramid(Vec3(inner_x, inner_y, inner_z), self) 48 | inner.block = mcpi.block.AIR 49 | inner.height = self.height - self.thick 50 | self.add_child(inner) 51 | inner.create() 52 | -------------------------------------------------------------------------------- /mcthings/renderers/renderer.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | 5 | class Renderer: 6 | """ Base class for all McThings Renderers """ 7 | 8 | """ 9 | Base class for all McThings Renderers 10 | 11 | A renderer reads the blocks data from mcthings.core.BlocksMemory and render it 12 | with a specific engine. For example, Raspberry PI uses the Python API to do it. 13 | """ 14 | 15 | def render(self, blocks_memory): 16 | """ 17 | Render the blocks included in the memory_chunk at position in the world 18 | 19 | :param blocks_memory: memory with the blocks to be rendered 20 | :return: 21 | """ 22 | 23 | def post_to_chat(self, message): 24 | """ 25 | Send a message to the chat in the renderer it it exists 26 | :param message: 27 | :return: 28 | """ 29 | 30 | def get_block(self, position): 31 | """ 32 | Get the rendered block at the given position 33 | :param position: 34 | :return: int with the block id 35 | """ 36 | 37 | def get_block_with_data(self, position): 38 | """ 39 | Get the rendered block at the given position 40 | :param position: 41 | :return: mcpi.block.Block with the id and data 42 | """ 43 | 44 | def get_blocks(self, init_pos, end_pos): 45 | """ 46 | Get the rendered cuboid at init_pos and end_pos 47 | :param init_pos: 48 | :param end_pos: 49 | :return: 50 | """ 51 | 52 | def get_pos(self, entity): 53 | """ 54 | Get the position of the entity in the World 55 | :param entity: 56 | :return: the position in Vec3 format 57 | """ -------------------------------------------------------------------------------- /tests/integration/test_rotate_blocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcthings.blocks import Blocks 10 | from mcthings.collage import Collage 11 | from mcthings.world import World 12 | from integration.base import TestBaseThing 13 | 14 | 15 | class TestRotateBlock(TestBaseThing): 16 | """ Test to rotate Blocks """ 17 | 18 | def test_build(self): 19 | World.renderer.post_to_chat("Rotating cuboids") 20 | 21 | pos = self.pos 22 | 23 | pos.x += 3 24 | blocks = Collage(pos) 25 | blocks.width = 7 26 | blocks.height = 2 27 | blocks.length = 3 28 | blocks.build() 29 | 30 | pos.x += 15 31 | blocks = Collage(pos) 32 | blocks.width = 7 33 | blocks.height = 2 34 | blocks.length = 5 35 | blocks.create() 36 | blocks.rotate(90) 37 | blocks.render() 38 | 39 | # Check that the blocks start and end point are correct 40 | init_blocks = Blocks(blocks.position) 41 | init_blocks.height = 5 42 | init_blocks.width = 1 43 | init_blocks.length = 1 44 | init_blocks.build() 45 | end_blocks = Blocks(blocks.end_position) 46 | end_blocks.height = 5 47 | end_blocks.width = 1 48 | end_blocks.length = 1 49 | end_blocks.build() 50 | 51 | try: 52 | blocks.rotate(45) 53 | except RuntimeError: 54 | logging.info("Detected right Exception for invalid %s degrees", str(45)) 55 | 56 | 57 | if __name__ == "__main__": 58 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 59 | unittest.main(warnings='ignore') 60 | -------------------------------------------------------------------------------- /tests/integration/test_schematic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi.block 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.blocks import Blocks 13 | from mcthings.schematic import Schematic 14 | from mcthings.world import World 15 | from integration.base import TestBaseThing 16 | 17 | 18 | class TestSchematic(TestBaseThing): 19 | """Test Schematic Thing""" 20 | 21 | def test_build(self): 22 | World.renderer.post_to_chat("Create and render schematics") 23 | pos = self.pos 24 | 25 | schematic = Schematic(pos) 26 | # 2012: https://www.minecraft-schematics.com/schematic/68/ 27 | schematic.file_path = "schematics/pirate-boat.schematic" 28 | # 2017: https://www.minecraft-schematics.com/schematic/9676/ 29 | # schematic.file_path = "schematics/chateau-fairmont.schematic" 30 | # schematic.file_path = "schematics/pyramid_hollow.schematic" 31 | schematic.change_blocks = {mcpi.block.ICE.id: mcpi.block.GLASS.id} 32 | schematic.file_path = "schematics/vxs.schematic" 33 | schematic.create() 34 | schematic.render() 35 | 36 | schematic = Schematic(Vec3(pos.x+4, pos.y, pos.z)) 37 | schematic.file_path = "schematics/veh_ambulance_mc.schematic" 38 | schematic.build() 39 | 40 | schematic = Schematic(Vec3(pos.x-4, pos.y, pos.z)) 41 | schematic.file_path = "schematics/alien_engi1a.schematic" 42 | schematic.create() 43 | schematic.flip_x() 44 | schematic.render() 45 | 46 | 47 | if __name__ == "__main__": 48 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 49 | unittest.main(warnings='ignore') 50 | -------------------------------------------------------------------------------- /mcthings/house.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | 6 | from mcpi.vec3 import Vec3 7 | 8 | from .thing import Thing 9 | 10 | 11 | class House(Thing): 12 | 13 | height = 3 14 | width = 5 15 | length = 5 16 | wall_width = 1 17 | door_size = 1 18 | mirror = False 19 | 20 | def create(self): 21 | 22 | init_x = self.position.x 23 | init_y = self.position.y 24 | init_z = self.position.z 25 | 26 | end_x = init_x + self.length - 1 27 | if self.mirror: 28 | end_x = init_x 29 | init_x = init_x - (self.length - 1) 30 | end_y = init_y + self.height - 1 31 | end_z = init_z + self.width - 1 32 | 33 | self.set_blocks( 34 | Vec3(init_x, init_y, init_z), 35 | Vec3(end_x, end_y, end_z), 36 | self.block.id) 37 | 38 | # Fill the cube with air so it becomes a kind of house 39 | init_x_empty = init_x + self.wall_width 40 | end_x_empty = end_x - self.wall_width 41 | self.set_blocks( 42 | Vec3(init_x_empty, init_y, init_z + self.wall_width), 43 | Vec3(end_x_empty, 44 | end_y - self.wall_width, 45 | end_z - self.wall_width), 46 | mcpi.block.AIR.id) 47 | 48 | init_door_x = init_x 49 | if self.mirror: 50 | init_door_x = end_x 51 | init_door = Vec3(init_door_x, init_y, init_z + self.wall_width) 52 | end_door_x = init_door_x + self.wall_width - 1 53 | end_door_y = init_y + self.door_size 54 | end_door_z = init_z + self.wall_width + self.door_size - 1 55 | end_door = Vec3(end_door_x, end_door_y, end_door_z) 56 | self.set_blocks(init_door, end_door, mcpi.block.AIR.id) 57 | 58 | self._end_position = Vec3(end_x, end_y, end_z) 59 | -------------------------------------------------------------------------------- /mcthings/fence.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi 5 | from mcpi.vec3 import Vec3 6 | 7 | from .thing import Thing 8 | 9 | 10 | class Fence(Thing): 11 | """ 12 | Build a block plane and empty it to create the fence 13 | """ 14 | 15 | fence_space = 5 16 | """ Space between the fence and the thing fenced """ 17 | thick = 1 18 | height = None 19 | thing = None 20 | 21 | def create(self): 22 | """ 23 | Create a fence around the configured thing 24 | :return: 25 | """ 26 | if self.thing is None: 27 | raise RuntimeError("Thing to be fenced is not defined") 28 | 29 | self.add_child(self.thing) 30 | 31 | init_x = self.thing.position.x - self.fence_space - self.thick 32 | init_y = self.thing.position.y 33 | init_z = self.thing.position.z - self.fence_space - self.thick 34 | 35 | self._position = Vec3(init_x, init_y, init_z) 36 | 37 | end_x = self.thing.end_position.x + self.fence_space + self.thick 38 | end_y = self.thing.end_position.y 39 | if self.height: 40 | end_y = self.thing.position.y + (self.height - 1) 41 | end_z = self.thing.end_position.z + self.fence_space + self.thick 42 | 43 | self._end_position = Vec3(end_x, end_y, end_z) 44 | 45 | self.set_blocks( 46 | Vec3(init_x, init_y, init_z), 47 | Vec3(end_x, end_y, end_z), 48 | self.block.id) 49 | 50 | # Fill the prism with air to became a rectangular wall 51 | self.set_blocks( 52 | Vec3(init_x + self.thick, init_y, init_z + self.thick), 53 | Vec3(end_x - self.thick, end_y, end_z - self.thick), 54 | mcpi.block.AIR.id) 55 | 56 | # Rebuild the thing because it is destroyed when emptying the fence 57 | # if we are not removing the fence 58 | if self.block.id != mcpi.block.AIR.id: 59 | self.thing.create() 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import codecs 7 | import os 8 | import re 9 | from setuptools import setup 10 | 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | readme_md = os.path.join(here, 'README.md') 13 | 14 | version_py = os.path.join(here, 'mcthings', '_version.py') 15 | with codecs.open(version_py, 'r', encoding='utf-8') as fd: 16 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 17 | fd.read(), re.MULTILINE).group(1) 18 | 19 | # Get the package description from the README.md file 20 | with codecs.open(readme_md, encoding='utf-8') as f: 21 | long_description = f.read() 22 | 23 | # allow setup.py to be run from any path 24 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 25 | 26 | setup( 27 | name='mcthings', 28 | version=version, 29 | packages=['mcthings', 'mcthings.decorators', 'mcthings.renderers'], 30 | include_package_data=True, 31 | license='ASL', 32 | description='McThings is a python library for building things in Minecraft', 33 | long_description=long_description, 34 | long_description_content_type='text/markdown', 35 | url='https://github.com/juntosdesdecasa/mcthings', 36 | author='Alvaro del Castillo', 37 | author_email='alvaro.delcastillo@gmail.com', 38 | keywords="development library minecraft buildings games", 39 | classifiers=[ 40 | 'Environment :: Console', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: Apache Software License', 43 | 'Operating System :: OS Independent', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 3', 46 | 'Topic :: Software Development' 47 | ], 48 | install_requires=[ 49 | 'mcpi', 50 | 'minecraftstuff', 51 | 'nbt' 52 | ], 53 | scripts=[ 54 | 'bin/vox2schematic' 55 | ], 56 | python_requires='>=3.5' 57 | ) 58 | -------------------------------------------------------------------------------- /bin/vox2schematic: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | 7 | import argparse 8 | import logging 9 | import sys 10 | 11 | from mcpi.vec3 import Vec3 12 | 13 | from mcthings.vox import Vox 14 | 15 | 16 | def parse_args(): 17 | """Parse command line arguments""" 18 | 19 | parser = argparse.ArgumentParser() 20 | 21 | parser.add_argument('voxfile') 22 | parser.add_argument('-o', '--outfile', help='Schematic filename', required=False) 23 | 24 | if len(sys.argv) == 1: 25 | parser.print_help() 26 | sys.exit(1) 27 | 28 | return parser.parse_args() 29 | 30 | 31 | def load_vox_file(vox_file): 32 | # The position should be optional. If not, we have a desing issue for this use case 33 | voxels = Vox(Vec3(0, 0, 0 )) 34 | voxels.file_path = vox_file 35 | # load the voxels in memory 36 | voxels.create() 37 | 38 | return voxels 39 | 40 | 41 | def print_error(str): 42 | sys.stderr.write(str + "\n") 43 | 44 | 45 | def main(): 46 | logging.basicConfig(level=logging.DEBUG, format="[%(asctime)s] - %(message)s") 47 | 48 | args = parse_args() 49 | 50 | vox_file = sys.argv[1] 51 | schematic_file = vox_file.replace(".vox", ".schematic") 52 | 53 | if args.outfile: 54 | schematic_file = args.outfile 55 | 56 | logging.info("Vox input file: %s", vox_file) 57 | logging.info("Schematic output file: %s", schematic_file) 58 | 59 | try: 60 | voxels = load_vox_file(vox_file) 61 | # TODO: Reimplement Thing to schematic to worl always from memory? 62 | voxels._blocks_memory.to_schematic(schematic_file) 63 | except FileNotFoundError as ex: 64 | print_error(str(ex)) 65 | except RuntimeError as ex: 66 | print_error(str(ex)) 67 | 68 | 69 | if __name__ == '__main__': 70 | try: 71 | main() 72 | except KeyboardInterrupt: 73 | s = "\n\nReceived Ctrl-C or other break signal. Exiting.\n" 74 | print_error(s) 75 | sys.exit(0) -------------------------------------------------------------------------------- /tests/integration/test_lines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi 10 | 11 | from mcthings.block import Block 12 | from mcthings.world import World 13 | from integration.base import TestBaseThing 14 | 15 | 16 | class TestLines(TestBaseThing): 17 | """Test Lines Thing""" 18 | 19 | def test_build(self): 20 | World.renderer.post_to_chat("Building lines of blocks") 21 | 22 | pos = self.pos 23 | blocks_number = 5 24 | 25 | # Line in incresing x 26 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 27 | for x in range(0, blocks_number): 28 | block_pos.x += 1 29 | Block(block_pos).build() 30 | 31 | # Line in decresing x 32 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 33 | for x in range(1, blocks_number): 34 | block_pos.x -= 1 35 | Block(block_pos).build() 36 | 37 | # Line in incresing y 38 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 39 | for y in range(1, blocks_number): 40 | block_pos.y += 1 41 | Block(block_pos).build() 42 | 43 | # Line in decresing y 44 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 45 | for y in range(1, blocks_number): 46 | block_pos.y -= 1 47 | Block(block_pos).build() 48 | 49 | # Line in incresing z 50 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 51 | for z in range(1, blocks_number): 52 | block_pos.z += 1 53 | Block(block_pos).build() 54 | 55 | # Line in decresing z 56 | block_pos = mcpi.vec3.Vec3(pos.x+1, pos.y, pos.z) 57 | for z in range(1, blocks_number): 58 | block_pos.z -= 1 59 | Block(block_pos).build() 60 | 61 | 62 | if __name__ == "__main__": 63 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 64 | unittest.main(warnings='ignore') 65 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | from nbt import nbt 11 | 12 | from mcthings.blocks import Blocks 13 | from mcthings.schematic import Schematic 14 | from mcthings.utils import find_min_max_cuboid_vertex, size_region 15 | 16 | 17 | class TestUtils(unittest.TestCase): 18 | """ Test for the utils library """ 19 | 20 | def test_cuboid_vertexes(self): 21 | # Find cuboid vertexes 22 | v1 = Vec3(1, 1, 0) 23 | v2 = Vec3(0, 1, 1) 24 | v_min, v_max = find_min_max_cuboid_vertex(v1, v2) 25 | 26 | assert v_min == Vec3(0, 1, 0) 27 | assert v_max == Vec3(1, 1, 1) 28 | 29 | v1 = Vec3(1, 0, 0) 30 | v2 = Vec3(0, 1, 1) 31 | v_min, v_max = find_min_max_cuboid_vertex(v1, v2) 32 | 33 | assert v_min == Vec3(0, 0, 0) 34 | assert v_max == Vec3(1, 1, 1) 35 | 36 | def test_size_region(self): 37 | # Load a schematic file with a know size and check this method 38 | schematic = Schematic(Vec3(0, 0, 0)) 39 | schematic.file_path = "schematics/alien_engi1a.schematic" 40 | schematic.create() 41 | size = size_region(schematic.position, schematic.end_position) 42 | 43 | data = nbt.NBTFile(schematic.file_path, 'rb') 44 | size_x = data["Width"].value 45 | size_y = data["Height"].value 46 | size_z = data["Length"].value 47 | expected_size = Vec3(size_x, size_y, size_z) 48 | 49 | assert expected_size == size 50 | 51 | # Let's check with blocks also 52 | blocks = Blocks(Vec3(0, 0, 0)) 53 | blocks.length = 5 54 | blocks.width = 5 55 | blocks.height = 5 56 | expected_size = Vec3(5, 5, 5) 57 | blocks.create() 58 | size = size_region(blocks.position, blocks.end_position) 59 | assert expected_size == size 60 | 61 | # build_schematic_nbt, extract_region and extract_region_with_data all need the renderer: not unit testing 62 | 63 | if __name__ == "__main__": 64 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 65 | unittest.main(warnings='ignore') 66 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | # For autodoc to work 16 | sys.path.insert(0, os.path.abspath('..')) 17 | # For API doc to be generated 18 | os.system("sphinx-apidoc -f -d 4 -o . ../mcthings") 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'McThings' 24 | copyright = '2020, Alvaro del Castillo' 25 | author = 'Alvaro del Castillo' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc'] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | # The theme to use for HTML and HTML Help pages. See the documentation for 47 | # a list of builtin themes. 48 | # 49 | # html_theme = 'alabaster' 50 | 51 | # Add any paths that contain custom static files (such as style sheets) here, 52 | # relative to this directory. They are copied after the builtin static files, 53 | # so a file named "default.css" will overwrite the builtin "default.css". 54 | html_static_path = ['_static'] 55 | 56 | # https://github.com/readthedocs/readthedocs.org/issues/2569#issuecomment-485117471 57 | master_doc = 'index' 58 | -------------------------------------------------------------------------------- /mcthings/decorators/border_decorator.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import math 5 | 6 | from mcpi.vec3 import Vec3 7 | 8 | from .decorator import Decorator 9 | 10 | 11 | class BorderDecorator(Decorator): 12 | """ 13 | A Border Decorator to build the border of the Thing. 14 | 15 | Create a border of 1 block around the Thing 16 | """ 17 | 18 | margin = 5 # Margin between the Thing and its border 19 | 20 | def create(self): 21 | """ 22 | Add a border to the Thing 23 | 24 | :return: 25 | """ 26 | 27 | (min_pos, max_pos) = self._thing.find_bounding_box() 28 | # The min height is always the floor of the thing position 29 | if min_pos.y < self._thing.position.y: 30 | min_pos.y = self._thing.position.y 31 | 32 | border_width = (max_pos.x - min_pos.x) + 1 + 2 * self.margin + 2 * 1 33 | border_width = math.ceil(border_width) 34 | border_large = (max_pos.z - min_pos.z) + 1 + 2 * self.margin + 2 * 1 35 | border_large = math.ceil(border_large) 36 | 37 | init_pos = Vec3(min_pos.x - (self.margin + 1), min_pos.y, min_pos.z - (self.margin + 1)) 38 | end_pos = Vec3(min_pos.x + (self.margin + 1), min_pos.y, min_pos.z + (self.margin + 1)) 39 | 40 | # Create the four borders of the Thing 41 | # Block by block with pre-clean so railways work 42 | init = init_pos 43 | end = Vec3(init_pos.x + border_width - 1, init_pos.y, init_pos.z) 44 | for x in range(0, border_width - 1): 45 | self.set_block(Vec3(init.x + x, init.y, init.z), self.block) 46 | 47 | init = end 48 | end = Vec3(init.x, init.y, init.z + border_large - 1) 49 | for z in range(0, border_large - 1): 50 | self.set_block(Vec3(init.x, init.y, init.z + z), self.block) 51 | 52 | init = end 53 | end = Vec3(init.x - (border_width - 1), init.y, init.z) 54 | for x in range(0, border_width - 1): 55 | self.set_block(Vec3(init.x - x, init.y, init.z), self.block) 56 | 57 | init = end 58 | end = Vec3(init.x, init.y, init.z - (border_large - 1)) 59 | for z in range(0, border_large - 1): 60 | self.set_block(Vec3(init.x, init.y, init.z - z), self.block) 61 | -------------------------------------------------------------------------------- /tests/unit/test_vox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | 11 | from mcthings.vox import Vox 12 | 13 | 14 | class TestVox(unittest.TestCase): 15 | """Test Vox Thing""" 16 | 17 | def test_parse_vox_file(self): 18 | 19 | # Old format 20 | vox = Vox(Vec3(0, 0 ,0)) 21 | vox.file_path = "vox/alien_engi1a.vox" 22 | vox.parse_vox_file() 23 | assert len(vox.voxels) == 180 24 | assert len(vox.palette) == 256 25 | 26 | # Old format with default palette 27 | vox1 = Vox(Vec3(0, 0 ,0)) 28 | vox1.file_path = "vox/chr_beardo3-default-palette.vox" 29 | vox1.parse_vox_file() 30 | assert len(vox1.voxels) == 299 31 | assert len(vox1.palette) == 255 # 1 color less than before? bug? 32 | 33 | # New format 34 | vox2 = Vox(Vec3(0, 0 ,0)) 35 | vox2.file_path = "vox/vxs.vox" 36 | vox2.parse_vox_file() 37 | assert len(vox2.voxels) == 3 38 | assert len(vox2.palette) == 256 39 | 40 | def test_voxel_position(self): 41 | vox2 = Vox(Vec3(0, 0 ,0)) 42 | vox2.file_path = "vox/vxs.vox" 43 | vox2.parse_vox_file() 44 | 45 | # initial position of the voxelers logo 46 | # * * 47 | # * 48 | # Base V block 49 | assert vox2.voxels[0].x == 1 50 | assert vox2.voxels[0].y == vox2.voxels[0].z == 0 51 | # Left V block 52 | assert vox2.voxels[1].x == vox2.voxels[1].y == 0 53 | assert vox2.voxels[1].z == 1 54 | # Right V block 55 | assert vox2.voxels[2].x == 2 56 | assert vox2.voxels[2].y == 0 57 | assert vox2.voxels[2].z == 1 58 | 59 | def test_voxel_color(self): 60 | vox2 = Vox(Vec3(0, 0 ,0)) 61 | vox2.file_path = "vox/vxs.vox" 62 | vox2.parse_vox_file() 63 | 64 | # red color: ee0000ff 65 | color = vox2.palette[vox2.voxels[0].color_index] 66 | assert color.hex_str == 'ee0000ff' 67 | assert color.minecraft() == 14 # red 68 | 69 | 70 | if __name__ == "__main__": 71 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 72 | unittest.main(warnings='ignore') 73 | -------------------------------------------------------------------------------- /mcthings/schematic.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author/s (©): Alvaro del Castillo 3 | 4 | import mcpi.block 5 | from nbt import nbt 6 | 7 | from mcpi.vec3 import Vec3 8 | 9 | from mcthings.thing import Thing 10 | 11 | 12 | class Schematic(Thing): 13 | _blocks_field = 'Blocks' 14 | _data_field = 'Data' 15 | file_path = None 16 | """ file path for the schematic file """ 17 | rotate_degrees = 0 18 | """ rotate the schematic """ 19 | change_blocks = {mcpi.block.AIR.id: mcpi.block.AIR.id} 20 | """ Change a block with other """ 21 | 22 | def find_bounding_box(self): 23 | """ In a Schematic the bounding box is inside the file data """ 24 | 25 | schematic = nbt.NBTFile(self.file_path, 'rb') 26 | 27 | size_x = schematic["Width"].value 28 | size_y = schematic["Height"].value 29 | size_z = schematic["Length"].value 30 | 31 | init_x = self.position.x 32 | init_y = self.position.y 33 | init_z = self.position.z 34 | 35 | self._end_position = Vec3(init_x + size_x - 1, 36 | init_y + size_y - 1, 37 | init_z + size_z - 1) 38 | 39 | return self.position, self.end_position 40 | 41 | def create(self): 42 | if not self.file_path: 43 | RuntimeError("Missing file_path param") 44 | 45 | schematic = nbt.NBTFile(self.file_path, 'rb') 46 | size_x = schematic["Width"].value 47 | size_y = schematic["Height"].value 48 | size_z = schematic["Length"].value 49 | 50 | init_pos = self.position 51 | 52 | for y in range(0, size_y): 53 | for z in range(0, size_z): 54 | for x in range(0, size_x): 55 | i = x + size_x * z + (size_x * size_z) * y 56 | block_id = schematic[self._blocks_field][i] 57 | block_data = schematic[self._data_field][i] & 0b00001111 # lower 4 bits 58 | block_pos = Vec3(init_pos.x + x, init_pos.y + y, init_pos.z + z) 59 | if block_id in self.change_blocks: 60 | block_id = self.change_blocks[block_id] 61 | self.set_block(block_pos, block_id, block_data) 62 | 63 | init_pos, self._end_position = self.find_bounding_box() 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | .idea/ 133 | 134 | # Local scripts 135 | sync-scenes 136 | 137 | # Legacy folder 138 | scenes 139 | -------------------------------------------------------------------------------- /tests/integration/test_vox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | import mcpi.block 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.schematic import Schematic 13 | from mcthings.vox import Vox 14 | from mcthings.world import World 15 | from integration.base import TestBaseThing 16 | 17 | 18 | class TestSchematic(TestBaseThing): 19 | """Test Schematic Thing""" 20 | 21 | def test_build(self): 22 | 23 | # New format vox 24 | vox = Vox(Vec3(self.pos.x, self.pos.y, self.pos.z-5)) 25 | vox.file_path = "vox/vxs.vox" 26 | vox.create() 27 | assert len(vox._blocks_memory.blocks) == 3 28 | vox.render() 29 | vox.unbuild() 30 | assert len(vox._blocks_memory.blocks) == 0 31 | vox.build() 32 | assert len(vox._blocks_memory.blocks) == 3 33 | vox.to_schematic("schematics/vxs.schematic", True) 34 | 35 | # Rotate the vox model 36 | vox.unbuild() 37 | vox.create() 38 | vox.rotate(90) 39 | vox.render() 40 | vox.unbuild() 41 | 42 | # Glass sphere with the voxelers logo inside: convert to glass block in Minecraft 43 | vox = Vox(Vec3(self.pos.x, self.pos.y, self.pos.z-20)) 44 | vox.file_path = "vox/vxs_glass_ball.vox" 45 | vox.create() 46 | vox.render() 47 | 48 | # Old format with default palette 49 | vox = Vox(self.pos) 50 | vox.file_path = "vox/chr_beardo3-default-palette.vox" 51 | vox.create() 52 | vox.flip_x() 53 | vox.render() 54 | 55 | # Old format vox 56 | vox = Vox(Vec3(self.pos.x + 10, self.pos.y, self.pos.z)) 57 | vox.file_path = "vox/alien_engi1a.vox" 58 | vox.create() 59 | vox.render() 60 | 61 | # Old format vox converted to new one with MV 62 | vox = Vox(Vec3(self.pos.x + 30, self.pos.y, self.pos.z)) 63 | vox.file_path = "vox/veh_ambulance_mc.vox" 64 | vox.create() 65 | vox.render() 66 | 67 | # Wool colors wall 68 | vox = Vox(Vec3(self.pos.x + 5, self.pos.y, self.pos.z + 10)) 69 | vox.file_path = "vox/minecraft_wool.vox" 70 | vox.create() 71 | vox.flip_x() 72 | vox.render() 73 | 74 | # Glass sphere with the voxelers logo inside: convert to glass block in Minecraft 75 | vox = Vox(Vec3(self.pos.x, self.pos.y, self.pos.z-20)) 76 | vox.file_path = "vox/vxs_glass_ball.vox" 77 | vox.create() 78 | vox.render() 79 | 80 | 81 | if __name__ == "__main__": 82 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 83 | unittest.main(warnings='ignore') 84 | -------------------------------------------------------------------------------- /mcthings/renderers/raspberry_pi.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | import logging 4 | import sys 5 | 6 | import mcpi 7 | from mcpi.minecraft import Minecraft 8 | from minecraftstuff import MinecraftDrawing 9 | 10 | from .renderer import Renderer 11 | from mcthings.blocks_memory import BlocksMemory 12 | 13 | 14 | class _Server: 15 | """ 16 | A Server manages the connection with the Minecraft server. 17 | 18 | Every World must have a Server in which built the World. 19 | """ 20 | 21 | def __init__(self, host="localhost", port="4711"): 22 | self._host = host 23 | self._port = port 24 | 25 | self._mc = Minecraft.create(address=host, port=port) 26 | self._drawing = MinecraftDrawing(self._mc) 27 | 28 | @property 29 | def drawing(self): 30 | """ Connection to MinecraftDrawing (only used in Things built with MinecraftDrawing)""" 31 | return self._drawing 32 | 33 | @property 34 | def mc(self): 35 | """ Connection to Minecraft """ 36 | return self._mc 37 | 38 | 39 | class RaspberryPi(Renderer): 40 | """ 41 | Renderer implemented using the Raspberry Pi Python API 42 | https://www.stuffaboutcode.com/p/minecraft-api-reference.html 43 | 44 | """ 45 | 46 | def __init__(self, host, port): 47 | try: 48 | self.server = _Server(host, port) 49 | except mcpi.connection.RequestError: 50 | logging.error("Can't connect to Minecraft/Minetest server %s:%s" % (host, port)) 51 | sys.exit(1) 52 | 53 | def render_cuboid_memory(self, memory): 54 | """ Render a memory with all blocks equal in a filled cuboid """ 55 | block = memory.blocks[0] 56 | 57 | init_pos, end_pos = memory.find_init_end_pos() 58 | 59 | self.server.mc.setBlocks(init_pos.x, init_pos.y, init_pos.z, 60 | end_pos.x, end_pos.y, end_pos.z, 61 | block.id) 62 | 63 | def render_memory(self, memory): 64 | """ Render memory """ 65 | 66 | for block in memory.blocks: 67 | if block.data is not None: 68 | self.server.mc.setBlock(block.pos.x, block.pos.y, block.pos.z, block.id, block.data) 69 | else: 70 | self.server.mc.setBlock(block.pos.x, block.pos.y, block.pos.z, block.id) 71 | 72 | def render(self, blocks_memory): 73 | if blocks_memory.memory_equal() and blocks_memory.is_cuboid(): 74 | self.render_cuboid_memory(blocks_memory) 75 | else: 76 | self.render_memory(blocks_memory) 77 | 78 | def post_to_chat(self, message): 79 | self.server.mc.postToChat(message) 80 | 81 | def get_block(self, pos): 82 | return self.server.mc.getBlock(pos.x, pos.y, pos.z) 83 | 84 | def get_block_with_data(self, pos): 85 | return self.server.mc.getBlockWithData(pos.x, pos.y, pos.z) 86 | 87 | def get_blocks(self, init_pos, end_pos): 88 | return self.server.mc.getBlocks(init_pos.x, init_pos.y, init_pos.z, end_pos.x, end_pos.y, end_pos.z) 89 | 90 | def get_pos(self, entity): 91 | return self.server.mc.entity.getTilePos( 92 | self.server.mc.getPlayerEntityId(entity)) 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # McThings Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project opening team at alvaro.delcastillo@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # McThings [![Build Status](https://travis-ci.org/Voxelers/mcthings.svg?branch=develop)](https://travis-ci.org/github/Voxelers/mcthings) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/Voxelers/mcthings/blob/develop/LICENSE) [![Documentation Status](https://readthedocs.org/projects/mcthings/badge/?version=latest)](https://mcthings.readthedocs.io/en/latest/?badge=latest) [![PyPI version](https://badge.fury.io/py/mcthings.svg)](https://badge.fury.io/py/mcthings) [![Twitter](docs/img/twitter.png)](https://twitter.com/McthingsP) 2 | 3 | A Python programming framework for building a 3D World of Scenes in Minecraft ([Procedural](https://en.wikipedia.org/wiki/Procedural_modeling) [CSG](https://en.wikipedia.org/wiki/Constructive_solid_geometry)). 4 | Scenes are compositions of Things (Python objects), created and transformed in memory and rendered 5 | using the 6 | [Raspberry PI Minecraft](https://www.minecraft.net/en-us/edition/pi/) [renderer](mcthings/renderers/raspberry_pi.py) implemented using the 7 | [API](https://www.stuffaboutcode.com/p/minecraft-api-reference.html) (which also works in [Minetest](https://github.com/arpruss/raspberryjammod-minetest)). 8 | This renderer is based 9 | on [mcpi library](https://github.com/martinohanlon/mcpi). More renderers are planned. It follows the pipeline: create and transform in memory (model in memory) and then render. 10 | 11 | [This is the reference notebook](https://github.com/juntosdesdecasa/minecraft/blob/develop/server/data/python/scene0_10.ipynb) 12 | with a complete sample. And there is a [intro video tutorial](https://www.youtube.com/watch?v=p6NUFdUbcYk&t=2s) and [a more complete one](https://www.youtube.com/watch?v=teGjAXomBVs&t=4s). 13 | 14 | A Thing is a built based on blocks (voxels based on cubes): [Pyramid](mcthings/pyramid.py), [River](mcthings/river.py), 15 | [House](mcthings/house.py), [Fence](mcthings/fence.py) 16 | and may others. All the Things share the [Thing API](mcthings/thing.py). 17 | A Thing can be [decorated](https://twitter.com/acstw/status/1265510248892239873) 18 | using existing decorators like [LightDecorator](mcthings/decorators/light_decorator.py) 19 | or you can create your own one. A [decorated house](https://github.com/juntosdesdecasa/mcthings_extra/blob/develop/tests/test_entity.py#L40). 20 | Scenes can also be decorated [like this sample](https://twitter.com/acstw/status/1267591965169811456) 21 | with a railway ([BorderDecorator](mcthings/decorators/border_decorator.py)) around a Scene. 22 | 23 | And Things can also be rotated. For example, in this scene [the castle is rotated 24 | 180 degrees](https://github.com/juntosdesdecasa/mcthings_scenes/tree/develop/notebooks/scene0_42.ipynb) so the portal is accessible from the town ways. 25 | 26 | There is also a repository for experimental, incubating or with extra dependencies Things 27 | at [McThings Extra](https://github.com/juntosdesdecasa/mcthings_extra). 28 | 29 | A [World](mcthings/world.py) is a list of Scenes placed in concrete positions. 30 | And a [Scene is a list](mcthings/scene.py) of Things built in a specific position and order. Scenes can be shared 31 | loading and saving them to files. Scenes can be also saved as Schematics 32 | and converted with [Mineways](http://www.realtimerendering.com/erich/minecraft/public/mineways/) 33 | to be used for [3D rendering and printing](https://twitter.com/acstw/status/1262944914234540032). 34 | You [can share scenes adding them 35 | to this repository](https://github.com/juntosdesdecasa/mcthings_scenes). 36 | And they can be [interactive](https://www.youtube.com/watch?v=TjHqt3WO-o0) 37 | as [in this app](https://github.com/juntosdesdecasa/mcthings_scenes/blob/develop/apps/scene_interactive.py). 38 | 39 | [This scene](https://github.com/juntosdesdecasa/mcthings_scenes/tree/develop/notebooks/scene_basic.ipynb) includes 40 | a river, a house in each side of the river and a bridge for crossing the river. 41 | 42 | ![A Scene in Minecraft](https://raw.githubusercontent.com/juntosdesdecasa/mcthings_scenes/develop/notebooks/img/scene_basic.png) 43 | 44 | Things can be built using [MinecraftDrawing](https://minecraft-stuff.readthedocs.io/en/latest/index.html). 45 | [Sphere](mcthings/sphere.py) and [Circle](mcthings/circle.py) Things are used with Pyramids in the next 46 | [scene](https://github.com/juntosdesdecasa/mcthings_scenes/tree/develop/notebooks/scene_sphere_circle_pyramid.ipynb): 47 | 48 | ![Pyramids with Spheres](https://raw.githubusercontent.com/juntosdesdecasa/mcthings_scenes/develop/notebooks/img/scene_sphere_circle_pyramid.png) 49 | 50 | And Things can also be built from [Schematics](https://www.minecraft-schematics.com/) (there are thousands!). 51 | There is a [sample notebook](https://github.com/juntosdesdecasa/mcthings_scenes/tree/develop/notebooks/Schematics.ipynb). 52 | 53 | ![Schematic inside McThings](https://raw.githubusercontent.com/juntosdesdecasa/mcthings_scenes/develop/notebooks/img/schematic.png) 54 | 55 | And Things can also be created from MagicaVoxel models. 56 | 57 | ![MagicaVoxel model inside McThings](docs/img/ambulance_mc.png) 58 | 59 | [Minecraft](https://www.minecraftforum.net/forums/mapping-and-modding-java-edition/minecraft-tools/3020985-mcthings-a-framework-for-creating-scenes-using) 60 | and [Minetest](https://forum.minetest.net/viewtopic.php?t=24719) forums pages. 61 | -------------------------------------------------------------------------------- /tests/unit/test_blocks_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 4 | # Author (©): Alvaro del Castillo 5 | 6 | import logging 7 | import unittest 8 | 9 | from mcpi.vec3 import Vec3 10 | from nbt import nbt 11 | 12 | from mcthings.blocks import Blocks 13 | from mcthings.blocks_memory import BlocksMemory, BlockMemory 14 | from mcthings.collage import Collage 15 | from mcthings.schematic import Schematic 16 | from mcthings.vox import Vox 17 | 18 | 19 | class TestBlocksMemory(unittest.TestCase): 20 | """Test BlocksMemory""" 21 | 22 | def test_create(self): 23 | pass 24 | 25 | # Add a test for all public methods at least 26 | 27 | def test_add_block(self): 28 | mem = BlocksMemory() 29 | mem.add(BlockMemory(0, 0, 0)) 30 | assert len(mem.blocks) == 1 31 | 32 | def test_find_init_end_pos(self): 33 | alien = Schematic(Vec3(0, 0, 0)) 34 | alien.file_path = "schematics/alien_engi1a.schematic" 35 | alien.create() 36 | 37 | init, end = alien.find_bounding_box() 38 | minit, mend = alien._blocks_memory.find_init_end_pos() 39 | 40 | assert init == minit 41 | assert end == mend 42 | 43 | def test_is_cuboid(self): 44 | 45 | blocks = Blocks(Vec3(0, 0,0)) 46 | blocks.create() 47 | assert blocks._blocks_memory.is_cuboid() 48 | 49 | # The schematic from a not cuboid vox is exported as a cuboid 50 | alien = Schematic(Vec3(0, 0, 0)) 51 | alien.file_path = "schematics/alien_engi1a.schematic" 52 | alien.create() 53 | 54 | assert alien._blocks_memory.is_cuboid() 55 | 56 | # The vox model is not a cuboid 57 | alien = Vox(Vec3(0, 0, 0)) 58 | alien.file_path = "vox/alien_engi1a.vox" 59 | alien.create() 60 | 61 | assert not alien._blocks_memory.is_cuboid() 62 | 63 | def test_memory_equal(self): 64 | blocks = Blocks(Vec3(0, 0,0)) 65 | blocks.create() 66 | assert blocks._blocks_memory.memory_equal() 67 | 68 | blocks = Collage(Vec3(0, 0,0)) 69 | blocks.create() 70 | assert not blocks._blocks_memory.memory_equal() 71 | 72 | def test_flip_x(self): 73 | blocks = Blocks(Vec3(20, 0, 0)) 74 | blocks.create() 75 | 76 | init_pos, end_pos = blocks._blocks_memory.find_init_end_pos() 77 | 78 | blocks.flip_x() 79 | finit_pos, fend_pos = blocks._blocks_memory.find_init_end_pos() 80 | 81 | assert finit_pos.x == init_pos.x - (blocks.width - 1) 82 | assert fend_pos.x == end_pos.x - (blocks.width - 1) 83 | 84 | def test_rotate(self): 85 | blocks = Blocks(Vec3(0, 0, 0)) 86 | blocks.width = 2 87 | blocks.length = 3 88 | blocks.height = 2 89 | blocks.create() 90 | 91 | init_pos, end_pos = blocks._blocks_memory.find_init_end_pos() 92 | blocks_size = len(blocks._blocks_memory.blocks) 93 | 94 | """ 95 | (1, 2, 1) 96 | ** 97 | ** 98 | *(*) 99 | (0, 0, 0) 100 | 101 | After 90 degrees rotation based on (*) 102 | (0, 1, 1) 103 | *** 104 | (*)** 105 | (-2 , 0, 0) 106 | """ 107 | 108 | blocks.rotate(90) 109 | rot_init_pos, rot_end_pos = blocks._blocks_memory.find_init_end_pos() 110 | rot_blocks_size = len(blocks._blocks_memory.blocks) 111 | 112 | assert blocks_size == rot_blocks_size 113 | 114 | assert rot_init_pos == Vec3(-2, 0, 0) 115 | assert rot_end_pos == Vec3(0, 1, 1) 116 | 117 | def test_set_block(self): 118 | mem = BlocksMemory() 119 | mem.set_block(Vec3(1, 0, 0), 1, 0) 120 | mem.set_block(Vec3(0, 0, 0), 0, 0) 121 | assert len(mem.blocks) == 2 122 | assert mem.blocks[0].id == 1 123 | 124 | def test_set_blocks(self): 125 | mem = BlocksMemory() 126 | # 3 x 2 x 2 = 12 blocks 127 | blocks_end_position = Vec3(2, 1, 1) 128 | mem.set_blocks(Vec3(0, 0, 0), blocks_end_position, 0) 129 | assert len(mem.blocks) == 12 130 | 131 | init_pos, end_pos = mem.find_init_end_pos() 132 | 133 | assert end_pos == blocks_end_position 134 | assert mem.is_cuboid() 135 | 136 | def test_find_block_at_pos(self): 137 | mem = BlocksMemory() 138 | pos = Vec3(1, 2, 3) 139 | block_id = 40 140 | block_data = 15 141 | mem.set_block(pos, block_id, block_data) 142 | mem.set_block(Vec3(0, 0, 0), 0, 0) 143 | 144 | block = mem.find_block_at_pos(pos) 145 | assert block.id == block_id and block.data == block_data 146 | 147 | def test_memory_to_nbt(self): 148 | # Load a schematic and count the number of blocks in the NBT structure 149 | alien = Schematic(Vec3(0, 0, 0)) 150 | alien.file_path = "schematics/alien_engi1a.schematic" 151 | alien_colors = 5 152 | alien.create() 153 | 154 | data = nbt.NBTFile(alien.file_path, 'rb') 155 | size_x = data["Width"].value 156 | size_y = data["Height"].value 157 | size_z = data["Length"].value 158 | expected_blocks = size_x * size_y * size_z 159 | 160 | blocks_bytes, data_bytes = alien._blocks_memory.to_nbt(alien.position, alien.end_position) 161 | 162 | assert len(blocks_bytes) == expected_blocks 163 | 164 | # Check the data reflects correctly the number of colors 165 | number_wool_colors = len(set(data_bytes)) 166 | 167 | assert number_wool_colors == alien_colors 168 | 169 | 170 | if __name__ == "__main__": 171 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') 172 | unittest.main(warnings='ignore') 173 | -------------------------------------------------------------------------------- /mcthings/scene.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | # TODO: at some point this must be a real Singleton 5 | import pickle 6 | 7 | from mcpi.vec3 import Vec3 8 | 9 | from mcthings.blocks_memory import BlocksMemory 10 | from mcthings.utils import build_schematic_nbt 11 | from mcthings.world import World 12 | 13 | 14 | class Scene: 15 | """ 16 | A scene is a container for all the things built using McThings. 17 | A scene can be built, unbuilt and moved. There is only one scene 18 | in a program using McThings. Things built are added automatically to 19 | the Scene. A Scene can also be loaded from a file, and 20 | it can be saved to a file. 21 | 22 | Before adding Things to the Scene, it must be connected to a 23 | Minecraft server (fill the Scene.server attribute) 24 | """ 25 | 26 | def __init__(self): 27 | self.things = [] 28 | """ map with the things in the scene """ 29 | self._decorators = [] 30 | """ decorators for the scene """ 31 | self._position = None 32 | """ position in the world of the scene """ 33 | self._end_position = None 34 | """ end position in the world of the scene """ 35 | 36 | World.add_scene(self) 37 | 38 | @property 39 | def end_position(self): 40 | """ end position of the thing """ 41 | return self._end_position 42 | 43 | @property 44 | def position(self): 45 | """ initial position of the thing """ 46 | return self._position 47 | 48 | def add(self, thing): 49 | """ Add a new thing to the scene """ 50 | if not self.things: 51 | # The initial position of the scene is the position 52 | # of its first thing added 53 | self._position = thing.position 54 | self.things.append(thing) 55 | 56 | def add_decorator(self, decorator): 57 | """ Add a new decorator to the scene """ 58 | self._decorators.append(decorator) 59 | 60 | def decorate(self): 61 | """ 62 | Call all decorators for the current Scene 63 | 64 | :return: 65 | """ 66 | 67 | for decorator in self._decorators: 68 | decorator(self).decorate() 69 | 70 | def build(self): 71 | """ Build all the things inside the Scene """ 72 | for thing in self.things: 73 | thing.build() 74 | 75 | (min_pos, max_pos) = self.find_bounding_box() 76 | self._end_position = max_pos 77 | 78 | def unbuild(self): 79 | """ Unbuild all the things inside the Scene """ 80 | for thing in self.things: 81 | thing.unbuild() 82 | 83 | def create(self): 84 | """ Create all the things inside the Scene """ 85 | for thing in self.things: 86 | thing.create() 87 | 88 | def reposition(self, position): 89 | """ 90 | Move all the things in the scene to a new relative position 91 | 92 | :param position: new position for the Scene 93 | :return: 94 | """ 95 | 96 | # All the things inside the scene must be moved 97 | diff_x = position.x - self._position.x 98 | diff_y = position.y - self._position.y 99 | diff_z = position.z - self._position.z 100 | 101 | for thing in self.things: 102 | repos_x = thing.position.x + diff_x 103 | repos_y = thing.position.y + diff_y 104 | repos_z = thing.position.z + diff_z 105 | 106 | thing._position = (Vec3(repos_x, repos_y, repos_z)) 107 | 108 | def move(self, position): 109 | """ 110 | Move the scene to a new position 111 | 112 | :param position: new position 113 | :return: 114 | """ 115 | 116 | self.unbuild() 117 | self.reposition(position) 118 | self.build() 119 | 120 | def load(self, file_path): 121 | """ Load a scene from a file (but no build it yet) """ 122 | self.things = pickle.load(open(file_path, "rb")) 123 | if self.things: 124 | self._position = self.things[0].position 125 | 126 | def save(self, file_path): 127 | """ Save a scene to a file """ 128 | 129 | def clean_memory(thing): 130 | for child in thing._children: 131 | clean_memory(child) 132 | thing._blocks_memory = BlocksMemory() 133 | 134 | # Clean the blocks_memory: it is not needed to recreate the scene 135 | for thing in self.things: 136 | clean_memory(thing) 137 | pickle.dump(self.things, open(file_path, "wb")) 138 | 139 | # Reload the memory 140 | self.create() 141 | 142 | def find_bounding_box(self): 143 | """ Compute the bounding box of the Scene """ 144 | 145 | def update_box(box_pos_min, box_pos_max, pos): 146 | # Update box_pos_min and box_pos_max checking pos 147 | box_pos_min.x = pos.x if pos.x < box_pos_min.x else box_pos_min.x 148 | box_pos_min.y = pos.y if pos.y < box_pos_min.y else box_pos_min.y 149 | box_pos_min.z = pos.z if pos.z < box_pos_min.z else box_pos_min.z 150 | box_pos_max.x = pos.x if pos.x > box_pos_max.x else box_pos_max.x 151 | box_pos_max.y = pos.y if pos.y > box_pos_max.y else box_pos_max.y 152 | box_pos_max.z = pos.z if pos.z > box_pos_max.z else box_pos_max.z 153 | 154 | return box_pos_min, box_pos_max 155 | 156 | # Default init values 157 | min_pos = Vec3(self._position.x, self._position.y, self._position.z) 158 | max_pos = Vec3(self._position.x, self._position.y, self._position.z) 159 | 160 | # Find the bounding box for the scene 161 | for thing in self.things: 162 | min_pos, max_pos = update_box(min_pos, max_pos, thing.position) 163 | if thing.end_position: 164 | min_pos, max_pos = update_box(min_pos, max_pos, thing.end_position) 165 | 166 | return min_pos, max_pos 167 | 168 | def to_schematic(self, file_path, block_data=False): 169 | """ 170 | Save the Scene into a Schematic file 171 | 172 | :param file_path: file in which to export the Scene in Schematic format 173 | :param block_data: extract blocks ids and data (much slower) 174 | :return: the Schematic object 175 | """ 176 | 177 | (min_pos, max_pos) = self.find_bounding_box() 178 | 179 | build_schematic_nbt(min_pos, max_pos, block_data).write_file(file_path) 180 | -------------------------------------------------------------------------------- /mcthings/thing.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import mcpi.block 5 | from mcpi.vec3 import Vec3 6 | 7 | from ._version import __version__ 8 | 9 | from .blocks_memory import BlocksMemory 10 | from .scene import Scene 11 | from .utils import build_schematic_nbt 12 | from .world import World 13 | 14 | 15 | class Thing: 16 | """ base class for all objects in mcthings library """ 17 | 18 | block = mcpi.block.BRICK_BLOCK 19 | """ block type used by the thing. Default to BRICK_BLOCK """ 20 | _block_empty = mcpi.block.AIR 21 | """ block type used to remove blocks in this Thing """ 22 | 23 | def __init__(self, position, parent=None, scene=None): 24 | """ 25 | Create a thing 26 | :param position: build position 27 | :param parent: parent Thing in which this one is included 28 | :param scene: scene in which this Thing is included 29 | """ 30 | 31 | self._blocks_memory = BlocksMemory() 32 | self._children = [] 33 | self._decorators = [] 34 | self._end_position = None 35 | self._parent = parent 36 | self._position = None 37 | self._scene = scene 38 | 39 | if position: 40 | if not (isinstance(position.x, int) and 41 | isinstance(position.y, int) and 42 | isinstance(position.z, int)): 43 | raise RuntimeError("Bad position for Thing", 44 | position.x, position.y, position.z) 45 | 46 | self._position = mcpi.vec3.Vec3(position.x, position.y, position.z) 47 | 48 | if scene is None: 49 | # If no Scenes exists yet, create a new one 50 | if not World.scenes: 51 | Scene() # Scene add itself to the World 52 | 53 | """ Use the default Scene """ 54 | self._scene = World.first_scene() 55 | 56 | # Add then thing built to the scene 57 | if parent is None: 58 | self._scene.add(self) 59 | 60 | # McThing version which created this Thing 61 | self._version = __version__ 62 | 63 | @property 64 | def end_position(self): 65 | """ end position of the thing """ 66 | return self._end_position 67 | 68 | @property 69 | def position(self): 70 | """ initial position of the thing """ 71 | return self._position 72 | 73 | @property 74 | def parent(self): 75 | """ parent Thing in which this one is included """ 76 | return self._position 77 | 78 | @property 79 | def scene(self): 80 | """ scene which this thing is included """ 81 | return self._scene 82 | 83 | def add_child(self, child): 84 | """ Add a children to this Thing """ 85 | self._children.append(child) 86 | 87 | def set_block(self, pos, block_id, block_data=None): 88 | self._blocks_memory.set_block(pos, block_id, block_data) 89 | 90 | def set_blocks(self, init_pos, end_pos, block_id): 91 | """ Add a cuboid with the same block for all blocks and without specific data""" 92 | self._blocks_memory.set_blocks(init_pos, end_pos, block_id) 93 | 94 | def create(self): 95 | """ 96 | Create the Thing in memory (BlocksMemory) 97 | :return: 98 | """ 99 | 100 | def render(self): 101 | """ 102 | Render the Thing from memory (BlocksMemory) to show it 103 | 104 | :return: 105 | """ 106 | 107 | World.renderer.render(self._blocks_memory) 108 | for child in self._children: 109 | child.render() 110 | 111 | def build(self): 112 | """ 113 | Build the thing and show it using the renderer at position coordinates 114 | 115 | :return: 116 | """ 117 | 118 | self.create() 119 | self.render() 120 | 121 | def unbuild(self): 122 | """ 123 | Unbuild the thing in Minecraft 124 | 125 | :return: 126 | """ 127 | 128 | self._blocks_memory.fill(self._block_empty) 129 | self.render() 130 | self._blocks_memory.blocks = [] 131 | 132 | def move(self, position): 133 | """ 134 | Move the thing to a new position 135 | 136 | :param position: new position 137 | :return: 138 | """ 139 | 140 | self.unbuild() 141 | self._position = position 142 | self.build() 143 | 144 | def rotate(self, degrees): 145 | """ 146 | Rotate the thing in the x,z space using the blocks memory. 147 | 148 | :param degrees: degrees to rotate (90, 180, 270) 149 | :return: 150 | """ 151 | 152 | self._blocks_memory.rotate(degrees, self.position) 153 | 154 | # Update the position and end_position after the rotation 155 | init_pos, end_pos = self._blocks_memory.find_init_end_pos() 156 | self._position = init_pos 157 | self._end_position = end_pos 158 | 159 | def flip_x(self): 160 | """ 161 | Flip x-axis the things using the blocks memory. 162 | 163 | :return: 164 | """ 165 | 166 | self._blocks_memory.flip_x(self.position) 167 | 168 | # Update the position and end_position after the rotation 169 | init_pos, end_pos = self._blocks_memory.find_init_end_pos() 170 | self._position = init_pos 171 | self._end_position = end_pos 172 | 173 | def to_schematic(self, file_path, blocks_data=False): 174 | """ 175 | Convert the Thing to a Schematic Object 176 | 177 | :file_path: file in which to export the Thing in Schematic format 178 | :blocks_data: include blocks data (much slower) 179 | :return: the Schematic object 180 | """ 181 | 182 | build_schematic_nbt(self.position, self.end_position, blocks_data).write_file(file_path) 183 | 184 | def add_decorator(self, decorator): 185 | """ 186 | Add a new Decorator to be called once the Thing is decorated 187 | 188 | :param decorator: a Decorator to be called 189 | :return: 190 | """ 191 | self._decorators.append(decorator) 192 | 193 | def decorate(self): 194 | """ 195 | Call all decorators for the current Thing 196 | 197 | :return: 198 | """ 199 | for decorator in self._decorators: 200 | decorator(self).decorate() 201 | for child in self._children: 202 | decorator(child).decorate() 203 | 204 | def find_bounding_box(self): 205 | """ Compute the bounding box of the Thing """ 206 | 207 | return self.position, self.end_position 208 | -------------------------------------------------------------------------------- /mcthings/utils.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | 4 | import logging 5 | from datetime import datetime 6 | 7 | import mcpi 8 | from mcpi.vec3 import Vec3 9 | from nbt.nbt import NBTFile, TAG_List, TAG_Int, TAG_Short, TAG_Byte_Array, TAG_String 10 | 11 | from mcthings.world import World 12 | 13 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s') 14 | 15 | 16 | def size_region(init_pos, end_pos): 17 | """ 18 | Measure (size) the cuboid between init_pos and end_pos 19 | :param init_pos: 20 | :param end_pos: 21 | :return: 22 | """ 23 | 24 | size_x = end_pos.x - init_pos.x + 1 25 | size_z = end_pos.z - init_pos.z + 1 26 | size_y = end_pos.y - init_pos.y + 1 27 | 28 | return Vec3(size_x, size_y, size_z) 29 | 30 | 31 | def extract_region(init_pos, end_pos): 32 | """ 33 | Extract a Minecraft world region with the id of the blocks 34 | 35 | :return: bytearrays for blocks ids and block data 36 | """ 37 | 38 | size = size_region(init_pos, end_pos) 39 | 40 | blocks = World.renderer.get_blocks(Vec3(init_pos.x, init_pos.y, init_pos.z), 41 | Vec3(end_pos.x, end_pos.y, end_pos.z)) 42 | blocks_list = list(blocks) 43 | 44 | # The order in getBlocks is z, x, y and for a Schematic it must be x, z, y 45 | block_list_ordered = [] 46 | 47 | for y in range(0, size.y): 48 | x_by_z = [] # x, z plane for y 49 | 50 | for x in range(0, size.x): 51 | z_row = [] 52 | for z in range(0, size.z): 53 | z_row.append(blocks_list[(x * size.z + z) + (size.x * size.z) * y]) 54 | x_by_z.append(z_row) 55 | 56 | for z in range(0, size.z): 57 | for x in range(0, size.x): 58 | block_list_ordered.append(x_by_z[x][z]) 59 | 60 | # Create the data_bytes 61 | data_bytes = bytearray() 62 | blocks_bytes = bytearray() 63 | for i in range(0, len(block_list_ordered)): 64 | blocks_bytes.append(block_list_ordered[i]) 65 | data_bytes.append(0) 66 | 67 | return blocks_bytes, data_bytes 68 | 69 | 70 | def extract_region_with_data(init_pos, end_pos): 71 | """ 72 | Extract a Minecraft world region with the id and data of the blocks 73 | 74 | :return: bytearrays for blocks ids and block data 75 | """ 76 | size = size_region(init_pos, end_pos) 77 | 78 | blocks_bytes = bytearray() 79 | data_bytes = bytearray() 80 | 81 | # Use the same loop than reading Schematic format: x -> z -> y 82 | for y in range(0, size.y): 83 | for z in range(0, size.z): 84 | for x in range(0, size.x): 85 | block_pos = mcpi.vec3.Vec3(init_pos.x + x, init_pos.y + y, init_pos.z + z) 86 | block = World.renderer.get_block_with_data( 87 | Vec3(block_pos.x, block_pos.y, block_pos.z)) 88 | blocks_bytes.append(block.id) 89 | data_bytes.append(block.data) 90 | 91 | return blocks_bytes, data_bytes 92 | 93 | 94 | def build_schematic_nbt(init_pos, end_pos, block_data=False, memory_data=None): 95 | """ 96 | Creates a NBT Object with the schematic data 97 | 98 | :param init_pos: initial position for extracting the Schematic 99 | :param end_pos: end position for extracting the Schematic 100 | :param block_data: extract blocks ids and data (much slower) 101 | :param memory_data: get blocks from memory 102 | 103 | 104 | :return: The NBT object with the Schematic 105 | """ 106 | size = size_region(init_pos, end_pos) 107 | 108 | # Profiling of Schematics export 109 | app_init = datetime.now() 110 | logging.info("Schematic: Exporting blocks: %i" % (size.x * size.y * size.z)) 111 | 112 | # Prepare the NBT Object 113 | nbtfile = NBTFile() 114 | nbtfile.name = "Schematic" 115 | nbtfile.tags.append(TAG_Short(name="Width", value=size.x)) 116 | nbtfile.tags.append(TAG_Short(name="Height", value=size.y)) 117 | nbtfile.tags.append(TAG_Short(name="Length", value=size.z)) 118 | nbt_blocks = TAG_Byte_Array(name="Blocks") 119 | nbtfile.tags.append(nbt_blocks) 120 | nbt_data = TAG_Byte_Array(name="Data") 121 | nbtfile.tags.append(nbt_data) 122 | 123 | # Additional fields need 124 | nbtfile.tags.append(TAG_String(name="Materials", value="Alpha")) 125 | nbtfile.tags.append(TAG_Int(name="'WEOriginX'", value=0)) 126 | nbtfile.tags.append(TAG_Int(name="'WEOriginY'", value=0)) 127 | nbtfile.tags.append(TAG_Int(name="'WEOriginZ'", value=0)) 128 | nbtfile.tags.append(TAG_Int(name="'WEOffsetX'", value=0)) 129 | nbtfile.tags.append(TAG_Int(name="'WEOffsetY'", value=0)) 130 | nbtfile.tags.append(TAG_Int(name="'WEOffsetZ'", value=0)) 131 | entities_list = TAG_List(name="Entities", type=TAG_Int) 132 | nbtfile.tags.append(entities_list) 133 | tile_entities_list = TAG_List(name="TileEntities", type=TAG_Int) 134 | nbtfile.tags.append(tile_entities_list) 135 | 136 | # Collect all blocks 137 | if not memory_data: 138 | if block_data: 139 | (blocks_bytes, data_bytes) = extract_region_with_data(init_pos, end_pos) 140 | else: 141 | (blocks_bytes, data_bytes) = extract_region(init_pos, end_pos) 142 | else: 143 | (blocks_bytes, data_bytes) = memory_data.to_nbt(init_pos, end_pos) 144 | 145 | nbt_blocks.value = blocks_bytes 146 | nbt_data.value = data_bytes 147 | 148 | total_time_min = (datetime.now() - app_init).total_seconds() 149 | logging.info("Schematic export finished in %.2f secs" % total_time_min) 150 | 151 | return nbtfile 152 | 153 | 154 | def find_min_max_cuboid_vertex(vertex, vertex_opposite): 155 | """ 156 | Find the min vertex and the max vertex for a given cuboid 157 | defined from two opposite vertexes 158 | 159 | :param vertex: a vertex in the cuboid 160 | :param vertex_opposite: the opposite vertex of the cuboid 161 | :return: vertex_min, vertex_max 162 | """ 163 | 164 | vertex_min = vertex_max = None 165 | 166 | width = abs(vertex_opposite.x - vertex.x) 167 | height = abs(vertex_opposite.y - vertex.y) 168 | length = abs(vertex_opposite.z - vertex.z) 169 | 170 | # Find all vertex in the up face: up1 and up3 are already known 171 | up1 = vertex_opposite 172 | up3 = Vec3(vertex.x, vertex_opposite.y, vertex.z) 173 | if vertex.y > vertex_opposite.y: 174 | up1 = vertex 175 | up3 = Vec3(vertex_opposite.x, vertex.y, vertex_opposite.z) 176 | # Now we need to find the two other vertexes up2, up4 177 | # Looking at up1 there are two options for up2 and up4 178 | up1x1 = Vec3(up1.x + width, up1.y, up1.z) 179 | up1x2 = Vec3(up1.x - width, up1.y, up1.z) 180 | up1z1 = Vec3(up1.x, up1.y, up1.z + length) 181 | up1z2 = Vec3(up1.x, up1.y, up1.z - length) 182 | # Looking at up3 there are two options for up2 and up4 183 | up3x1 = Vec3(up3.x + width, up3.y, up3.z) 184 | up3x2 = Vec3(up3.x - width, up3.y, up3.z) 185 | up3z1 = Vec3(up3.x, up3.y, up3.z + length) 186 | up3z2 = Vec3(up3.x, up3.y, up3.z - length) 187 | # The right vertex is the common one for up2 and up4 188 | if up1x1 == up3z1 or up1x1 == up3z2: 189 | up2 = up1x1 190 | elif up1x2 == up3z1 or up1x2 == up3z2: 191 | up2 = up1x2 192 | else: 193 | raise RuntimeError("Bad min an max vertex for cuboid") 194 | if up1z1 == up3x1 or up1z1 == up3x2: 195 | up4 = up1z1 196 | elif up1z2 == up3x1 or up1z2 == up3x2: 197 | up4 = up1z2 198 | else: 199 | raise RuntimeError("Bad min an max vertex for cuboid") 200 | 201 | # Now select the min and max vertex for up face 202 | x_min = x_max = up1.x # init with a possible value 203 | z_min = z_max = up1.z # init with a possible value 204 | for v in [up1, up2, up3, up4]: 205 | x_max = v.x if v.x > x_max else x_max 206 | x_min = v.x if v.x < x_min else x_min 207 | z_max = v.z if v.z > z_max else z_max 208 | z_min = v.z if v.z < z_min else z_min 209 | 210 | # And now select the min and max vertex 211 | for v in [up1, up2, up3, up4]: 212 | if v.x == x_min and v.z == z_min: 213 | vertex_min = Vec3(v.x, v.y - height, v.z) 214 | if v.x == x_max and v.z == z_max: 215 | vertex_max = Vec3(v.x, v.y, v.z) 216 | 217 | if vertex_min is None or vertex_max is None: 218 | raise RuntimeError("Bad min an max vertex for cuboid") 219 | 220 | return vertex_min, vertex_max 221 | 222 | 223 | -------------------------------------------------------------------------------- /mcthings/blocks_memory.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author (©): Alvaro del Castillo 3 | import logging 4 | import math 5 | 6 | from mcpi.vec3 import Vec3 7 | import mcpi.block 8 | 9 | from mcthings.utils import size_region, find_min_max_cuboid_vertex 10 | 11 | 12 | class BlockMemory: 13 | 14 | def __init__(self, block_id, block_data, pos): 15 | self.id = block_id 16 | self.data = block_data 17 | self.pos = pos 18 | 19 | 20 | class BlocksMemory: 21 | """ 22 | Blocks memory for a Thing 23 | """ 24 | 25 | def __init__(self): 26 | 27 | self.blocks = [] 28 | self._blocks_pos = {} 29 | 30 | def add(self, block_memory): 31 | """ 32 | Add a new block to the memory. 33 | 34 | :param block_memory: memory for a block 35 | :return: 36 | """ 37 | 38 | self.blocks.append(block_memory) 39 | 40 | def find_init_end_pos(self): 41 | """ Find the init and end cuboid positions from all the blocks in the memory """ 42 | 43 | first_pos = self.blocks[0].pos 44 | 45 | init_pos = Vec3(first_pos.x, first_pos.y, first_pos.z) 46 | end_pos = Vec3(first_pos.x, first_pos.y, first_pos.z) 47 | 48 | for block in self.blocks: 49 | pos = block.pos 50 | if pos.x < init_pos.x: 51 | init_pos = Vec3(pos.x, init_pos.y, init_pos.z) 52 | if pos.y < init_pos.y: 53 | init_pos = Vec3(init_pos.x, pos.y, init_pos.z) 54 | if pos.z < init_pos.z: 55 | init_pos = Vec3(init_pos.x, init_pos.y, pos.z) 56 | 57 | if pos.x > end_pos.x: 58 | end_pos = Vec3(pos.x, end_pos.y, end_pos.z) 59 | if pos.y > end_pos.y: 60 | end_pos = Vec3(end_pos.x, pos.y, end_pos.z) 61 | if pos.z > end_pos.z: 62 | end_pos = Vec3(end_pos.x, end_pos.y, pos.z) 63 | 64 | return init_pos, end_pos 65 | 66 | def is_cuboid(self): 67 | """ Check if the memory is a filled cuboid """ 68 | 69 | cuboid = False 70 | 71 | # Check that the number of blocks needed for the filled cuboid is the same that the blocks 72 | init_pos, vertex_max = self.find_init_end_pos() 73 | size = size_region(init_pos, vertex_max) 74 | 75 | if size.x * size.y * size.z == len(self.blocks): 76 | cuboid = True 77 | 78 | return cuboid 79 | 80 | def memory_equal(self): 81 | """ Check if all the blocks in the memory are equal """ 82 | equal = True 83 | 84 | if self.blocks: 85 | last_block = self.blocks[0] 86 | 87 | for block in self.blocks: 88 | if block.id != last_block.id or block.data != last_block.data: 89 | equal = False 90 | break 91 | last_block = block 92 | else: 93 | equal = False 94 | 95 | return equal 96 | 97 | def flip_x(self, position): 98 | """ 99 | Flip based on x-axis the blocks in memory using position as base position from which to rotate 100 | :param position: base position from which to rotate 101 | :return: 102 | """ 103 | 104 | for block in self.blocks: 105 | # Find the x position and flip it 106 | width = abs(block.pos.x - position.x) 107 | # TODO: the flip could be done in two directions (left or right) 108 | # This one the the flip to the right 109 | x_flipped = position.x - width 110 | block.pos.x = x_flipped 111 | 112 | def fill(self, fill_block): 113 | """ 114 | Fill all blocks in memory with fill_block 115 | 116 | :param fill_block: block to be used to fill all memory 117 | :return: 118 | """ 119 | 120 | for block in self.blocks: 121 | block.id = fill_block.id 122 | block.data = fill_block.data 123 | 124 | def rotate(self, degrees, position): 125 | """ 126 | Rotate degrees the blocks in memory using position as base position from which to rotate 127 | :param degrees: degrees to rotate (90, 180, 270) 128 | :param position: base position from which to rotate 129 | :return: 130 | """ 131 | valid_degrees = [90, 180, 270] 132 | 133 | if degrees not in [90, 180, 270]: 134 | raise RuntimeError("Invalid degrees: %s (valid: %s) " % (degrees, valid_degrees)) 135 | 136 | cos_degrees = math.cos(math.radians(degrees)) 137 | sin_degrees = math.sin(math.radians(degrees)) 138 | 139 | def rotate_x(pos_x, pos_z): 140 | return pos_x * cos_degrees - pos_z * sin_degrees 141 | 142 | def rotate_z(pos_x, pos_z): 143 | return pos_z * cos_degrees + pos_x * sin_degrees 144 | 145 | # Base position for the rotation 146 | init_pos = position 147 | rotated_blocks = [] 148 | 149 | # Rotate all blocks with respect the initial position and add them 150 | for block in self.blocks: 151 | b = block.id 152 | d = block.data 153 | 154 | x = block.pos.x - init_pos.x 155 | z = block.pos.z - init_pos.z 156 | rotated_x = round(init_pos.x + rotate_x(x, z)) 157 | rotated_z = round(init_pos.z + rotate_z(x, z)) 158 | rotated_blocks.append(BlockMemory(b, d, Vec3(rotated_x, block.pos.y, rotated_z))) 159 | 160 | # Replace all blocks in memory with the rotated ones 161 | self.blocks = [] 162 | for rotated_block in rotated_blocks: 163 | self.set_block(rotated_block.pos, rotated_block.id, rotated_block.data) 164 | 165 | def set_block(self, pos, block_id, block_data=None): 166 | self.add(BlockMemory(block_id, block_data, pos)) 167 | 168 | def set_blocks(self, vertex, vertex_opposite, block_id): 169 | """ Add a cuboid with the same block for all blocks and without specific data """ 170 | 171 | block_data = None 172 | 173 | width = abs(vertex_opposite.x - vertex.x) + 1 174 | height = abs(vertex_opposite.y - vertex.y) + 1 175 | length = abs(vertex_opposite.z - vertex.z) + 1 176 | 177 | vertex_min, vertex_max = find_min_max_cuboid_vertex(vertex, vertex_opposite) 178 | 179 | for y in range(0, height): 180 | for z in range(0, length): 181 | for x in range(0, width): 182 | block_pos = Vec3(vertex_min.x + x, vertex_min.y + y, vertex_min.z + z) 183 | self.set_block(block_pos, block_id, block_data) 184 | 185 | def _create_blocks_pos(self): 186 | logging.info("Creating the memory cache with positions") 187 | for block in self.blocks: 188 | self._blocks_pos[str(block.pos)] = block 189 | logging.info("Done memory cache with positions") 190 | 191 | def find_block_at_pos(self, pos): 192 | """ 193 | Find a block in memory give its position 194 | TODO: Improve performance 195 | 196 | :param pos: position for the block 197 | :return: the block found or None 198 | """ 199 | 200 | if not self._blocks_pos: 201 | self._create_blocks_pos() 202 | 203 | block_found = None 204 | if str(pos) in self._blocks_pos: 205 | block_found = self._blocks_pos[str(pos)] 206 | 207 | return block_found 208 | 209 | def to_nbt(self, init_pos, end_pos): 210 | """ 211 | Convert the blocks of memory to NBT format for exporting as Schematic 212 | The NBT must be a complete cuboid with air in the positions where 213 | there are no data in blocks memory. 214 | 215 | 216 | :return: bytearrays for blocks ids and block data 217 | """ 218 | 219 | size = size_region(init_pos, end_pos) 220 | 221 | blocks_bytes = bytearray() 222 | data_bytes = bytearray() 223 | 224 | # Use the same loop than reading Schematic format: x -> z -> y 225 | for y in range(0, size.y): 226 | for z in range(0, size.z): 227 | for x in range(0, size.x): 228 | block_data = 0 229 | block_id = mcpi.block.AIR.id 230 | block_pos = Vec3(init_pos.x + x, init_pos.y + y, init_pos.z + z) 231 | # Find if there is a block at block_pos 232 | mem_block = self.find_block_at_pos(block_pos) 233 | if mem_block: 234 | block_id = mem_block.id 235 | block_data = mem_block.data 236 | blocks_bytes.append(block_id) 237 | data_bytes.append(block_data) 238 | 239 | return blocks_bytes, data_bytes 240 | 241 | def build_schematic(self): 242 | init_pos, end_pos = self.find_init_end_pos() 243 | 244 | return self.to_nbt(init_pos, end_pos, self) 245 | 246 | def to_schematic(self, file_path): 247 | """ 248 | Convert the blocks memory to a Schematic Object 249 | 250 | :file_path: file in which to export the memory in Schematic format 251 | :return: the Schematic object 252 | """ 253 | 254 | self.build_schematic().write_file(file_path) 255 | 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mcthings/vox.py: -------------------------------------------------------------------------------- 1 | # Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0 2 | # Author/s (©): Alvaro del Castillo 3 | 4 | from math import sqrt 5 | 6 | import chunk 7 | import logging 8 | 9 | import mcpi.block 10 | from mcpi.vec3 import Vec3 11 | 12 | from mcthings.thing import Thing 13 | 14 | 15 | class Voxel: 16 | def __init__(self, bytes): 17 | self.x = bytes[0] 18 | self.y = bytes[1] 19 | self.z = bytes[2] 20 | self.color_index = bytes[3] - 1 21 | 22 | 23 | class Color: 24 | """ RGBA format palette """ 25 | _color2minecraft = {} # Cache to convert a color to minecraft color 26 | 27 | def __init__(self, hex_str): 28 | self.hex_str = hex_str 29 | 30 | def rgb(self): 31 | red = int(self.hex_str[0:2], 16) 32 | green = int(self.hex_str[2:4], 16) 33 | blue = int(self.hex_str[4:6], 16) 34 | 35 | return red, green, blue 36 | 37 | def compare(self, color): 38 | # https://www.compuphase.com/cmetric.htm 39 | r1, g1, b1 = self.rgb() 40 | r2, g2, b2 = color.rgb() 41 | 42 | read_mean = (r1 + r2) / 2 43 | r = r1 - r2 44 | g = g1 - g2 45 | b = b1 - b2 46 | return sqrt((round((512 + read_mean) * r * r) >> 8) + 4.0 * g * g + (round((767 - read_mean) * b * b) >> 8)) 47 | 48 | def minecraft(self): 49 | # https://gaming.stackexchange.com/questions/47212/what-are-the-color-values-for-dyed-wool 50 | mc_colors = [ 51 | ("White", "e4e4e4"), 52 | ("Orange", "ea7e35"), 53 | ("Magenta", "be49c9"), 54 | ("Light Blue", "6387d2"), 55 | ("Yellow", "c2b51c"), 56 | ("Lime", "39ba2e"), 57 | ("Pink", "d98199"), 58 | ("Grey", "414141"), 59 | ("Light grey", "a0a7a7"), 60 | ("Cyan", "267191"), 61 | ("Purple", "7e34bf"), 62 | ("Blue", "253193"), 63 | ("Brown", "56331c"), 64 | ("Green", "364b18"), 65 | ("Red", "9e2b27"), 66 | ("Black", "181414") 67 | ] 68 | 69 | mc_color_number = {} 70 | mc_colors_hex = {} 71 | for i in range(0, len(mc_colors)): 72 | mc_color_number[mc_colors[i][1]] = i 73 | mc_colors_hex[mc_colors[i][1]] = mc_colors[i][0] 74 | 75 | # Find the closest Minecraft color 76 | rgb = self.hex_str[0:6] 77 | dist = float('inf') 78 | if rgb in mc_colors_hex: 79 | # Direct mapping 80 | color = rgb 81 | elif rgb in Color._color2minecraft: 82 | # Color already mapped 83 | color = Color._color2minecraft[rgb] 84 | else: 85 | for mc_color in mc_colors_hex: 86 | cdist = self.compare(Color(mc_color)) 87 | if cdist < dist: 88 | dist = cdist 89 | color = mc_color 90 | self._color2minecraft[rgb] = color 91 | 92 | return mc_color_number[color] 93 | 94 | 95 | class VoxDefaultPalette: 96 | # Removed first "0x00000000" (it does not appear in MV) 97 | # Reverse order: ABGR 98 | palette = [ 99 | "0xffffffff", "0xffccffff", "0xff99ffff", "0xff66ffff", "0xff33ffff", "0xff00ffff", "0xffffccff", 100 | "0xffccccff", 101 | "0xff99ccff", "0xff66ccff", "0xff33ccff", "0xff00ccff", "0xffff99ff", "0xffcc99ff", "0xff9999ff", 102 | "0xff6699ff", "0xff3399ff", "0xff0099ff", "0xffff66ff", "0xffcc66ff", "0xff9966ff", "0xff6666ff", "0xff3366ff", 103 | "0xff0066ff", 104 | "0xffff33ff", "0xffcc33ff", "0xff9933ff", "0xff6633ff", "0xff3333ff", "0xff0033ff", "0xffff00ff", 105 | "0xffcc00ff", "0xff9900ff", "0xff6600ff", "0xff3300ff", "0xff0000ff", "0xffffffcc", "0xffccffcc", "0xff99ffcc", 106 | "0xff66ffcc", 107 | "0xff33ffcc", "0xff00ffcc", "0xffffcccc", "0xffcccccc", "0xff99cccc", "0xff66cccc", "0xff33cccc", 108 | "0xff00cccc", "0xffff99cc", "0xffcc99cc", "0xff9999cc", "0xff6699cc", "0xff3399cc", "0xff0099cc", "0xffff66cc", 109 | "0xffcc66cc", 110 | "0xff9966cc", "0xff6666cc", "0xff3366cc", "0xff0066cc", "0xffff33cc", "0xffcc33cc", "0xff9933cc", 111 | "0xff6633cc", "0xff3333cc", "0xff0033cc", "0xffff00cc", "0xffcc00cc", "0xff9900cc", "0xff6600cc", "0xff3300cc", 112 | "0xff0000cc", 113 | "0xffffff99", "0xffccff99", "0xff99ff99", "0xff66ff99", "0xff33ff99", "0xff00ff99", "0xffffcc99", 114 | "0xffcccc99", "0xff99cc99", "0xff66cc99", "0xff33cc99", "0xff00cc99", "0xffff9999", "0xffcc9999", "0xff999999", 115 | "0xff669999", 116 | "0xff339999", "0xff009999", "0xffff6699", "0xffcc6699", "0xff996699", "0xff666699", "0xff336699", 117 | "0xff006699", "0xffff3399", "0xffcc3399", "0xff993399", "0xff663399", "0xff333399", "0xff003399", "0xffff0099", 118 | "0xffcc0099", 119 | "0xff990099", "0xff660099", "0xff330099", "0xff000099", "0xffffff66", "0xffccff66", "0xff99ff66", 120 | "0xff66ff66", "0xff33ff66", "0xff00ff66", "0xffffcc66", "0xffcccc66", "0xff99cc66", "0xff66cc66", "0xff33cc66", 121 | "0xff00cc66", 122 | "0xffff9966", "0xffcc9966", "0xff999966", "0xff669966", "0xff339966", "0xff009966", "0xffff6666", 123 | "0xffcc6666", "0xff996666", "0xff666666", "0xff336666", "0xff006666", "0xffff3366", "0xffcc3366", "0xff993366", 124 | "0xff663366", 125 | "0xff333366", "0xff003366", "0xffff0066", "0xffcc0066", "0xff990066", "0xff660066", "0xff330066", 126 | "0xff000066", "0xffffff33", "0xffccff33", "0xff99ff33", "0xff66ff33", "0xff33ff33", "0xff00ff33", "0xffffcc33", 127 | "0xffcccc33", 128 | "0xff99cc33", "0xff66cc33", "0xff33cc33", "0xff00cc33", "0xffff9933", "0xffcc9933", "0xff999933", 129 | "0xff669933", "0xff339933", "0xff009933", "0xffff6633", "0xffcc6633", "0xff996633", "0xff666633", "0xff336633", 130 | "0xff006633", 131 | "0xffff3333", "0xffcc3333", "0xff993333", "0xff663333", "0xff333333", "0xff003333", "0xffff0033", 132 | "0xffcc0033", "0xff990033", "0xff660033", "0xff330033", "0xff000033", "0xffffff00", "0xffccff00", "0xff99ff00", 133 | "0xff66ff00", 134 | "0xff33ff00", "0xff00ff00", "0xffffcc00", "0xffcccc00", "0xff99cc00", "0xff66cc00", "0xff33cc00", 135 | "0xff00cc00", "0xffff9900", "0xffcc9900", "0xff999900", "0xff669900", "0xff339900", "0xff009900", "0xffff6600", 136 | "0xffcc6600", 137 | "0xff996600", "0xff666600", "0xff336600", "0xff006600", "0xffff3300", "0xffcc3300", "0xff993300", 138 | "0xff663300", "0xff333300", "0xff003300", "0xffff0000", "0xffcc0000", "0xff990000", "0xff660000", "0xff330000", 139 | "0xff0000ee", 140 | "0xff0000dd", "0xff0000bb", "0xff0000aa", "0xff000088", "0xff000077", "0xff000055", "0xff000044", 141 | "0xff000022", "0xff000011", "0xff00ee00", "0xff00dd00", "0xff00bb00", "0xff00aa00", "0xff008800", "0xff007700", 142 | "0xff005500", 143 | "0xff004400", "0xff002200", "0xff001100", "0xffee0000", "0xffdd0000", "0xffbb0000", "0xffaa0000", 144 | "0xff880000", "0xff770000", "0xff550000", "0xff440000", "0xff220000", "0xff110000", "0xffeeeeee", "0xffdddddd", 145 | "0xffbbbbbb", 146 | "0xffaaaaaa", "0xff888888", "0xff777777", "0xff555555", "0xff444444", "0xff222222", "0xff111111" 147 | ] 148 | 149 | 150 | class Vox(Thing): 151 | file_path = None 152 | """ file path for the MagicaVoxel vox file """ 153 | 154 | def parse_vox_file(self): 155 | if not self.file_path: 156 | RuntimeError("Missing file_path param") 157 | 158 | self.voxels = [] 159 | self.palette = [] 160 | self.materials = [] 161 | 162 | # Read the vox data in RIFF format 163 | # https://github.com/python/cpython/blob/3.8/Lib/chunk.py 164 | vox_file = open(self.file_path, "rb") 165 | vox_chunk = chunk.Chunk(vox_file, bigendian=False) 166 | chunk_name = vox_chunk.getname().decode("utf-8") 167 | if chunk_name != 'VOX ': 168 | raise RuntimeError('File %s is not a VOX file' % self.file_path) 169 | version = vox_chunk.chunksize 170 | if version != 150: 171 | raise RuntimeError('File %s has a not supported VOX version %i' % (self.file_path, version)) 172 | # Let's read chunks 173 | """ 174 | 2. Chunk Structure 175 | ------------------------------------------------------------------------------- 176 | # Bytes | Type | Value 177 | ------------------------------------------------------------------------------- 178 | 1x4 | char | chunk id 179 | 4 | int | num bytes of chunk content (N) 180 | 4 | int | num bytes of children chunks (M) 181 | N | | chunk content 182 | M | | children chunks 183 | ------------------------------------------------------------------------------- 184 | """ 185 | # MAIN Chunk 186 | main_chunk = chunk.Chunk(vox_file, bigendian=False) 187 | # Pass last 4 bytes for MAIN Chunk with children chunks 188 | vox_file.seek(vox_file.tell() + 4) 189 | 190 | # SIZE CHUNK 191 | """ 192 | ------------------------------------------------------------------------------- 193 | # Bytes | Type | Value 194 | ------------------------------------------------------------------------------- 195 | 4 | int | size x 196 | 4 | int | size y 197 | 4 | int | size z : gravity direction 198 | ------------------------------------------------------------------------------- 199 | """ 200 | size_chunk = chunk.Chunk(vox_file, bigendian=False) 201 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 202 | x = size_chunk.read(4) 203 | y = size_chunk.read(4) 204 | z = size_chunk.read(4) 205 | 206 | # XYZI voxels 207 | """ 208 | ------------------------------------------------------------------------------- 209 | # Bytes | Type | Value 210 | ------------------------------------------------------------------------------- 211 | 4 | int | numVoxels (N) 212 | 4 x N | int | (x, y, z, colorIndex) : 1 byte for each component 213 | ------------------------------------------------------------------------------- 214 | """ 215 | xyzi_chunk = chunk.Chunk(vox_file, bigendian=False) 216 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 217 | n_voxels_bytes = xyzi_chunk.read(4) 218 | n_voxels = int.from_bytes(n_voxels_bytes, "little") 219 | for i in range(0, n_voxels): 220 | self.voxels.append(Voxel(xyzi_chunk.read(4))) 221 | # Transform or palette chunk or no more chunk if default palette 222 | try: 223 | transform_chunk = chunk.Chunk(vox_file, bigendian=False) 224 | except EOFError: 225 | transform_chunk = None 226 | logging.info("Legacy vox file with default palette") 227 | if transform_chunk and transform_chunk.chunkname.decode("utf-8") == "nTRN": 228 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 229 | transform_chunk.skip() 230 | 231 | group_chunk = chunk.Chunk(vox_file, bigendian=False) 232 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 233 | group_chunk.skip() 234 | 235 | transform_chunk = chunk.Chunk(vox_file, bigendian=False) 236 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 237 | # transform_chunk.skip() # it is skipping 1 byte in the next chunk 238 | vox_file.read(transform_chunk.getsize()) 239 | 240 | shape_chunk = chunk.Chunk(vox_file, bigendian=False) 241 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 242 | vox_file.read(shape_chunk.getsize()) 243 | 244 | # Layers chunk 245 | NUM_LAYERS = 8 # By default 246 | for i in range(0, NUM_LAYERS): 247 | layer_chunk = chunk.Chunk(vox_file, bigendian=False) 248 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 249 | vox_file.read(layer_chunk.getsize()) 250 | """ 251 | 7. Chunk id 'RGBA' : palette 252 | ------------------------------------------------------------------------------- 253 | # Bytes | Type | Value 254 | ------------------------------------------------------------------------------- 255 | 4 x 256 | int | (R, G, B, A) : 1 byte for each component 256 | | * 257 | | * color [0-254] are mapped to palette index [1-255], e.g : 258 | | 259 | | for ( int i = 0; i <= 254; i++ ) { 260 | | palette[i + 1] = ReadRGBA(); 261 | | } 262 | ------------------------------------------------------------------------------- 263 | """ 264 | rgba_chunk = chunk.Chunk(vox_file, bigendian=False) 265 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 266 | else: 267 | rgba_chunk = transform_chunk 268 | vox_file.seek(vox_file.tell() + 4) # notice 269 | if rgba_chunk: 270 | if rgba_chunk.getname().decode("utf-8") != 'RGBA': 271 | raise RuntimeError('VOX format not supported (multimodel?)') 272 | for i in range(0, round(rgba_chunk.getsize() / 4)): 273 | # RGBA 274 | color_bytes = rgba_chunk.read(1) + rgba_chunk.read(1) + rgba_chunk.read(1) + rgba_chunk.read(1) 275 | self.palette.append(Color(color_bytes.hex())) 276 | else: 277 | # Default palette 278 | for i in range(0, len(VoxDefaultPalette.palette)): 279 | color = VoxDefaultPalette.palette[i].replace('0x', '') 280 | # Convert ABGR to RGBA 281 | color = color[::-1] 282 | self.palette.append(Color(color)) 283 | 284 | # Read the materials palette 285 | """ 286 | (4) Material Chunk : "MATL" 287 | 288 | int32 : material id 289 | DICT : material properties 290 | (_type : str) _diffuse, _metal, _glass, _emit 291 | (_weight : float) range 0 ~ 1 292 | (_rough : float) 293 | (_spec : float) 294 | (_ior : float) 295 | (_att : float) 296 | (_flux : float) 297 | (_plastic) 298 | """ 299 | # One material per each color 300 | for i in range(0, len(self.palette)): 301 | try: 302 | materials_chunk = chunk.Chunk(vox_file, bigendian=False) 303 | if materials_chunk.getname().decode("utf-8") != 'MATL': 304 | logging.info("Material data not found") 305 | break 306 | else: 307 | vox_file.seek(vox_file.tell() + 4) # number of children chunks 308 | material_id = int.from_bytes(materials_chunk.read(4), "little") 309 | dict_entries_len = int.from_bytes(materials_chunk.read(4), "little") 310 | # Read the _type key from dict 311 | key_str_len = int.from_bytes(materials_chunk.read(4), "little") 312 | key_str = materials_chunk.read(key_str_len).decode('utf-8') 313 | value_str_len = int.from_bytes(materials_chunk.read(4), "little") 314 | value_str = materials_chunk.read(value_str_len).decode('utf-8') 315 | self.materials.append(value_str) 316 | materials_chunk.skip() 317 | if materials_chunk.tell() > materials_chunk.getsize(): 318 | vox_file.seek(vox_file.tell() - 1) # Hack: not sure why skip goes 1 byte more 319 | except EOFError: 320 | logging.info("Material data not found") 321 | break 322 | 323 | @classmethod 324 | def find_minecraft_material(cls, material): 325 | mc_material = None 326 | if material == '_glass': 327 | mc_material = mcpi.block.GLASS 328 | elif material == '_metal': 329 | mc_material = mcpi.block.IRON_BLOCK 330 | return mc_material 331 | 332 | def create(self): 333 | 334 | self.parse_vox_file() 335 | 336 | for voxel in self.voxels: 337 | voxel_color = self.palette[voxel.color_index] 338 | minecraft_material = None 339 | if self.materials: 340 | minecraft_material = self.find_minecraft_material(self.materials[voxel.color_index]) 341 | minecraft_color = voxel_color.minecraft() 342 | 343 | # y, z are the reverse in vox format 344 | pos = Vec3(self.position.x + voxel.x, 345 | self.position.y + voxel.z, 346 | self.position.z + voxel.y 347 | ) 348 | 349 | if self.block == self._block_empty: 350 | self.set_block(pos, self._block_empty) 351 | elif minecraft_material: 352 | self.set_block(pos, minecraft_material.id) 353 | else: 354 | self.set_block(pos, mcpi.block.WOOL.id, minecraft_color) 355 | 356 | init_pos, end_pos = self._blocks_memory.find_init_end_pos() 357 | self._end_position = end_pos 358 | --------------------------------------------------------------------------------