pylitematic 0.0.1__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pylitematic-0.0.1 → pylitematic-0.0.3}/PKG-INFO +4 -1
- {pylitematic-0.0.1 → pylitematic-0.0.3}/pyproject.toml +8 -2
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/__init__.py +2 -2
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/block_property.py +0 -66
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/block_state.py +12 -5
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/geometry.py +48 -10
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/region.py +153 -32
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/resource_location.py +4 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/schematic.py +9 -7
- pylitematic-0.0.3/src/pylitematic/test.py +71 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/PKG-INFO +4 -1
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/SOURCES.txt +2 -1
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/requires.txt +4 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/tests/test_block_property.py +0 -42
- pylitematic-0.0.3/tests/test_schematic.py +40 -0
- pylitematic-0.0.1/src/pylitematic/test.py +0 -23
- {pylitematic-0.0.1 → pylitematic-0.0.3}/README.md +0 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/setup.cfg +0 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/dependency_links.txt +0 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/top_level.txt +0 -0
- {pylitematic-0.0.1 → pylitematic-0.0.3}/tests/test_resource_location.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pylitematic
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: Load, modify, and save Litematica schematics
|
5
5
|
Author-email: Boscawinks <bosca.winks@gmx.de>
|
6
6
|
License: GPL-3.0-only
|
@@ -13,6 +13,9 @@ Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: bitpacking>=0.1.0
|
14
14
|
Requires-Dist: nbtlib>=2.0.4
|
15
15
|
Requires-Dist: numpy>=2.2.6
|
16
|
+
Requires-Dist: twos>=0.0.1
|
17
|
+
Provides-Extra: dev
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
16
19
|
|
17
20
|
# pylitematic
|
18
21
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "pylitematic"
|
7
|
-
version = "0.0.
|
7
|
+
version = "0.0.3"
|
8
8
|
description = "Load, modify, and save Litematica schematics"
|
9
9
|
authors = [
|
10
10
|
{ name="Boscawinks", email="bosca.winks@gmx.de" }
|
@@ -12,7 +12,8 @@ authors = [
|
|
12
12
|
dependencies = [
|
13
13
|
"bitpacking >=0.1.0",
|
14
14
|
"nbtlib >=2.0.4",
|
15
|
-
"numpy >=2.2.6"
|
15
|
+
"numpy >=2.2.6",
|
16
|
+
"twos >= 0.0.1"
|
16
17
|
]
|
17
18
|
readme = "README.md"
|
18
19
|
requires-python = ">=3.10.12"
|
@@ -21,6 +22,11 @@ classifiers = [
|
|
21
22
|
"Programming Language :: Python :: 3",
|
22
23
|
]
|
23
24
|
|
25
|
+
[project.optional-dependencies]
|
26
|
+
dev = [
|
27
|
+
"pytest"
|
28
|
+
]
|
29
|
+
|
24
30
|
[project.urls]
|
25
31
|
Homepage = "https://github.com/boscawinks/pylitematic"
|
26
32
|
Repository = "https://github.com/boscawinks/pylitematic"
|
@@ -1,7 +1,7 @@
|
|
1
|
-
__version__ = "0.0.
|
1
|
+
__version__ = "0.0.3"
|
2
2
|
|
3
3
|
from .block_state import BlockState
|
4
4
|
from .geometry import BlockPosition, Size3D
|
5
5
|
from .region import Region
|
6
|
-
from .resource_location import
|
6
|
+
from .resource_location import BlockId
|
7
7
|
from .schematic import Schematic
|
@@ -14,72 +14,6 @@ ENUM_VALUE_REGEX: str = r"[a-z]+(_[a-z]+)*" # snake case
|
|
14
14
|
ENUM_VALUE_PATTERN: re.Pattern = re.compile(ENUM_VALUE_REGEX)
|
15
15
|
|
16
16
|
|
17
|
-
class Property():
|
18
|
-
|
19
|
-
__slots__ = ("_name", "_value")
|
20
|
-
|
21
|
-
def __init__(self, name: str, value: Any | PropertyValue) -> None:
|
22
|
-
if not PROPERTY_NAME_PATTERN.fullmatch(name):
|
23
|
-
raise ValueError(f"Invalid property name {name!r}")
|
24
|
-
self._name = name
|
25
|
-
|
26
|
-
if not isinstance(value, PropertyValue):
|
27
|
-
value = PropertyValue.value_factory(value=value)
|
28
|
-
self._value = value
|
29
|
-
|
30
|
-
def __str__(self) -> str:
|
31
|
-
return f"{self._name}={self._value}"
|
32
|
-
|
33
|
-
def __repr__(self) -> str:
|
34
|
-
return (
|
35
|
-
f"{type(self).__name__}("
|
36
|
-
f"name: {self._name}, value: {self._value!r})")
|
37
|
-
|
38
|
-
def __eq__(self, other: Any) -> bool:
|
39
|
-
if not isinstance(other, Property):
|
40
|
-
return NotImplemented
|
41
|
-
return (self.name, self.value) == (other.name, other.value)
|
42
|
-
|
43
|
-
def __lt__(self, other: Any) -> bool:
|
44
|
-
if not isinstance(other, Property):
|
45
|
-
return NotImplemented
|
46
|
-
return (self.name, self.value) < (other.name, other.value)
|
47
|
-
|
48
|
-
@property
|
49
|
-
def name(self) -> str:
|
50
|
-
return self._name
|
51
|
-
|
52
|
-
@property
|
53
|
-
def value(self) -> Any:
|
54
|
-
return self._value.get()
|
55
|
-
|
56
|
-
@value.setter
|
57
|
-
def value(self, value: Any) -> None:
|
58
|
-
self._value.set(value)
|
59
|
-
|
60
|
-
def to_string(self) -> str:
|
61
|
-
return str(self)
|
62
|
-
|
63
|
-
@staticmethod
|
64
|
-
def from_string(string: str, value: str | None = None) -> Property:
|
65
|
-
if value is None:
|
66
|
-
# tread string as "name=value"
|
67
|
-
try:
|
68
|
-
string, value = string.split("=")
|
69
|
-
except ValueError as exc:
|
70
|
-
raise ValueError(f"Invalid property string {string!r}") from exc
|
71
|
-
return Property(name=string, value=PropertyValue.from_string(value))
|
72
|
-
|
73
|
-
def to_nbt(self) -> tuple[str, nbtlib.String]:
|
74
|
-
# return nbtlib.Compound(Name=nbtlib.String(self._name), Value=self._value.to_nbt()})
|
75
|
-
return self._name, self._value.to_nbt()
|
76
|
-
|
77
|
-
@staticmethod
|
78
|
-
def from_nbt(name: str, nbt: nbtlib.String) -> Property:
|
79
|
-
# return Property.from_string(name=nbt["Name"], value=str(nbt["Value"]))
|
80
|
-
return Property.from_string(string=name, value=str(nbt))
|
81
|
-
|
82
|
-
|
83
17
|
class Properties(dict):
|
84
18
|
|
85
19
|
def __init__(self, *args, **kwargs):
|
@@ -4,7 +4,7 @@ from copy import deepcopy
|
|
4
4
|
from nbtlib import Compound
|
5
5
|
from typing import Any, Iterator
|
6
6
|
|
7
|
-
from .resource_location import
|
7
|
+
from .resource_location import BlockId
|
8
8
|
from .block_property import Properties
|
9
9
|
|
10
10
|
|
@@ -12,8 +12,15 @@ class BlockState:
|
|
12
12
|
|
13
13
|
__slots__ = ("_id", "_props")
|
14
14
|
|
15
|
-
def __init__(self, _id: str, **props: Any) -> None:
|
16
|
-
|
15
|
+
def __init__(self, _id: str | BlockId, **props: Any) -> None:
|
16
|
+
if isinstance(_id, str):
|
17
|
+
_id = BlockId.from_string(_id)
|
18
|
+
elif not isinstance(_id, BlockId):
|
19
|
+
raise TypeError(
|
20
|
+
f"'_id' has to be str or {BlockId.__name__}, got"
|
21
|
+
f" {type(_id).__name__}")
|
22
|
+
self._id: BlockId = _id
|
23
|
+
|
17
24
|
self._props: Properties = Properties(**props)
|
18
25
|
|
19
26
|
def __getitem__(self, name: str) -> Any:
|
@@ -58,8 +65,8 @@ class BlockState:
|
|
58
65
|
f"id: {self._id!r}, props: {self._props!r})")
|
59
66
|
|
60
67
|
@property
|
61
|
-
def id(self) ->
|
62
|
-
return
|
68
|
+
def id(self) -> BlockId:
|
69
|
+
return self._id
|
63
70
|
|
64
71
|
def props(self) -> Iterator[tuple[str, Any]]:
|
65
72
|
return self._props.items()
|
@@ -19,11 +19,14 @@ class Vec3i:
|
|
19
19
|
def __str__(self) -> str:
|
20
20
|
return str(list(self))
|
21
21
|
|
22
|
+
def __len__(self) -> int:
|
23
|
+
return 3
|
24
|
+
|
22
25
|
def __add__(self, other) -> Vec3i:
|
23
26
|
arr = np.array(self)
|
24
|
-
|
27
|
+
other = self._to_array(other)
|
25
28
|
try:
|
26
|
-
result = arr +
|
29
|
+
result = arr + other
|
27
30
|
except Exception:
|
28
31
|
return NotImplemented
|
29
32
|
return type(self)(*result.astype(int))
|
@@ -33,9 +36,9 @@ class Vec3i:
|
|
33
36
|
|
34
37
|
def __sub__(self, other) -> Vec3i:
|
35
38
|
arr = np.array(self)
|
36
|
-
|
39
|
+
other = self._to_array(other)
|
37
40
|
try:
|
38
|
-
result = arr -
|
41
|
+
result = arr - other
|
39
42
|
except Exception:
|
40
43
|
return NotImplemented
|
41
44
|
return type(self)(*result.astype(int))
|
@@ -48,13 +51,34 @@ class Vec3i:
|
|
48
51
|
return NotImplemented
|
49
52
|
return type(self)(*result.astype(int))
|
50
53
|
|
51
|
-
def __mul__(self,
|
52
|
-
|
53
|
-
|
54
|
+
def __mul__(self, other) -> Vec3i:
|
55
|
+
arr = np.array(self)
|
56
|
+
other = self._to_array(other)
|
57
|
+
try:
|
58
|
+
result = arr * other
|
59
|
+
except Exception:
|
60
|
+
return NotImplemented
|
61
|
+
return type(self)(*result.astype(int))
|
62
|
+
|
63
|
+
def __rmul__(self, other) -> Vec3i:
|
64
|
+
return self.__mul__(other)
|
65
|
+
|
66
|
+
def __floordiv__(self, other) -> Vec3i:
|
67
|
+
arr = np.array(self)
|
68
|
+
other = self._to_array(other)
|
69
|
+
try:
|
70
|
+
result = arr // other
|
71
|
+
except Exception:
|
72
|
+
return NotImplemented
|
73
|
+
return type(self)(*result.astype(int))
|
54
74
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
75
|
+
def __rfloordiv__(self, other) -> Vec3i:
|
76
|
+
arr = np.array(self)
|
77
|
+
try:
|
78
|
+
result = other // arr
|
79
|
+
except Exception:
|
80
|
+
return NotImplemented
|
81
|
+
return type(self)(*result.astype(int))
|
58
82
|
|
59
83
|
def __neg__(self) -> Vec3i:
|
60
84
|
return type(self)(-self._a, -self._b, -self._c)
|
@@ -167,3 +191,17 @@ class Size3D(Vec3i):
|
|
167
191
|
return (
|
168
192
|
f"{type(self).__name__}("
|
169
193
|
f"width={self.width}, height={self.height}, length={self.length})")
|
194
|
+
|
195
|
+
def end(self, axis: tuple[int,...] | int | None = None) -> BlockPosition:
|
196
|
+
limit = self - np.sign(self)
|
197
|
+
|
198
|
+
if axis is None:
|
199
|
+
return BlockPosition(*limit)
|
200
|
+
|
201
|
+
if not isinstance(axis, tuple):
|
202
|
+
axis = (axis, )
|
203
|
+
|
204
|
+
ret = np.zeros_like(limit, dtype=int)
|
205
|
+
for ax in axis:
|
206
|
+
ret[ax] = limit[ax]
|
207
|
+
return BlockPosition(*ret)
|
@@ -1,12 +1,15 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from bitpacking import bitpack, bitunpack
|
4
|
+
from functools import cached_property
|
4
5
|
import nbtlib
|
5
6
|
import numpy as np
|
6
7
|
import twos
|
8
|
+
from typing import Iterator
|
7
9
|
|
8
10
|
from .block_state import BlockState
|
9
11
|
from .geometry import BlockPosition, Size3D
|
12
|
+
from .resource_location import BlockId
|
10
13
|
|
11
14
|
|
12
15
|
AIR = BlockState("air")
|
@@ -22,27 +25,54 @@ class Region:
|
|
22
25
|
self._size: Size3D = Size3D(*size)
|
23
26
|
|
24
27
|
self._palette: list[BlockState] = [AIR]
|
25
|
-
self._palette_map: dict[BlockState, int] = {AIR: 0}
|
26
|
-
self._blocks = np.zeros(abs(self._size))
|
27
|
-
|
28
|
-
|
29
|
-
self.
|
30
|
-
self.
|
28
|
+
self._palette_map: dict[BlockState, int] = {AIR: 0}
|
29
|
+
self._blocks = np.zeros(abs(self._size), dtype=int)
|
30
|
+
|
31
|
+
# TODO: Add support
|
32
|
+
self._entities = nbtlib.List[nbtlib.Compound]()
|
33
|
+
self._tile_entities = nbtlib.List[nbtlib.Compound]()
|
34
|
+
self._block_ticks = nbtlib.List[nbtlib.Compound]()
|
35
|
+
self._fluid_ticks = nbtlib.List[nbtlib.Compound]()
|
36
|
+
|
37
|
+
def __contains__(self, item) -> bool:
|
38
|
+
if isinstance(item, BlockPosition):
|
39
|
+
return all(self.lower <= item) and all(item <= self.upper)
|
40
|
+
elif isinstance(item, BlockState):
|
41
|
+
index = self._palette_map.get(item)
|
42
|
+
if index is None:
|
43
|
+
return False
|
44
|
+
return np.any(self._blocks == index)
|
45
|
+
elif isinstance(item, BlockId):
|
46
|
+
return any(
|
47
|
+
(bs.id == item and np.any(self._blocks == idx))
|
48
|
+
for bs, idx in self._palette_map.items())
|
49
|
+
else:
|
50
|
+
return False
|
31
51
|
|
32
|
-
def
|
33
|
-
|
34
|
-
index = self._blocks[key.x, key.y, key.z]
|
35
|
-
return self._palette[index]
|
52
|
+
def __eq__(self, other) -> bool:
|
53
|
+
palette = np.array(self._palette, dtype=object)
|
36
54
|
|
37
|
-
|
55
|
+
if isinstance(other, BlockState):
|
56
|
+
matches = np.array([state == other for state in palette])
|
57
|
+
return matches[self._blocks]
|
58
|
+
|
59
|
+
elif isinstance(other, BlockId):
|
60
|
+
matches = np.array([state.id == other for state in palette])
|
61
|
+
return matches[self._blocks]
|
62
|
+
|
63
|
+
else:
|
64
|
+
return NotImplemented
|
65
|
+
|
66
|
+
def __getitem__(self, key):
|
67
|
+
index = self._key_to_index(key)
|
68
|
+
indices = self._blocks[index]
|
38
69
|
if np.isscalar(indices):
|
39
70
|
return self._palette[indices]
|
40
71
|
|
41
72
|
return np.array(self._palette, dtype=object)[indices]
|
42
73
|
|
43
74
|
def __setitem__(self, key, value) -> None:
|
44
|
-
|
45
|
-
key = key.to_tuple()
|
75
|
+
index = self._key_to_index(key)
|
46
76
|
|
47
77
|
if isinstance(value, list):
|
48
78
|
value = np.array(value, dtype=object)
|
@@ -52,11 +82,10 @@ class Region:
|
|
52
82
|
if value not in self._palette_map:
|
53
83
|
self._palette_map[value] = len(self._palette)
|
54
84
|
self._palette.append(value)
|
55
|
-
index = self._palette_map[value]
|
56
|
-
self._blocks[key] = index
|
85
|
+
self._blocks[index] = self._palette_map[value]
|
57
86
|
|
58
87
|
elif isinstance(value, np.ndarray):
|
59
|
-
if value.shape != self._blocks[
|
88
|
+
if value.shape != self._blocks[index].shape:
|
60
89
|
raise ValueError(
|
61
90
|
"Shape mismatch between assigned array and target slice")
|
62
91
|
|
@@ -69,11 +98,49 @@ class Region:
|
|
69
98
|
self._palette.append(state)
|
70
99
|
idx.append(self._palette_map[state])
|
71
100
|
index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
|
72
|
-
self._blocks[
|
101
|
+
self._blocks[index] = index_array
|
73
102
|
else:
|
74
103
|
raise TypeError(
|
75
104
|
"Value must be a BlockState or a list of BlockStates")
|
76
105
|
|
106
|
+
def _expand_index(self, index):
|
107
|
+
if not isinstance(index, tuple):
|
108
|
+
index = (index,)
|
109
|
+
ndim = self._blocks.ndim
|
110
|
+
result = []
|
111
|
+
for item in index:
|
112
|
+
if item is Ellipsis:
|
113
|
+
result.extend([slice(None)] * (ndim - len(index) + 1))
|
114
|
+
else:
|
115
|
+
result.append(item)
|
116
|
+
while len(result) < ndim:
|
117
|
+
result.append(slice(None))
|
118
|
+
return tuple(result)
|
119
|
+
|
120
|
+
def _to_internal(self, pos):
|
121
|
+
index = []
|
122
|
+
for i, item in enumerate(pos):
|
123
|
+
offset = self.lower[i]
|
124
|
+
if isinstance(item, int):
|
125
|
+
index.append(item - offset)
|
126
|
+
elif isinstance(item, slice):
|
127
|
+
start = item.start - offset if item.start is not None else None
|
128
|
+
stop = item.stop - offset if item.stop is not None else None
|
129
|
+
index.append(slice(start, stop, item.step))
|
130
|
+
else:
|
131
|
+
index.append(item)
|
132
|
+
return tuple(index)
|
133
|
+
|
134
|
+
def _from_internal(self, index: tuple[int, int, int]) -> BlockPosition:
|
135
|
+
return self.lower + index
|
136
|
+
|
137
|
+
def _key_to_index(self, key):
|
138
|
+
if isinstance(key, BlockPosition):
|
139
|
+
index = tuple(key)
|
140
|
+
else:
|
141
|
+
index = self._expand_index(key)
|
142
|
+
return self._to_internal(index)
|
143
|
+
|
77
144
|
def compact_palette(self) -> None:
|
78
145
|
idx = np.unique(self._blocks)
|
79
146
|
# always include minecraft:air in a palette
|
@@ -123,14 +190,25 @@ class Region:
|
|
123
190
|
def size(self) -> Size3D:
|
124
191
|
return self._size
|
125
192
|
|
193
|
+
@cached_property
|
194
|
+
def sign(self) -> Size3D:
|
195
|
+
return Size3D(*np.sign(self._size))
|
196
|
+
|
126
197
|
@property
|
127
198
|
def origin(self) -> BlockPosition:
|
128
199
|
return self._origin
|
129
200
|
|
130
|
-
@
|
201
|
+
@cached_property
|
202
|
+
def limit(self) -> BlockPosition:
|
203
|
+
return self._origin + self._size.end()
|
204
|
+
|
205
|
+
@cached_property
|
206
|
+
def start(self) -> BlockPosition:
|
207
|
+
return BlockPosition(0, 0, 0)
|
208
|
+
|
209
|
+
@cached_property
|
131
210
|
def end(self) -> BlockPosition:
|
132
|
-
return self.
|
133
|
-
self._size > 0, self._size - 1, self._size + 1)
|
211
|
+
return self._size.end()
|
134
212
|
|
135
213
|
@property
|
136
214
|
def width(self) -> int:
|
@@ -149,33 +227,76 @@ class Region:
|
|
149
227
|
return np.prod(self.shape).item()
|
150
228
|
|
151
229
|
@property
|
152
|
-
def
|
230
|
+
def block_count(self) -> int:
|
231
|
+
# TODO: Add filter BlockState and rename to count()
|
153
232
|
return np.count_nonzero(self._blocks)
|
154
233
|
|
155
234
|
@property
|
156
235
|
def shape(self) -> tuple[int, int, int]:
|
157
236
|
return self._blocks.shape
|
158
237
|
|
159
|
-
@
|
238
|
+
@cached_property
|
160
239
|
def lower(self) -> BlockPosition:
|
161
|
-
return BlockPosition(*np.min((self.
|
240
|
+
return BlockPosition(*np.min((self.start, self.end), axis=0))
|
162
241
|
|
163
|
-
@
|
242
|
+
@cached_property
|
164
243
|
def upper(self) -> BlockPosition:
|
165
|
-
return BlockPosition(*np.max((self.
|
244
|
+
return BlockPosition(*np.max((self.start, self.end), axis=0))
|
166
245
|
|
167
|
-
@
|
246
|
+
@cached_property
|
168
247
|
def bounds(self) -> tuple[BlockPosition, BlockPosition]:
|
169
248
|
return self.lower, self.upper
|
170
249
|
|
171
|
-
|
172
|
-
|
250
|
+
@cached_property
|
251
|
+
def global_lower(self) -> BlockPosition:
|
252
|
+
return BlockPosition(*np.min((self.origin, self.limit), axis=0))
|
173
253
|
|
174
|
-
|
175
|
-
|
254
|
+
@cached_property
|
255
|
+
def global_upper(self) -> BlockPosition:
|
256
|
+
return BlockPosition(*np.max((self.origin, self.limit), axis=0))
|
176
257
|
|
177
|
-
|
178
|
-
|
258
|
+
@cached_property
|
259
|
+
def global_bounds(self) -> tuple[BlockPosition, BlockPosition]:
|
260
|
+
return self.global_lower, self.global_upper
|
261
|
+
|
262
|
+
def blocks(
|
263
|
+
self,
|
264
|
+
include: BlockState | list[BlockState] | None = None,
|
265
|
+
exclude: BlockState | list[BlockState] | None = None,
|
266
|
+
ignore_props: bool = False,
|
267
|
+
) -> Iterator[tuple[BlockPosition, BlockState]]:
|
268
|
+
if isinstance(include, BlockState):
|
269
|
+
include = [include]
|
270
|
+
if isinstance(exclude, BlockState):
|
271
|
+
exclude = [exclude]
|
272
|
+
|
273
|
+
for z, y, x in np.ndindex(self.shape[::-1]):
|
274
|
+
pos = BlockPosition(x, y, z) * self.sign
|
275
|
+
state = self[pos]
|
276
|
+
|
277
|
+
if exclude:
|
278
|
+
if not ignore_props:
|
279
|
+
if state in exclude:
|
280
|
+
continue
|
281
|
+
else:
|
282
|
+
if any(state.id == ex.id for ex in exclude):
|
283
|
+
continue
|
284
|
+
|
285
|
+
if include:
|
286
|
+
if not ignore_props:
|
287
|
+
if state not in include:
|
288
|
+
continue
|
289
|
+
else:
|
290
|
+
if not any(state.id == s.id for s in include):
|
291
|
+
continue
|
292
|
+
|
293
|
+
yield pos, state
|
294
|
+
|
295
|
+
def to_global(self, local_pos: BlockPosition) -> BlockPosition:
|
296
|
+
return self._origin + local_pos
|
297
|
+
|
298
|
+
def to_local(self, global_pos: BlockPosition) -> BlockPosition:
|
299
|
+
return global_pos - self._origin
|
179
300
|
|
180
301
|
def to_nbt(self) -> nbtlib.Compound:
|
181
302
|
nbt = nbtlib.Compound()
|
@@ -8,8 +8,8 @@ import time
|
|
8
8
|
import twos
|
9
9
|
from typing import Iterator
|
10
10
|
|
11
|
-
from .geometry import BlockPosition, Size3D
|
12
|
-
from .region import Region
|
11
|
+
from pylitematic.geometry import BlockPosition, Size3D
|
12
|
+
from pylitematic.region import Region
|
13
13
|
|
14
14
|
|
15
15
|
DEFAULT_VERSION_MAJOR: int = 7
|
@@ -107,7 +107,7 @@ class Schematic:
|
|
107
107
|
lowers = []
|
108
108
|
uppers = []
|
109
109
|
for reg in self._regions.values():
|
110
|
-
lower, upper = reg.
|
110
|
+
lower, upper = reg.global_bounds
|
111
111
|
lowers.append(lower)
|
112
112
|
uppers.append(upper)
|
113
113
|
return (
|
@@ -121,7 +121,7 @@ class Schematic:
|
|
121
121
|
|
122
122
|
@property
|
123
123
|
def blocks(self) -> int:
|
124
|
-
return sum(reg.
|
124
|
+
return sum(reg.block_count for reg in self._regions.values())
|
125
125
|
|
126
126
|
@property
|
127
127
|
def region_count(self) -> int:
|
@@ -142,7 +142,6 @@ class Schematic:
|
|
142
142
|
|
143
143
|
def add_region(self, name: str, region: Region) -> None:
|
144
144
|
self._regions[name] = region
|
145
|
-
self._update()
|
146
145
|
|
147
146
|
def remove_region(self, name: str) -> Region:
|
148
147
|
return self._regions.pop(name)
|
@@ -214,7 +213,10 @@ class Schematic:
|
|
214
213
|
|
215
214
|
name = meta["Name"].unpack()
|
216
215
|
author = meta["Author"].unpack()
|
217
|
-
|
216
|
+
try:
|
217
|
+
desc = meta["Description"].unpack()
|
218
|
+
except KeyError:
|
219
|
+
desc = ""
|
218
220
|
|
219
221
|
preview = meta.get("PreviewImageData")
|
220
222
|
if preview is not None:
|
@@ -248,7 +250,7 @@ class Schematic:
|
|
248
250
|
schem = Schematic(
|
249
251
|
name=name,
|
250
252
|
author=author,
|
251
|
-
description=
|
253
|
+
description=desc,
|
252
254
|
regions=regions,
|
253
255
|
preview=preview,
|
254
256
|
version_major=major,
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import pathlib
|
3
|
+
from pylitematic import BlockPosition, BlockId, BlockState, Region, Schematic, Size3D
|
4
|
+
|
5
|
+
# path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/subs.litematic")
|
6
|
+
# path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/regions.litematic")
|
7
|
+
# path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/creeper_test.litematic")
|
8
|
+
# stone = BlockState.from_string("minecraft:stone")
|
9
|
+
# dirt = BlockState.from_string("minecraft:dirt")
|
10
|
+
# s = Schematic.load(path)
|
11
|
+
# print(f"{s.volume=} {s.size=} {s.bounds=}")
|
12
|
+
# for name, reg in s.regions():
|
13
|
+
# print(name)
|
14
|
+
# print(f"\t{reg.shape=} {reg.volume=} {reg.block_count=}")
|
15
|
+
# print(f"\t{reg.origin=!s} {reg.limit=!s}")
|
16
|
+
# print(f"\t{reg.start=!s} {reg.end=!s}")
|
17
|
+
# print(f"\t{reg.lower=!s} {reg.upper=!s} {reg.size=}")
|
18
|
+
# # print(f"\t{reg[..., 1, 0]}")
|
19
|
+
# # print(f"\t{reg[:][1][0]}")
|
20
|
+
# # print(f"\t{reg[BlockPosition(0, 1, 0)]}")
|
21
|
+
# # reg[1,1,1] = BlockState.from_string("minecraft:stone")
|
22
|
+
# # print("lol: ", reg[reg.end])
|
23
|
+
# # reg[0,:,0] = BlockState("minecraft:obsidian")
|
24
|
+
# # reg[0,:,0] = [dirt, stone, dirt]
|
25
|
+
# # print(reg[...,0])
|
26
|
+
# # print(reg[np.array([BlockPosition(0, 0, 0), BlockPosition(1, 1, 1)])])
|
27
|
+
# # print(f"\t{reg[:]}")
|
28
|
+
# # for pos, state in reg.blocks(exclude_air=True):
|
29
|
+
# # print(pos, state)
|
30
|
+
# # for pos, state in reg.blocks((BlockState("oak_log", axis="x"), BlockState("spruce_log", axis="z")), ignore_props=True):
|
31
|
+
# # reg[...,-1] = stone
|
32
|
+
# for pos, state in reg.blocks(exclude=BlockState("air")):
|
33
|
+
# print(f"\t{pos} {reg._to_internal(pos)}: {state}")
|
34
|
+
# for pos, state in reg.blocks(include=BlockState("lime_wool")):
|
35
|
+
# reg[pos] = BlockState("minecraft:blue_wool")
|
36
|
+
# for pos, state in reg.blocks(include=BlockState("tripwire"), ignore_props=True):
|
37
|
+
# reg[pos] = BlockState("minecraft:glass")
|
38
|
+
# # print(BlockState("oak_log", axis="x") in reg)
|
39
|
+
# # print(BlockPosition(1, 1, 0) in reg)
|
40
|
+
# # print(ResourceLocation("birch_log") in reg)
|
41
|
+
# # print(reg[0,:,2])
|
42
|
+
# s.save("/mnt/d/minecraft/schematics/Litematica/test/aaa.litematic")
|
43
|
+
|
44
|
+
air = BlockState("air")
|
45
|
+
stone = BlockState("stone")
|
46
|
+
dirt = BlockState("dirt")
|
47
|
+
grass = BlockState("grass_block")
|
48
|
+
cobble = BlockState("mossy_cobblestone")
|
49
|
+
snow = BlockState("snow_block")
|
50
|
+
pumpkin = BlockState("carved_pumpkin", facing="west")
|
51
|
+
|
52
|
+
ground = Region(size=Size3D(16, 9, 16), origin=BlockPosition(0, 0, 0))
|
53
|
+
ground[:,:5,:] = stone
|
54
|
+
ground[:,5:8,:] = dirt
|
55
|
+
ground[:,8:,:] = grass
|
56
|
+
|
57
|
+
boulder = Region(size=(4, 4, 4), origin=ground.origin+[6, ground.height, 6])
|
58
|
+
boulder[:] = cobble
|
59
|
+
|
60
|
+
# snow_man = Region(size=(1, 3, 1), origin=boulder.origin+[1, boulder.height, 1])
|
61
|
+
snow_man = Region(size=(-1, -3, -1), origin=boulder.origin+[1, boulder.height+2, 1])
|
62
|
+
snow_man[:] = snow
|
63
|
+
snow_man[0,snow_man.upper.y,0] = pumpkin
|
64
|
+
|
65
|
+
schem = Schematic(name="scene", author="Boscawinks", description="A simple scene")
|
66
|
+
schem.add_region("ground", ground)
|
67
|
+
schem.add_region("boulder", boulder)
|
68
|
+
schem.add_region("snow_man", snow_man)
|
69
|
+
schem.save(f"/mnt/d/minecraft/schematics/Litematica/test/{schem.name}.litematic")
|
70
|
+
|
71
|
+
print(snow_man == snow)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pylitematic
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: Load, modify, and save Litematica schematics
|
5
5
|
Author-email: Boscawinks <bosca.winks@gmx.de>
|
6
6
|
License: GPL-3.0-only
|
@@ -13,6 +13,9 @@ Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: bitpacking>=0.1.0
|
14
14
|
Requires-Dist: nbtlib>=2.0.4
|
15
15
|
Requires-Dist: numpy>=2.2.6
|
16
|
+
Requires-Dist: twos>=0.0.1
|
17
|
+
Provides-Extra: dev
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
16
19
|
|
17
20
|
# pylitematic
|
18
21
|
|
@@ -6,7 +6,6 @@ from pylitematic.block_property import (
|
|
6
6
|
BooleanValue,
|
7
7
|
EnumValue,
|
8
8
|
IntegerValue,
|
9
|
-
Property,
|
10
9
|
PropertyValue,
|
11
10
|
)
|
12
11
|
|
@@ -50,44 +49,3 @@ def test_value():
|
|
50
49
|
|
51
50
|
with pytest.raises(TypeError):
|
52
51
|
PropertyValue.value_factory(value=3.14)
|
53
|
-
|
54
|
-
|
55
|
-
def test_property():
|
56
|
-
# TODO
|
57
|
-
# * test sorting of properties
|
58
|
-
|
59
|
-
def check_prop(
|
60
|
-
prop: Property,
|
61
|
-
name: str,
|
62
|
-
target: Any,
|
63
|
-
string: str,
|
64
|
-
nbt: tuple[str, String],
|
65
|
-
) -> None:
|
66
|
-
assert prop.name == name
|
67
|
-
assert prop.value == target
|
68
|
-
prop.value = target
|
69
|
-
assert str(prop) == string
|
70
|
-
assert prop.to_string() == string
|
71
|
-
assert prop.to_nbt() == nbt
|
72
|
-
|
73
|
-
proto_props = [
|
74
|
-
("enabled", True, "true", String("true"), "enabled=true"),
|
75
|
-
("facing", "north", "north", String("north"), "facing=north"),
|
76
|
-
("age", 42, "42", String("42"), "age=42"),
|
77
|
-
]
|
78
|
-
|
79
|
-
for name, target, string, nbt, prop_string in proto_props:
|
80
|
-
prop = Property(name=name, value=target)
|
81
|
-
check_prop(prop, name, target, prop_string, (name, nbt))
|
82
|
-
prop = Property.from_string(string=name, value=string)
|
83
|
-
check_prop(prop, name, target, prop_string, (name, nbt))
|
84
|
-
prop = Property.from_string(string=prop_string)
|
85
|
-
check_prop(prop, name, target, prop_string, (name, nbt))
|
86
|
-
prop = Property.from_nbt(name=name, nbt=nbt)
|
87
|
-
check_prop(prop, name, target, prop_string, (name, nbt))
|
88
|
-
|
89
|
-
with pytest.raises(TypeError):
|
90
|
-
Property(name="float", value=3.14)
|
91
|
-
|
92
|
-
with pytest.raises(ValueError):
|
93
|
-
Property(name="Uppercase", value=1)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from pylitematic import BlockPosition, BlockState, Region, Schematic, Size3D
|
2
|
+
|
3
|
+
|
4
|
+
def test_schematic(tmp_path):
|
5
|
+
air = BlockState("air")
|
6
|
+
stone = BlockState("stone")
|
7
|
+
dirt = BlockState("dirt")
|
8
|
+
grass = BlockState("grass_block")
|
9
|
+
cobble = BlockState("mossy_cobblestone")
|
10
|
+
snow = BlockState("snow_block")
|
11
|
+
pumpkin = BlockState("carved_pumpkin", facing="west")
|
12
|
+
|
13
|
+
ground = Region(size=Size3D(16, 9, 16))
|
14
|
+
ground[:,:5,:] = stone
|
15
|
+
ground[:,5:8,:] = dirt
|
16
|
+
ground[:,8:,:] = grass
|
17
|
+
|
18
|
+
boulder = Region(
|
19
|
+
size=(4, 4, 4), origin=ground.origin+[6, ground.height, 6])
|
20
|
+
for pos, block in boulder.blocks():
|
21
|
+
if block == air:
|
22
|
+
boulder[pos] = cobble
|
23
|
+
# ^ since region is empty, this is equivalent to boulder[:] = cobble
|
24
|
+
|
25
|
+
snow_man = Region(
|
26
|
+
size=(1, 3, 1), origin=boulder.origin+[1, boulder.height, 1])
|
27
|
+
snow_man[:] = snow
|
28
|
+
snow_man[BlockPosition(0, 2, 0)] = pumpkin
|
29
|
+
|
30
|
+
schem = Schematic(
|
31
|
+
name="scene", author="Boscawinks", description="A simple scene")
|
32
|
+
schem.add_region("ground", ground)
|
33
|
+
schem.add_region("boulder", boulder)
|
34
|
+
schem.add_region("snow_man", snow_man)
|
35
|
+
|
36
|
+
save_path = tmp_path / f"{schem.name}.litematic"
|
37
|
+
schem.save(save_path)
|
38
|
+
|
39
|
+
copy = schem.load(save_path)
|
40
|
+
assert copy.to_nbt() == schem.to_nbt()
|
@@ -1,23 +0,0 @@
|
|
1
|
-
import pathlib
|
2
|
-
from pylitematic import BlockPosition, BlockState, Schematic
|
3
|
-
|
4
|
-
|
5
|
-
path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/subs.litematic")
|
6
|
-
path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/creeper_test.litematic")
|
7
|
-
path = pathlib.Path("/mnt/d/minecraft/schematics/Litematica/test/regions.litematic")
|
8
|
-
stone = BlockState.from_string("minecraft:stone")
|
9
|
-
dirt = BlockState.from_string("minecraft:dirt")
|
10
|
-
s = Schematic.load(path)
|
11
|
-
print(f"{s.volume=} {s.size=} {s.bounds=}")
|
12
|
-
for name, reg in s.regions():
|
13
|
-
print(name)
|
14
|
-
print(f"\t{reg.shape=} {reg.volume=} {reg.blocks=}")
|
15
|
-
print(f"\t{reg.origin=!s} {reg.end=!s}")
|
16
|
-
print(f"\t{reg.lower=} {reg.upper=!s} {reg.size=}")
|
17
|
-
# print(f"\t{reg[..., 1, 0]}")
|
18
|
-
# print(f"\t{reg[:][1][0]}")
|
19
|
-
# print(f"\t{reg[BlockPosition(0, 1, 0)]}")
|
20
|
-
reg[1,1,1] = BlockState.from_string("minecraft:stone")
|
21
|
-
reg[0,:,0] = [dirt, stone, dirt]
|
22
|
-
print(f"\t{reg[:]}")
|
23
|
-
s.save("/mnt/d/minecraft/schematics/Litematica/test/pylitematic.litematic")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|