From 507fd3ab79f14ee37042b03fc49326c427d97326 Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Tue, 16 Sep 2025 23:16:43 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migration=20to=20nbtx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 35 +++---- pyproject.toml | 9 +- src/mcstructure/__init__.py | 179 ++++++++++++++++++------------------ 3 files changed, 110 insertions(+), 113 deletions(-) diff --git a/poetry.lock b/poetry.lock index c924cfc..9987206 100644 --- a/poetry.lock +++ b/poetry.lock @@ -565,6 +565,22 @@ rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-bo testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pygments (<2.19)", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] +[[package]] +name = "nbtx" +version = "0.3.0" +description = "Minecraft NBT parser" +optional = false +python-versions = ">=3.13" +groups = ["main"] +files = [ + {file = "nbtx-0.3.0-py3-none-any.whl", hash = "sha256:0171d7e581a5a58471eac56e4bec39108254bb191d55bae4fc622a76970c0c46"}, + {file = "nbtx-0.3.0.tar.gz", hash = "sha256:22154e73125b1fd86dd8a2da17670a2d87c7c3c65d040de7476b71507ef3689c"}, +] + +[package.extras] +dev = ["codespell (==2.4.1)", "coverage (==7.10.6)", "docstr-coverage (==2.3.2)", "isort (==6.0.1)", "mypy (==1.17.1)", "nox (==2025.5.1)", "pytest (==8.4.1)", "ruff (==0.12.12)"] +docs = ["pdoc (==15.0.4)"] + [[package]] name = "numpy" version = "2.3.0" @@ -703,21 +719,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pynbt" -version = "2.0.0" -description = "Tiny, liberally-licensed NBT library (Minecraft)." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "PyNBT-2.0.0-py3-none-any.whl", hash = "sha256:e46a7b92574482d23f608b7af3ade8df23d48fced952d86727676147c2e8e9f6"}, - {file = "PyNBT-2.0.0.tar.gz", hash = "sha256:250128278421dedd497b3b4276ed5df6353562b221c03472796e5bf19d2a3077"}, -] - -[package.extras] -test = ["pytest"] - [[package]] name = "pytest" version = "8.4.1" @@ -1091,5 +1092,5 @@ docs = ["Sphinx", "furo", "myst-parser", "sphinx-copybutton"] [metadata] lock-version = "2.1" -python-versions = ">=3.11" -content-hash = "b761dc74c12d1b08d2abde57aa6a2b6cf7ee98cb0e61027ecd197ad540ad33e7" +python-versions = ">=3.13" +content-hash = "7bd114d1fce906ab16c76a65e9066b46cd2678a4e7bd5e86f57facd9888b898f" diff --git a/pyproject.toml b/pyproject.toml index 4f3d712..dfa0bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ version = "0.0.1b6" description = "Read and write Minecraft .mcstructure files." dependencies = [ "numpy==2.3.0", - "pynbt==2", + "nbtx==0.3.1", ] -requires-python = ">=3.11" +requires-python = ">=3.13" readme = "README.md" license = {text = "MIT"} classifiers = [ @@ -41,8 +41,9 @@ docs = [ "sphinx-copybutton>=0", ] +[tool.mypy] +strict = true + [project.urls] "Documentation" = "https://mcstructure.readthedocs.io/en/latest/" "Source Code" = "https://github.com/phoenixr-codes/mcstructure/" - - diff --git a/src/mcstructure/__init__.py b/src/mcstructure/__init__.py index 38fb284..1f88745 100644 --- a/src/mcstructure/__init__.py +++ b/src/mcstructure/__init__.py @@ -11,11 +11,11 @@ from dataclasses import dataclass from functools import partial from itertools import repeat -from typing import Any, BinaryIO, Tuple, Self +from typing import Any, BinaryIO, Tuple, Self, cast import numpy as np from numpy.typing import NDArray -from pynbt import BaseTag, NBTFile, TAG_Compound, TAG_Int, TAG_List, TAG_String # type: ignore +import nbtx Coordinate = Tuple[int, int, int] @@ -37,47 +37,6 @@ """ -# TODO: cover all tags -def _into_pyobj(tag: BaseTag) -> Any: - """ - Turns an NBT tree into a python tree. - """ - if isinstance(tag, (TAG_Compound, dict)): - res = {} - for key, value in tag.items(): - if isinstance(value, BaseTag): - value = _into_pyobj(value) - res[key] = value - return res - - if isinstance(tag, (TAG_List, list)): - res = [] - for value in tag: - if isinstance(value, BaseTag): - value = _into_pyobj(value) - res.append(value) - return res - - if isinstance(tag, BaseTag): - return tag.value - - return tag - - -# TODO: cover all types -def _into_tag(obj: Any) -> BaseTag: - """ - Turn a python tree into an NBT tree. - """ - if isinstance(obj, int): - return TAG_Int(obj) - - if isinstance(obj, str): - return TAG_String(obj) - - return obj - - def is_valid_structure_name(name: str, *, with_prefix: bool = False) -> bool: """ Validates the structure name. @@ -310,7 +269,7 @@ def __init__( self._palette.append(fill) @classmethod - def load(cls, file: BinaryIO): + def load(cls, file: BinaryIO) -> Self: """ Loads a structure from a file. @@ -328,20 +287,22 @@ def load(cls, file: BinaryIO): file File object to read. """ - nbt = NBTFile(file, little_endian=True) - size: tuple[int, int, int] = tuple(x.value for x in nbt["size"]) # type: ignore + nbt_root = nbtx.load(file, endianness="little") + assert isinstance(nbt_root, nbtx.TagCompound) + nbt_body = nbt_root.as_python()[""] + size: tuple[int, int, int] = tuple(x.value for x in nbt_body.value.fil(lambda x: x.name == "size", n=1)) struct = cls(size, None) struct.structure = np.array( - [_into_pyobj(x) for x in nbt["structure"]["block_indices"][0]], + [x for x in nbt_body["structure"]["block_indices"][0]], dtype=np.intc, ).reshape(size) struct._palette.extend( [ - Block(block["name"].value, **_into_pyobj(block["states"].value)) - for block in nbt["structure"]["palette"]["default"]["block_palette"] + Block(block["name"].value, **(block["states"])) + for block in nbt_body["structure"]["palette"]["default"]["block_palette"] ] ) @@ -385,7 +346,7 @@ def _get_str_array( Block.stringify, with_namespace=with_namespace, with_states=with_states ) ) - return vec(arr) + return cast(NDArray[Any], vec(arr)) def _add_block_to_palette(self, block: Block | None) -> int: """ @@ -442,65 +403,99 @@ def dump(self, file: BinaryIO) -> None: file File object to write to. """ - nbt = NBTFile( - value=dict( - format_version=TAG_Int(1), - size=TAG_List(TAG_Int, map(TAG_Int, self._size)), - structure=TAG_Compound( - dict( - block_indices=TAG_List( - TAG_List, - [ - TAG_List( - TAG_Int, map(TAG_Int, self.structure.flatten()) + nbt = nbtx.TagCompound[Any, Any]( + name="", + value=[ + nbtx.TagInt(name="format_version", value=1), + nbtx.TagList( + name="size", + child_id=nbtx.TagInt.id(), + value=[ + nbtx.TagInt("", self._size[0]), + nbtx.TagInt("", self._size[1]), + nbtx.TagInt("", self._size[2]), + ], + ), + nbtx.TagCompound( + name="structure", + value=[ + nbtx.TagList( + name="block_indices", + child_id=nbtx.TagList.id(), + value=[ + nbtx.TagList( + name="", + child_id=nbtx.TagInt.id(), + value=[nbtx.TagInt("", i.item()) for i in self.structure.flatten()], ), - TAG_List( - TAG_Int, - map(TAG_Int, repeat(-1, self.structure.size)), + nbtx.TagList( + name="", + child_id=nbtx.TagInt.id(), + value=list(repeat(nbtx.TagInt("", -1), self.structure.size)), ), ], ), - entities=TAG_List(TAG_Compound, []), - palette=TAG_Compound( - dict( - default=TAG_Compound( - dict( - block_palette=TAG_List( - TAG_Compound, - [ - TAG_Compound( - dict( - name=TAG_String( - block.identifier + nbtx.TagList( + name="entities", + child_id=nbtx.TagCompound.id(), + value=[], + ), + nbtx.TagCompound( + name="palette", + value=[ + nbtx.TagCompound( + name="default", + value=[ + nbtx.TagList( + name="block_palette", + child_id=nbtx.TagList.id(), + value=[ + nbtx.TagCompound( + name="", + value=[ + nbtx.TagString( + name="name", + value=block.identifier, ), - states=TAG_Compound( - { - state_name: _into_tag( - state_value + nbtx.TagCompound( + name="states", + value=[ + ( + nbtx.TagInt(state_name, state_value) if isinstance(state_value, int) else + nbtx.TagString(state_name, state_value) if isinstance(state_value, str) else + nbtx.TagByte(state_name, state_value) # TODO: confirm bools are stored as bytes ) for state_name, state_value in block.states.items() - } + ] ), - version=TAG_Int( - COMPABILITY_VERSION + nbtx.TagInt( + name="version", + value=COMPABILITY_VERSION ), - ) + ] ) for block in self._palette ], ), - block_position_data=TAG_Compound({}), - ) + nbtx.TagCompound(name="block_position_data", value=[]), + ] ) - ) + ] ), - ) + ], + ), + nbtx.TagList( + name="structure_world_origin", + value=[ + nbtx.TagInt(name="", value=0), + nbtx.TagInt(name="", value=0), + nbtx.TagInt(name="", value=0), + ], + child_id=nbtx.TagInt.id(), ), - structure_world_origin=TAG_List(TAG_Int, [0, 0, 0]), - ), - little_endian=True, + ], ) - nbt.save(file, little_endian=True) + nbtx.dump(nbt, file, endianness="little") def get_block(self, coordinate: Coordinate) -> Block | None: """ From 1d4c9de8c6fc2250513ab7a579b1ea9e687602db Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Wed, 17 Sep 2025 19:16:44 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=90=9B=20Fix=20loading=20and=20child?= =?UTF-8?q?=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcstructure/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mcstructure/__init__.py b/src/mcstructure/__init__.py index 1f88745..8749057 100644 --- a/src/mcstructure/__init__.py +++ b/src/mcstructure/__init__.py @@ -289,8 +289,8 @@ def load(cls, file: BinaryIO) -> Self: """ nbt_root = nbtx.load(file, endianness="little") assert isinstance(nbt_root, nbtx.TagCompound) - nbt_body = nbt_root.as_python()[""] - size: tuple[int, int, int] = tuple(x.value for x in nbt_body.value.fil(lambda x: x.name == "size", n=1)) + nbt_body = nbt_root.as_python() + size: tuple[int, int, int] = tuple(nbt_body["size"]) struct = cls(size, None) @@ -301,7 +301,7 @@ def load(cls, file: BinaryIO) -> Self: struct._palette.extend( [ - Block(block["name"].value, **(block["states"])) + Block(block["name"], **(block["states"])) for block in nbt_body["structure"]["palette"]["default"]["block_palette"] ] ) @@ -448,7 +448,7 @@ def dump(self, file: BinaryIO) -> None: value=[ nbtx.TagList( name="block_palette", - child_id=nbtx.TagList.id(), + child_id=nbtx.TagCompound.id(), value=[ nbtx.TagCompound( name="", From 9909031092fd80a45495a5d489e7f006f3bfb385 Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Wed, 17 Sep 2025 19:17:08 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rework=20repo=20struct?= =?UTF-8?q?ure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/README.md | 12 ------- examples/area.py | 5 ++- examples/copystruct.py | 7 ++-- examples/diagonal.py | 4 ++- examples/out/area.mcstructure | Bin 0 -> 2046 bytes examples/out/diagonal.mcstructure | Bin 0 -> 8266 bytes .../large_nether.mcstructure | Bin examples/out/simple.mcstructure | Bin 0 -> 382 bytes examples/pack/README.md | 30 ------------------ examples/pack/makepack.py | 21 ------------ examples/pack/src/manifest.json | 16 ---------- examples/read_and_display.py | 5 ++- examples/simple.py | 5 ++- examples/structures/barrel.mcstructure | Bin 1295 -> 0 bytes .../structures/caged_villager.mcstructure | Bin 6561 -> 0 bytes examples/structures/dirt_house.mcstructure | Bin 1551 -> 0 bytes examples/structures/scarecrow.mcstructure | Bin 573 -> 0 bytes poetry.lock | 8 ++--- pyproject.toml | 2 +- .../structures => samples}/barrel.mcstructure | Bin .../caged_villager.mcstructure | Bin .../cmdblock.mcstructure | Bin .../dirt_house.mcstructure | Bin .../large_nether.mcstructure | Bin .../scarecrow.mcstructure | Bin 25 files changed, 25 insertions(+), 90 deletions(-) delete mode 100644 examples/README.md create mode 100644 examples/out/area.mcstructure create mode 100644 examples/out/diagonal.mcstructure rename examples/{pack/src/structures => out}/large_nether.mcstructure (100%) create mode 100644 examples/out/simple.mcstructure delete mode 100644 examples/pack/README.md delete mode 100755 examples/pack/makepack.py delete mode 100644 examples/pack/src/manifest.json delete mode 100644 examples/structures/barrel.mcstructure delete mode 100644 examples/structures/caged_villager.mcstructure delete mode 100644 examples/structures/dirt_house.mcstructure delete mode 100644 examples/structures/scarecrow.mcstructure rename {examples/pack/src/structures => samples}/barrel.mcstructure (100%) rename {examples/pack/src/structures => samples}/caged_villager.mcstructure (100%) rename {examples/structures => samples}/cmdblock.mcstructure (100%) rename {examples/pack/src/structures => samples}/dirt_house.mcstructure (100%) rename {examples/structures => samples}/large_nether.mcstructure (100%) rename {examples/pack/src/structures => samples}/scarecrow.mcstructure (100%) diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 08b32ed..0000000 --- a/examples/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# `examples` directory - -```text -examples -├── *.py # example scripts -├── README.md # This file -├── pack/ # See pack/README.md -└── structures/ # example structures -``` - -Strcutures beginning with `my_` are generated from a script. Others are -created in Minecraft. diff --git a/examples/area.py b/examples/area.py index 04bfbdd..2656ed9 100644 --- a/examples/area.py +++ b/examples/area.py @@ -1,4 +1,7 @@ from mcstructure import Block, Structure +from pathlib import Path + +here = Path(__file__).parent struct = Structure( (6, 6, 6), # Size of the Structure 声明结构大小,注意这是大小,其坐标从0开始 @@ -13,5 +16,5 @@ print(struct._get_str_array(with_namespace=False, with_states=True)) # write into file 写入文件 -with open("structures/my_create.mcstructure", "wb") as f: +with here.joinpath("out/area.mcstructure").open("wb") as f: struct.dump(f) diff --git a/examples/copystruct.py b/examples/copystruct.py index f4d73ba..959e01c 100644 --- a/examples/copystruct.py +++ b/examples/copystruct.py @@ -1,7 +1,10 @@ from mcstructure import Structure +from pathlib import Path -with open("examples/structures/large_nether.mcstructure", "rb") as f: +here = Path(__file__).parent + +with here.joinpath("../samples/large_nether.mcstructure").open("rb") as f: struct = Structure.load(f) -with open("examples/pack/src/structures/large_nether.mcstructure", "wb") as f: +with here.joinpath("out/large_nether.mcstructure").open("wb") as f: struct.dump(f) diff --git a/examples/diagonal.py b/examples/diagonal.py index 9f9f6de..bd388c5 100644 --- a/examples/diagonal.py +++ b/examples/diagonal.py @@ -1,5 +1,7 @@ from mcstructure import Block, Structure +from pathlib import Path +here = Path(__file__).parent BLOCK = Block("minecraft:grass") @@ -13,5 +15,5 @@ .set_block((5, 5, 5), BLOCK) ) -with open("examples/my_diagonal.mcstructure", "wb") as f: +with here.joinpath("out/diagonal.mcstructure").open("wb") as f: struct.dump(f) diff --git a/examples/out/area.mcstructure b/examples/out/area.mcstructure new file mode 100644 index 0000000000000000000000000000000000000000..e23ed789aa4103a4765801cb6f1852753bcb7a90 GIT binary patch literal 2046 zcmeH|QEJ0542IRYw6&po*v1aC%NQ+DhFD@3S=pc`?QJ$YEN!6lv4lYcG6Mg`zh%Vd z0L&vaQ94xrMUps%6#&>ZBtDWcx!z3DSx72ZR0k$Kq%1AGp4M7_y%wmJTc(xLXJ84k#!qQ*vbl-4+hmk}Bn@rUv^~&j=iM z3Ica)2-wjcIu29`Hu{DvZj;H0N(sysM$)>c)hK_N$XR0W16yyNa?gM8J;anfW_^u{ Y#ixC}AliRNX=@*aKR669FN$;g0_e)Dxc~qF literal 0 HcmV?d00001 diff --git a/examples/out/diagonal.mcstructure b/examples/out/diagonal.mcstructure new file mode 100644 index 0000000000000000000000000000000000000000..4dbd58116182dc86e7e4c18c05fa44807281fc8e GIT binary patch literal 8266 zcmeI&QAz_b5C-6>DYglypx|M=Oo+R)WpFnsnOOx->@74Ywt9oU`~!i3ObC2$;ZGoP z_cWEdB#ZA{RZ89vk>8TaCp*{r@PBzytLtEEWq(Jna~a;E{qtYkA3sW+$5^TyB%eM8^odLT0D`~Li~s-t literal 0 HcmV?d00001 diff --git a/examples/pack/src/structures/large_nether.mcstructure b/examples/out/large_nether.mcstructure similarity index 100% rename from examples/pack/src/structures/large_nether.mcstructure rename to examples/out/large_nether.mcstructure diff --git a/examples/out/simple.mcstructure b/examples/out/simple.mcstructure new file mode 100644 index 0000000000000000000000000000000000000000..016b1bb421b35ae6c1cd109751a59ae53d9235b1 GIT binary patch literal 382 zcmaiw(Q3mm3`LbWtg)ec=te)LKV#HHDWZuTWTk`s{AP!y&C9k676KoMd*uMk8}v~| zRDUB$976>FwuZ!CWK8Z~HqTi|Ds!u|kln+1i0##L=(we13uu~L{!-b#K;Qq!Hb6mH znUX6D-3}%kNR@KeQIGSW4-4y0vCz~IaHKtq9H@o8lDi)vi_&kySj52gS3ST%Z$1KD{KS0E85dZ)H literal 0 HcmV?d00001 diff --git a/examples/pack/README.md b/examples/pack/README.md deleted file mode 100644 index c8ee44b..0000000 --- a/examples/pack/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# `pack` directory - -This directory is a Minecraft behaviour pack which can be used to test -structures. - -```text -pack -├── README.md # This file -├── build/ # Built behaviour pack -├── makepack.py # Builds the pack -└── src/ # Content of the pack - ├── manifest.json - └── structures -``` - -## Usage - -Update new structures with the following three commands. - -```bash -mkdir -p build # Create output directory if not present yet -rm -rf src/structures/*.mcstructure # Remove old structures -cp ../structures/*.mcstructure src/structures # Add new structures -./makepack.py # Build the pack -``` - -## TODO - -* use Makefile -* version will never change, remove it from the build diff --git a/examples/pack/makepack.py b/examples/pack/makepack.py deleted file mode 100755 index 7cc8134..0000000 --- a/examples/pack/makepack.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -# copied from https://github.com/phoenixr-codes/transparent-pumpkin/blob/main/build.py - -import json -import pathlib -import zipfile - -NAME = "mcstructure-python" - -src = pathlib.Path("src") - -with (src / "manifest.json").open("r") as f: - data = json.load(f) - version = ".".join(map(str, data["header"]["version"])) - -print(f"building v{version} ...") - -with zipfile.ZipFile(f"build/{NAME}-v{version}.mcpack", "w") as archive: - for path in src.rglob("*"): - archive.write(path, arcname=path.relative_to(src)) diff --git a/examples/pack/src/manifest.json b/examples/pack/src/manifest.json deleted file mode 100644 index 2f8ec65..0000000 --- a/examples/pack/src/manifest.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "format_version": 2, - "header": { - "name": "mcstructure python", - "uuid": "41e1112d-279f-46fa-9a57-9d8452ef8eef", - "version": [1, 0, 0], - "min_engine_version": [1, 16, 0] - }, - "modules": [ - { - "type": "data", - "uuid": "a6510d79-f45e-4ae8-b398-964b410f3433", - "version": [1, 0, 0] - } - ] -} \ No newline at end of file diff --git a/examples/read_and_display.py b/examples/read_and_display.py index 1b488d3..48eae54 100644 --- a/examples/read_and_display.py +++ b/examples/read_and_display.py @@ -1,6 +1,9 @@ from mcstructure import Structure +from pathlib import Path -with open("structures/dirt_house.mcstructure", "rb") as f: +here = Path(__file__).parent + +with here.joinpath("../structures/dirt_house.mcstructure").open("rb") as f: struct = Structure.load(f) print(struct) diff --git a/examples/simple.py b/examples/simple.py index 6ae30ed..676270a 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,4 +1,7 @@ from mcstructure import Block, Structure +from pathlib import Path + +here = Path(__file__).parent struct = Structure( (2, 2, 2), # Size of the Structure 声明结构大小,注意这是大小,其坐标从0开始 @@ -9,5 +12,5 @@ struct.set_block((1, 1, 1), Block("minecraft:iron_block")) # write into file 写入文件 -with open("structures/my_simple.mcstructure", "wb") as f: +with here.joinpath("out/simple.mcstructure").open("wb") as f: struct.dump(f) diff --git a/examples/structures/barrel.mcstructure b/examples/structures/barrel.mcstructure deleted file mode 100644 index 680102ecb3a794cb95eaa42c27690eddcf3c6c24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1295 zcmb7^-%7(U6vj{1Zfi0&b;1yz!fP*81TPdp#LJSkXCo|4Mw0qx&*w|{G@69Xi4a&1 z6i#|hethSYeheU+z(SjbyYdx{6=+ij5^7o2!eHgL;zDEuj=MsVT;m%=W+1P6%eYn=Ga>L#LZg|6&*b90eyxyZfdhM!}lH~9t zwF5GO7j7R#wZi&&y=Sn)8F`A)Kb$d0F|xxMMT*g3A2h}2Fv}>#=rGG9#ps~pD8=ZY z<0QrCu+wRZK@R`6vlOGlKF(5%4*NL&&nS8z>H%Cu4HbO_gK0LSq#T3`(>e0=6ZHa?y!(sfbh-^%2_QK!`C%r}{q^>?TnDgVq1;~?~9TDLE4 z;>~3b&z?W!a^6<{cX_gxZ|SMmNX8~Kp-dYrc*Uh=sDJL%tJKs(r5hDAI#oJUS!`%! zm0Dsb`jV(tp~qr~f^&sF7wK{6pUL1!@;~$PN`sol2>KYwpS_24MYfww}L>8Gx;wc*LRi_PMj0UQ@ly{*tZkyn7gN9)&eJy(CS3MO) zYUF7V1F)$$dPN+SW@mBFTcrk``uU4o9J(grp1+ugyIK+#W&T+%rt!}|{dy|K)}~5` zr!QYVdp08`*UEOyYW03uj5rtGP@T&m<26z1EzA3HNnREQQjdg@dQK{~;_g&STl+Rz zZkD9ZDrmR8Y$kL2c6R}w*ulH`z7@yj!;+}881AAAOZBu&!_UuLr1`Wean-Cz3qvbVFl z@9*vG-SR))xpQaV-`Twr$l&AN?cLoySzV|5B8po1(ZEr3-=j~%J|>nLj#~!k^X&zN zzLgq%nrTxAZO7+uR7zibHonN$X!qvs{>{Bx&N`tli(;uoHhw^GbRc!Rl@kl>`9q~lORFTo zH12g&N3TU39O4mFgf@ZY5X(>T3|(-qV6Z;>4C=A6cmV!m0V*Y;P9jDzxm)yD3f;Y! z+iLfN*Y)tucZ?W8RJk&ezxIMEYTZ+rQ5373yXe%|?|Dn62SDje%DAIvpt$ zVKn?P7(=Z}J{xxxn zocVRXTk_tx>aiQa+|nxq-l*-u>SVOQMUBZV;jK`HRdQDjbuMeAkoEG}gvERlStXdP zfN6ahOzV(8kH=etseH-d{0NTSAe|g1-@w(rEAMk`W_RD zsk10hDkmcnuxn=T3vl8T9q##s)>Pjj=Ix+nC-TcI)HZXI;ugX%hi>7|7Iyi#a?9=e zlfp0lFe&W)n+pUE4KjT>o{#wFD1M+3r3Fp}O>0P zoT>vbUUEJ_m1*#CBrYKFD83)+)ZFKVapVNPeG>WzZmq}~Ay{tO81eVEH4P~Dk@PvD zz3{B5q#xxFP3kwlNL36&vU9S<@N5v9hwIE0zQ%@uhD^dW-U|Tmd1%9W#;(ga{)PwU zmEV>tzvBu5USGtVG0qa3`=R<31UbDhMjl$lX^pS58?-&mq@F_n2h-m9ZHKluXuD`g zfvb0Gm9|BID5y@`xlZrWc2^F>d8o9PAhtztc@0e>4Sg@g{^E7HMLS=pu9qeV40~wr zD-osd(@p}@C|>e%SwEp2iyNouvLuNvJYNkH6~kb?J`6kIC5toZAy~sZz6{T$7a>vl zBibIJ4a?637jpuNfk+P+v?Cq%IZtq%TRPGlz5v@RY^Ges5WamwN%@`xh_tKjs1ZGynhq diff --git a/examples/structures/dirt_house.mcstructure b/examples/structures/dirt_house.mcstructure deleted file mode 100644 index d57c5291e6fe8fafefc5a401eda7d094079517e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1551 zcmeHH+it@k40V=Xl1y!-O}l){eg=`?W@sryKvr$vx99!RW@n|1qNcCg!;VD2a5xwo zy8uYnu(fU{gLp;fwKWL<#EQ>%vVr0wt5lTC|#k?uZWZ? z`b4nO7)-hZK5^Wb&mP3aHY3{HM~vsjT5L0hJf4fUjPqthGq`W jEJacV$wABSPc>WYt!tF9PS@J>)%}`^)CVQTCl}^#{zYNK diff --git a/examples/structures/scarecrow.mcstructure b/examples/structures/scarecrow.mcstructure deleted file mode 100644 index a0f8e812d7bcb1d7c2530b69ef71e808b48cc908..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 573 zcma)4fojAc3{BUvs~xw&Zj61)envb^kI+_&rdPJ_+pp|9HkGwk4h|MjVtl^5BsKu$ zBec#Bisc!7AZH5z5ETUa#IlV3vQoSe5ZF(eCy!ze$KL5qk~O3;h`R(9aZ8ra$x<+1 z_~O8w_t72Ce)VjU1<8?8vTETbZf**G$dfZ+4>#v9-=hqgW-Nc)d(m X&d&EnI!`CE`7czlv0rg$--+}WxEQK2 diff --git a/poetry.lock b/poetry.lock index 9987206..e793a1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -567,14 +567,14 @@ testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0, [[package]] name = "nbtx" -version = "0.3.0" +version = "0.3.2" description = "Minecraft NBT parser" optional = false python-versions = ">=3.13" groups = ["main"] files = [ - {file = "nbtx-0.3.0-py3-none-any.whl", hash = "sha256:0171d7e581a5a58471eac56e4bec39108254bb191d55bae4fc622a76970c0c46"}, - {file = "nbtx-0.3.0.tar.gz", hash = "sha256:22154e73125b1fd86dd8a2da17670a2d87c7c3c65d040de7476b71507ef3689c"}, + {file = "nbtx-0.3.2-py3-none-any.whl", hash = "sha256:118e7ee1598d83dbc14d44912c0546fec290f4101eb2224fdf8766024dc4796a"}, + {file = "nbtx-0.3.2.tar.gz", hash = "sha256:b7934e673317d6f9751941b96a75424eb1ea29938b8da26a67a549a6715c541a"}, ] [package.extras] @@ -1093,4 +1093,4 @@ docs = ["Sphinx", "furo", "myst-parser", "sphinx-copybutton"] [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "7bd114d1fce906ab16c76a65e9066b46cd2678a4e7bd5e86f57facd9888b898f" +content-hash = "6393f4d88b5ee273460ab50b8fb11482096892a3d14f6a600d8119dd6f665401" diff --git a/pyproject.toml b/pyproject.toml index dfa0bf4..42a4ca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.0.1b6" description = "Read and write Minecraft .mcstructure files." dependencies = [ "numpy==2.3.0", - "nbtx==0.3.1", + "nbtx==0.3.2", ] requires-python = ">=3.13" readme = "README.md" diff --git a/examples/pack/src/structures/barrel.mcstructure b/samples/barrel.mcstructure similarity index 100% rename from examples/pack/src/structures/barrel.mcstructure rename to samples/barrel.mcstructure diff --git a/examples/pack/src/structures/caged_villager.mcstructure b/samples/caged_villager.mcstructure similarity index 100% rename from examples/pack/src/structures/caged_villager.mcstructure rename to samples/caged_villager.mcstructure diff --git a/examples/structures/cmdblock.mcstructure b/samples/cmdblock.mcstructure similarity index 100% rename from examples/structures/cmdblock.mcstructure rename to samples/cmdblock.mcstructure diff --git a/examples/pack/src/structures/dirt_house.mcstructure b/samples/dirt_house.mcstructure similarity index 100% rename from examples/pack/src/structures/dirt_house.mcstructure rename to samples/dirt_house.mcstructure diff --git a/examples/structures/large_nether.mcstructure b/samples/large_nether.mcstructure similarity index 100% rename from examples/structures/large_nether.mcstructure rename to samples/large_nether.mcstructure diff --git a/examples/pack/src/structures/scarecrow.mcstructure b/samples/scarecrow.mcstructure similarity index 100% rename from examples/pack/src/structures/scarecrow.mcstructure rename to samples/scarecrow.mcstructure From d3cc62367669d9da941002b6a42718add34dabcc Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sat, 20 Sep 2025 23:53:54 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=92=9A=20Update=20python=20version=20?= =?UTF-8?q?for=20readthedocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 31ca60b..0e8ca9f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.11" + python: "3.13" python: install: From a9fd0ee694fda58b565500a8e451eefb09393d86 Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sun, 21 Sep 2025 01:02:48 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=90=9B=20Prefill=20with=20air?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/diagonal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/diagonal.py b/examples/diagonal.py index bd388c5..4d00714 100644 --- a/examples/diagonal.py +++ b/examples/diagonal.py @@ -5,7 +5,7 @@ BLOCK = Block("minecraft:grass") -struct = Structure((10, 10, 10), BLOCK) +struct = Structure((10, 10, 10), Block("minecraft:air")) ( struct.set_block((0, 0, 0), BLOCK) .set_block((1, 1, 1), BLOCK) From 0eb2d3431140057aa0ba9793e4cf1530318b52aa Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sun, 21 Sep 2025 01:03:02 +0200 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8=20Add=20packer=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/packer.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 scripts/packer.py diff --git a/scripts/packer.py b/scripts/packer.py new file mode 100644 index 0000000..8afdfdf --- /dev/null +++ b/scripts/packer.py @@ -0,0 +1,58 @@ +""" +Packs mcstructure files into a usable MCBE add-on. +""" + +from datetime import datetime +import json +from pathlib import Path +from uuid import uuid4 +from tempfile import TemporaryDirectory +import zipfile + +here = Path(__file__).parent +root = here.parent +logo = root / "logo.png" +structures = root / "examples/out" + +def main() -> None: + timestamp = datetime.now().strftime("%d.%m %H:%M") + name = f"mcstructure pack - {timestamp}" + manifest = { + "format_version": 2, + "header": { + "name": name, + "description": "github.com/phoenixr-codes/mcstructure", + "uuid": str(uuid4()), + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0], + }, + "modules": [ + { + "type": "data", + "uuid": str(uuid4()), + "version": [1, 0, 0] + } + ], + "metadata": { + "product_type": "addon" + } + } + with TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + with (temp_dir / "manifest.json").open("w") as f: + json.dump(manifest, f) + with (temp_dir / "pack_icon.png").open("wb") as f: + f.write(logo.read_bytes()) + structures_dir = temp_dir / "structures" + structures_dir.mkdir() + for structure_path in structures.glob("*.mcstructure"): + destination = structures_dir / structure_path.name + destination.write_bytes(structure_path.read_bytes()) + print(f"adding {structure_path}") + with zipfile.ZipFile((root / name.replace(".", "_")).with_suffix(".mcpack"), "w") as archive: + for source_path in temp_dir.glob("**/*"): + print(f"archiving {source_path}") + archive.write(source_path, arcname=source_path.relative_to(temp_dir)) + +if __name__ == "__main__": + main() From fa53d45e4e48b02ec119b5624d8458a6701df3c2 Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sun, 21 Sep 2025 01:03:57 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=8D=B1=20Build=20mcstructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/out/diagonal.mcstructure | Bin 8266 -> 8313 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/out/diagonal.mcstructure b/examples/out/diagonal.mcstructure index 4dbd58116182dc86e7e4c18c05fa44807281fc8e..44e8ffc91e6d1e7a89fdbdc63673db8b3208c5e8 100644 GIT binary patch delta 82 zcmX@*@Y7*J7$f7x#NCYe$j@w>zwzJVmttaIVBlb3$V<#kjt%u7u!N=z%UO3W;p M{6=1K@)h|q0CJ!kpa1{> delta 34 ocmezAaLQpq*k)nQ$84JwIIn?-=Zu@b@!#TSW@KQP%%o5b0QFo9F8}}l From 6820180a17ddcd588663297a25650f1305a3aaef Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sun, 21 Sep 2025 01:04:15 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=99=88=20Add=20Minecraft=20related=20?= =?UTF-8?q?build=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2f75543..9b0d858 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# artifacts +*.mcpack +*.mcaddon + # editors & IDEs .vscode @@ -160,4 +164,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ From 0f0cd0ac3e5247dd23c4e6d3fddd0be504e45529 Mon Sep 17 00:00:00 2001 From: Jonas da Silva Date: Sun, 21 Sep 2025 01:16:24 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=8E=A8=20Format=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/packer.py | 18 +++++------- src/mcstructure/__init__.py | 57 ++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/scripts/packer.py b/scripts/packer.py index 8afdfdf..3f2e2da 100644 --- a/scripts/packer.py +++ b/scripts/packer.py @@ -14,6 +14,7 @@ logo = root / "logo.png" structures = root / "examples/out" + def main() -> None: timestamp = datetime.now().strftime("%d.%m %H:%M") name = f"mcstructure pack - {timestamp}" @@ -26,16 +27,8 @@ def main() -> None: "version": [1, 0, 0], "min_engine_version": [1, 21, 0], }, - "modules": [ - { - "type": "data", - "uuid": str(uuid4()), - "version": [1, 0, 0] - } - ], - "metadata": { - "product_type": "addon" - } + "modules": [{"type": "data", "uuid": str(uuid4()), "version": [1, 0, 0]}], + "metadata": {"product_type": "addon"}, } with TemporaryDirectory() as temp_dir_str: temp_dir = Path(temp_dir_str) @@ -49,10 +42,13 @@ def main() -> None: destination = structures_dir / structure_path.name destination.write_bytes(structure_path.read_bytes()) print(f"adding {structure_path}") - with zipfile.ZipFile((root / name.replace(".", "_")).with_suffix(".mcpack"), "w") as archive: + with zipfile.ZipFile( + (root / name.replace(".", "_")).with_suffix(".mcpack"), "w" + ) as archive: for source_path in temp_dir.glob("**/*"): print(f"archiving {source_path}") archive.write(source_path, arcname=source_path.relative_to(temp_dir)) + if __name__ == "__main__": main() diff --git a/src/mcstructure/__init__.py b/src/mcstructure/__init__.py index 8749057..9274a00 100644 --- a/src/mcstructure/__init__.py +++ b/src/mcstructure/__init__.py @@ -302,7 +302,9 @@ def load(cls, file: BinaryIO) -> Self: struct._palette.extend( [ Block(block["name"], **(block["states"])) - for block in nbt_body["structure"]["palette"]["default"]["block_palette"] + for block in nbt_body["structure"]["palette"]["default"][ + "block_palette" + ] ] ) @@ -426,12 +428,17 @@ def dump(self, file: BinaryIO) -> None: nbtx.TagList( name="", child_id=nbtx.TagInt.id(), - value=[nbtx.TagInt("", i.item()) for i in self.structure.flatten()], + value=[ + nbtx.TagInt("", i.item()) + for i in self.structure.flatten() + ], ), nbtx.TagList( name="", child_id=nbtx.TagInt.id(), - value=list(repeat(nbtx.TagInt("", -1), self.structure.size)), + value=list( + repeat(nbtx.TagInt("", -1), self.structure.size) + ), ), ], ), @@ -461,26 +468,46 @@ def dump(self, file: BinaryIO) -> None: name="states", value=[ ( - nbtx.TagInt(state_name, state_value) if isinstance(state_value, int) else - nbtx.TagString(state_name, state_value) if isinstance(state_value, str) else - nbtx.TagByte(state_name, state_value) # TODO: confirm bools are stored as bytes + nbtx.TagInt( + state_name, + state_value, + ) + if isinstance( + state_value, int + ) + else ( + nbtx.TagString( + state_name, + state_value, + ) + if isinstance( + state_value, + str, + ) + else nbtx.TagByte( + state_name, + state_value, + ) + ) # TODO: confirm bools are stored as bytes ) for state_name, state_value in block.states.items() - ] + ], ), nbtx.TagInt( name="version", - value=COMPABILITY_VERSION + value=COMPABILITY_VERSION, ), - ] + ], ) for block in self._palette ], ), - nbtx.TagCompound(name="block_position_data", value=[]), - ] + nbtx.TagCompound( + name="block_position_data", value=[] + ), + ], ) - ] + ], ), ], ), @@ -650,7 +677,11 @@ def combine(self, other: Structure, position: Coordinate = (0, 0, 0)) -> Structu # Calculate the new size needed to accommodate both structures end_pos = (ox + other._size[0], oy + other._size[1], oz + other._size[2]) - new_size = (max(self._size[0], end_pos[0]), max(self._size[1], end_pos[1]), max(self._size[2], end_pos[2])) + new_size = ( + max(self._size[0], end_pos[0]), + max(self._size[1], end_pos[1]), + max(self._size[2], end_pos[2]), + ) combined = Structure(new_size, None)