pylitematic 0.0.0__py3-none-any.whl → 0.0.2__py3-none-any.whl

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/__init__.py CHANGED
@@ -1 +1,7 @@
1
- __version__ = "0.0.0"
1
+ __version__ = "0.0.2"
2
+
3
+ from .block_state import BlockState
4
+ from .geometry import BlockPosition, Size3D
5
+ from .region import Region
6
+ from .resource_location import ResourceLocation
7
+ from .schematic import Schematic
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import json
5
+ import nbtlib
6
+ import re
7
+ from typing import Any
8
+
9
+
10
+ PROPERTY_NAME_REGEX: str = r"[a-z][a-z0-9_]*"
11
+ PROPERTY_NAME_PATTERN: re.Pattern = re.compile(PROPERTY_NAME_REGEX)
12
+
13
+ ENUM_VALUE_REGEX: str = r"[a-z]+(_[a-z]+)*" # snake case
14
+ ENUM_VALUE_PATTERN: re.Pattern = re.compile(ENUM_VALUE_REGEX)
15
+
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
+ class Properties(dict):
84
+
85
+ def __init__(self, *args, **kwargs):
86
+ props = {}
87
+ for name, value in dict(*args, **kwargs).items():
88
+ self.validate_name(name)
89
+ props[name] = PropertyValue.value_factory(value)
90
+ super().__init__(props)
91
+
92
+ def __getitem__(self, key):
93
+ return super().__getitem__(key).get()
94
+
95
+ def __setitem__(self, key, value):
96
+ if key not in self:
97
+ self.validate_name(key)
98
+ super().__setitem__(key, PropertyValue.value_factory(value))
99
+ else:
100
+ super().__getitem__(key).set(value)
101
+
102
+ def __lt__(self, other: Any) -> bool:
103
+ if not isinstance(other, Properties):
104
+ return NotImplemented
105
+ return sorted(self.items()) < sorted(other.items())
106
+
107
+ def __hash__(self) -> int:
108
+ return hash(tuple(sorted(self)))
109
+
110
+ def __str__(self) -> str:
111
+ props_str = [f"{n}={v}" for n, v in sorted(super().items())]
112
+ return f"[{','.join(props_str)}]"
113
+
114
+ def __repr__(self) -> str:
115
+ props_reps = [f"{n}: {v!r}" for n, v in sorted(super().items())]
116
+ return f"{type(self).__name__}({', '.join(props_reps)})"
117
+
118
+ def get(self, key, default=None):
119
+ value = super().get(key, None)
120
+ if value is None:
121
+ return default
122
+ return value.get()
123
+
124
+ def setdefault(self, key, default=None):
125
+ if key not in self:
126
+ self[key] = default
127
+ return self[key]
128
+
129
+ def update(self, *args, **kwargs):
130
+ for name, value in dict(*args, **kwargs).items():
131
+ self[name] = value
132
+
133
+ def pop(self, key, default=None):
134
+ value = super().pop(key, None)
135
+ if value is None:
136
+ return default
137
+ return value.get()
138
+
139
+ def popitem(self):
140
+ name, value = super().popitem()
141
+ return name, value.get()
142
+
143
+ def values(self):
144
+ for value in super().values():
145
+ yield value.get()
146
+
147
+ def items(self):
148
+ for key, value in super().items():
149
+ yield key, value.get()
150
+
151
+ @staticmethod
152
+ def is_valid_name(name: str) -> bool:
153
+ return PROPERTY_NAME_PATTERN.fullmatch(name) is not None
154
+
155
+ @staticmethod
156
+ def validate_name(name: str) -> None:
157
+ if not Properties.is_valid_name(name=name):
158
+ raise ValueError(f"Invalid property name {name!r}")
159
+
160
+ def to_string(self) -> str:
161
+ return str(self)
162
+
163
+ @staticmethod
164
+ def from_string(string: str) -> Properties:
165
+ if string in ("", "[]"):
166
+ return Properties()
167
+
168
+ if not (string.startswith("[") and string.endswith("]")):
169
+ raise ValueError(f"Invalid properties string {string!r}")
170
+ string = string[1:-1]
171
+
172
+ props = {}
173
+ for prop in string.split(","):
174
+ try:
175
+ name, val_str = prop.split("=")
176
+ except ValueError as exc:
177
+ raise ValueError(f"Invalid property string {string!r}") from exc
178
+ if name in props:
179
+ ValueError(f"Duplicate property name {name!r}")
180
+ props[name] = PropertyValue.from_string(string=val_str).get()
181
+
182
+ return Properties(props)
183
+
184
+ def to_nbt(self) -> nbtlib.Compound:
185
+ return nbtlib.Compound(
186
+ {name: value.to_nbt() for name, value in sorted(super().items())})
187
+
188
+ @staticmethod
189
+ def from_nbt(nbt: nbtlib.Compound) -> Properties:
190
+ props = {}
191
+ for name, value in nbt.items():
192
+ props[name] = PropertyValue.from_nbt(nbt=value).get()
193
+ return Properties(props)
194
+
195
+
196
+ class PropertyValue(ABC):
197
+
198
+ __slots__ = ("_value")
199
+ __registry: dict[type, type[PropertyValue]] = {}
200
+
201
+ def __init_subclass__(cls, **kwargs) -> None:
202
+ super().__init_subclass__(**kwargs)
203
+ py_type = cls.python_type()
204
+ if py_type in cls.__registry:
205
+ raise ValueError(
206
+ f"Duplicate Value subclass for type {py_type.__name__!r}:"
207
+ f" {cls.__registry[py_type].__name__} vs {cls.__name__}")
208
+ cls.__registry[py_type] = cls
209
+
210
+ def __init__(self, value: Any) -> None:
211
+ self.set(value)
212
+
213
+ def __str__(self) -> str:
214
+ return json.dumps(self._value)
215
+
216
+ def __repr__(self) -> str:
217
+ return f"{type(self).__name__}({self._value!r})"
218
+
219
+ def __hash__(self) -> int:
220
+ return hash((self.__class__, self._value))
221
+
222
+ def __eq__(self, other: Any) -> bool:
223
+ if not isinstance(other, self.__class__):
224
+ return NotImplemented
225
+ return self._value == other._value
226
+
227
+ def __lt__(self, other: Any) -> bool:
228
+ if not isinstance(other, PropertyValue):
229
+ return NotImplemented
230
+ return self._value < other._value
231
+
232
+ @classmethod
233
+ @abstractmethod
234
+ def is_valid_value(cls, value: Any) -> bool:
235
+ ...
236
+
237
+ @classmethod
238
+ def validate_value(cls, value: Any) -> None:
239
+ if not isinstance(value, cls.python_type()):
240
+ raise TypeError(
241
+ f"{cls.__name__} expects value of type"
242
+ f" {cls.python_type().__name__}, got {type(value).__name__}"
243
+ f" ({value!r})")
244
+ if not cls.is_valid_value(value):
245
+ raise ValueError(f"Invalid value {value!r} for {cls.__name__}")
246
+
247
+ def get(self) -> Any:
248
+ return self._value
249
+
250
+ def set(self, value: Any) -> None:
251
+ self.validate_value(value=value)
252
+ self._value = self.python_type()(value)
253
+
254
+ @classmethod
255
+ @abstractmethod
256
+ def python_type(cls) -> type:
257
+ """Return the native Python type this Value corresponds to."""
258
+
259
+ @staticmethod
260
+ def value_factory(value: Any) -> PropertyValue:
261
+ reg = PropertyValue.__registry
262
+ sub_cls = reg.get(type(value))
263
+ if sub_cls is None:
264
+ opt_str = ", ".join(map(lambda x: x.__name__, reg))
265
+ raise TypeError(
266
+ f"No Value subclass registered for {type(value).__name__} value"
267
+ f" {value!r}. Classes registered for: {opt_str}")
268
+ return sub_cls(value)
269
+
270
+ def to_string(self) -> str:
271
+ return str(self)
272
+
273
+ @staticmethod
274
+ def from_string(string: str) -> PropertyValue:
275
+ try:
276
+ value = json.loads(string)
277
+ except json.JSONDecodeError:
278
+ value = string
279
+ return PropertyValue.value_factory(value)
280
+
281
+ def to_nbt(self) -> nbtlib.String:
282
+ return nbtlib.String(self)
283
+
284
+ @staticmethod
285
+ def from_nbt(nbt: nbtlib.String) -> PropertyValue:
286
+ return PropertyValue.from_string(str(nbt))
287
+
288
+
289
+ class BooleanValue(PropertyValue):
290
+
291
+ @classmethod
292
+ def is_valid_value(cls, value: Any) -> bool:
293
+ return True
294
+
295
+ @classmethod
296
+ def python_type(cls) -> type:
297
+ """Return the native Python type a BooleanValue corresponds to."""
298
+ return bool
299
+
300
+
301
+ class IntegerValue(PropertyValue):
302
+
303
+ @classmethod
304
+ def is_valid_value(cls, value: Any) -> bool:
305
+ return value >= 0
306
+
307
+ @classmethod
308
+ def python_type(cls) -> type:
309
+ """Return the native Python type an IntegerValue corresponds to."""
310
+ return int
311
+
312
+
313
+ class EnumValue(PropertyValue):
314
+
315
+ def __str__(self) -> str:
316
+ return self._value
317
+
318
+ @classmethod
319
+ def is_valid_value(cls, value: Any) -> bool:
320
+ return ENUM_VALUE_PATTERN.fullmatch(value) is not None
321
+
322
+ @classmethod
323
+ def python_type(cls) -> type:
324
+ """Return the native Python type an EnumValue corresponds to."""
325
+ return str
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from nbtlib import Compound
5
+ from typing import Any, Iterator
6
+
7
+ from .resource_location import ResourceLocation
8
+ from .block_property import Properties
9
+
10
+
11
+ class BlockState:
12
+
13
+ __slots__ = ("_id", "_props")
14
+
15
+ def __init__(self, _id: str, **props: Any) -> None:
16
+ self._id: ResourceLocation = ResourceLocation.from_string(_id)
17
+ self._props: Properties = Properties(**props)
18
+
19
+ def __getitem__(self, name: str) -> Any:
20
+ try:
21
+ return self._props[name]
22
+ except KeyError as exc:
23
+ raise KeyError(
24
+ f"{type(self).__name__} '{self}' does not"
25
+ f" have {name!r} property") from exc
26
+
27
+ # def __getattr__(self, name: str) -> Any:
28
+ # return self[name]
29
+
30
+ def __contains__(self, name: str) -> bool:
31
+ return name in self._props
32
+
33
+ def __len__(self) -> int:
34
+ return len(self._props)
35
+
36
+ def __eq__(self, other: Any) -> bool:
37
+ if isinstance(other, str):
38
+ other = BlockState.from_string(other)
39
+ elif not isinstance(other, BlockState):
40
+ return NotImplemented
41
+ return (self.id, self._props) == (other.id, other._props)
42
+
43
+ def __lt__(self, other: Any) -> bool:
44
+ if not isinstance(other, BlockState):
45
+ return NotImplemented
46
+ return (self.id, self._props) < (other.id, other._props)
47
+
48
+ def __hash__(self) -> int:
49
+ return hash((self._id, self._props))
50
+
51
+ def __str__(self) -> str:
52
+ props_str = "" if not self._props else str(self._props)
53
+ return f"{self.id}{props_str}"
54
+
55
+ def __repr__(self) -> str:
56
+ return (
57
+ f"{type(self).__name__}("
58
+ f"id: {self._id!r}, props: {self._props!r})")
59
+
60
+ @property
61
+ def id(self) -> str:
62
+ return str(self._id)
63
+
64
+ def props(self) -> Iterator[tuple[str, Any]]:
65
+ return self._props.items()
66
+
67
+ def to_string(self) -> str:
68
+ return str(self)
69
+
70
+ @staticmethod
71
+ def from_string(string: str) -> BlockState:
72
+ idx = string.find("[") # basic parsing to separate block:id[name=value]
73
+ if idx == -1:
74
+ id, props = string, ""
75
+ else:
76
+ id, props = string[:idx], string[idx:]
77
+
78
+ state = BlockState(id)
79
+ state._props = Properties.from_string(props)
80
+ return state
81
+
82
+ def to_nbt(self) -> Compound:
83
+ nbt = Compound()
84
+ nbt["Name"] = self._id.to_nbt()
85
+ if self._props:
86
+ nbt["Properties"] = self._props.to_nbt()
87
+ return nbt
88
+
89
+ @staticmethod
90
+ def from_nbt(nbt: Compound) -> BlockState:
91
+ state = BlockState(str(nbt["Name"]))
92
+ state._props = Properties.from_nbt(nbt.get("Properties", Compound()))
93
+ return state
94
+
95
+ def with_id(self, id: str) -> BlockState:
96
+ state = BlockState(id)
97
+ state._props = deepcopy(self._props)
98
+ return state
99
+
100
+ def with_props(self, **props: Any) -> BlockState:
101
+ state = BlockState(self.id)
102
+ new_props = deepcopy(self._props)
103
+ for name, value in props.items():
104
+ if value is None:
105
+ del new_props[name]
106
+ else:
107
+ new_props[name] = value
108
+ state._props = new_props
109
+ return state
110
+
111
+ def without_props(self) -> BlockState:
112
+ return BlockState(self.id)
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import nbtlib
5
+ import numpy as np
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Vec3i:
10
+ _a: int
11
+ _b: int
12
+ _c: int
13
+
14
+ def __post_init__(self) -> None:
15
+ object.__setattr__(self, "_a", self._to_int(self._a))
16
+ object.__setattr__(self, "_b", self._to_int(self._b))
17
+ object.__setattr__(self, "_c", self._to_int(self._c))
18
+
19
+ def __str__(self) -> str:
20
+ return str(list(self))
21
+
22
+ def __len__(self) -> int:
23
+ return 3
24
+
25
+ def __add__(self, other) -> Vec3i:
26
+ arr = np.array(self)
27
+ other = self._to_array(other)
28
+ try:
29
+ result = arr + other
30
+ except Exception:
31
+ return NotImplemented
32
+ return type(self)(*result.astype(int))
33
+
34
+ def __radd__(self, other) -> Vec3i:
35
+ return self.__add__(other)
36
+
37
+ def __sub__(self, other) -> Vec3i:
38
+ arr = np.array(self)
39
+ other = self._to_array(other)
40
+ try:
41
+ result = arr - other
42
+ except Exception:
43
+ return NotImplemented
44
+ return type(self)(*result.astype(int))
45
+
46
+ def __rsub__(self, other) -> Vec3i:
47
+ arr = np.array(self)
48
+ try:
49
+ result = other - arr
50
+ except Exception:
51
+ return NotImplemented
52
+ return type(self)(*result.astype(int))
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))
74
+
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))
82
+
83
+ def __neg__(self) -> Vec3i:
84
+ return type(self)(-self._a, -self._b, -self._c)
85
+
86
+ def __getitem__(self, index: int) -> int:
87
+ return self.to_tuple()[index]
88
+
89
+ def __iter__(self):
90
+ return iter(self.to_tuple())
91
+
92
+ def __abs__(self) -> Vec3i:
93
+ return type(self)(*np.abs(self))
94
+
95
+ def __lt__(self, other):
96
+ return np.array(self) < self._to_array(other)
97
+
98
+ def __le__(self, other):
99
+ return np.array(self) <= self._to_array(other)
100
+
101
+ def __gt__(self, other):
102
+ return np.array(self) > self._to_array(other)
103
+
104
+ def __ge__(self, other):
105
+ return np.array(self) >= self._to_array(other)
106
+
107
+ def __eq__(self, other):
108
+ return np.array(self) == self._to_array(other)
109
+
110
+ def __ne__(self, other):
111
+ return np.array(self) != self._to_array(other)
112
+
113
+ def __array__(self, dtype: type | None = None, copy: bool = True):
114
+ arr = np.array([self._a, self._b, self._c], dtype=dtype)
115
+ if copy:
116
+ return arr.copy()
117
+ else:
118
+ return arr
119
+
120
+ def _to_array(self, other):
121
+ if isinstance(other, Vec3i):
122
+ return np.array(other)
123
+ else:
124
+ return other
125
+
126
+ @staticmethod
127
+ def _to_int(value) -> int:
128
+ if isinstance(value, (int, np.integer)):
129
+ return int(value)
130
+ elif isinstance(value, float):
131
+ if value.is_integer():
132
+ return int(value)
133
+ raise TypeError(
134
+ f"{type(value).__name__} value {value!r} is not"
135
+ " int, numpy integer, or whole float")
136
+
137
+ def to_tuple(self) -> tuple[int, int, int]:
138
+ return (self._a, self._b, self._c)
139
+
140
+ @classmethod
141
+ def from_tuple(cls, t: tuple[int, int, int]) -> Vec3i:
142
+ return cls(*t)
143
+
144
+ def to_nbt(self) -> nbtlib.Compound:
145
+ return nbtlib.Compound({
146
+ "x": nbtlib.Int(self._a),
147
+ "y": nbtlib.Int(self._b),
148
+ "z": nbtlib.Int(self._c),
149
+ })
150
+
151
+ @classmethod
152
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Vec3i:
153
+ return cls(int(nbt["x"]), int(nbt["y"]), int(nbt["z"]))
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class BlockPosition(Vec3i):
158
+
159
+ @property
160
+ def x(self) -> int:
161
+ return self._a
162
+
163
+ @property
164
+ def y(self) -> int:
165
+ return self._b
166
+
167
+ @property
168
+ def z(self) -> int:
169
+ return self._c
170
+
171
+ def __repr__(self) -> str:
172
+ return f"{type(self).__name__}(x={self.x}, y={self.y}, z={self.z})"
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class Size3D(Vec3i):
177
+
178
+ @property
179
+ def width(self) -> int:
180
+ return self._a
181
+
182
+ @property
183
+ def height(self) -> int:
184
+ return self._b
185
+
186
+ @property
187
+ def length(self) -> int:
188
+ return self._c
189
+
190
+ def __repr__(self) -> str:
191
+ return (
192
+ f"{type(self).__name__}("
193
+ f"width={self.width}, height={self.height}, length={self.length})")
pylitematic/region.py ADDED
@@ -0,0 +1,328 @@
1
+ from __future__ import annotations
2
+
3
+ from bitpacking import bitpack, bitunpack
4
+ from functools import cached_property
5
+ import nbtlib
6
+ import numpy as np
7
+ import twos
8
+ from typing import Iterator
9
+
10
+ from .block_state import BlockState
11
+ from .geometry import BlockPosition, Size3D
12
+ from .resource_location import ResourceLocation
13
+
14
+
15
+ AIR = BlockState("air")
16
+
17
+
18
+ class Region:
19
+ def __init__(
20
+ self,
21
+ size: tuple[int, int, int] | Size3D,
22
+ origin: tuple[int, int, int] | BlockPosition = (0, 0, 0),
23
+ ):
24
+ self._origin: BlockPosition = BlockPosition(*origin)
25
+ self._size: Size3D = Size3D(*size)
26
+
27
+ self._palette: list[BlockState] = [AIR]
28
+ self._palette_map: dict[BlockState, int] = {AIR: 0}
29
+ self._blocks = np.zeros(abs(self._size), dtype=int)
30
+
31
+ self._entities: list[nbtlib.Compound] = []
32
+ self._tile_entities: list[nbtlib.Compound] = []
33
+ self._block_ticks: list[nbtlib.Compound] = []
34
+ self._fluid_ticks: list[nbtlib.Compound] = []
35
+
36
+ def __contains__(self, item) -> bool:
37
+ if isinstance(item, BlockPosition):
38
+ return all(self.lower <= item) and all(item <= self.upper)
39
+ elif isinstance(item, BlockState):
40
+ index = self._palette_map.get(item)
41
+ if index is None:
42
+ return False
43
+ return np.any(self._blocks == index)
44
+ elif isinstance(item, ResourceLocation):
45
+ return any(
46
+ (bs.id == item and np.any(self._blocks == idx))
47
+ for bs, idx in self._palette_map.items())
48
+ else:
49
+ return False
50
+
51
+ def _expand_index(self, index):
52
+ if not isinstance(index, tuple):
53
+ index = (index,)
54
+ ndim = self._blocks.ndim
55
+ result = []
56
+ for item in index:
57
+ if item is Ellipsis:
58
+ result.extend([slice(None)] * (ndim - len(index) + 1))
59
+ else:
60
+ result.append(item)
61
+ while len(result) < ndim:
62
+ result.append(slice(None))
63
+ return tuple(result)
64
+
65
+ def _to_internal(self, pos):
66
+ index = []
67
+ for i, item in enumerate(pos):
68
+ offset = self.lower[i]
69
+ if isinstance(item, int):
70
+ index.append(item - offset)
71
+ elif isinstance(item, slice):
72
+ start = item.start - offset if item.start is not None else None
73
+ stop = item.stop - offset if item.stop is not None else None
74
+ index.append(slice(start, stop, item.step))
75
+ else:
76
+ index.append(item)
77
+ return tuple(index)
78
+
79
+ def _from_internal(self, index: tuple[int, int, int]) -> BlockPosition:
80
+ return self.lower + index
81
+
82
+ def _key_to_index(self, key):
83
+ if isinstance(key, BlockPosition):
84
+ index = tuple(key)
85
+ else:
86
+ index = self._expand_index(key)
87
+ return self._to_internal(index)
88
+
89
+ def __getitem__(self, key):
90
+ index = self._key_to_index(key)
91
+ indices = self._blocks[index]
92
+ if np.isscalar(indices):
93
+ return self._palette[indices]
94
+
95
+ return np.array(self._palette, dtype=object)[indices]
96
+
97
+ def __setitem__(self, key, value) -> None:
98
+ index = self._key_to_index(key)
99
+
100
+ if isinstance(value, list):
101
+ value = np.array(value, dtype=object)
102
+
103
+ if isinstance(value, BlockState):
104
+ # assign single BlockState to slice
105
+ if value not in self._palette_map:
106
+ self._palette_map[value] = len(self._palette)
107
+ self._palette.append(value)
108
+ self._blocks[index] = self._palette_map[value]
109
+
110
+ elif isinstance(value, np.ndarray):
111
+ if value.shape != self._blocks[index].shape:
112
+ raise ValueError(
113
+ "Shape mismatch between assigned array and target slice")
114
+
115
+ # look up (or add) indices for all BlockStates
116
+ unique_states, xdi = np.unique(value, return_inverse=True)
117
+ idx = []
118
+ for state in unique_states:
119
+ if state not in self._palette_map:
120
+ self._palette_map[state] = len(self._palette)
121
+ self._palette.append(state)
122
+ idx.append(self._palette_map[state])
123
+ index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
124
+ self._blocks[index] = index_array
125
+ else:
126
+ raise TypeError(
127
+ "Value must be a BlockState or a list of BlockStates")
128
+
129
+ def compact_palette(self) -> None:
130
+ idx = np.unique(self._blocks)
131
+ # always include minecraft:air in a palette
132
+ if 0 not in idx:
133
+ idx = np.insert(idx, 0, 0)
134
+ index_map = {old: new for new, old in enumerate(idx)}
135
+
136
+ # compacted palette and mapping
137
+ palette = np.array(self._palette, dtype=object)[idx].tolist()
138
+ palette_map = {res: idx for idx, res in enumerate(palette)}
139
+
140
+ lookup = np.full(max(index_map) + 1, -1, dtype=int)
141
+ for old, new in index_map.items():
142
+ lookup[old] = new
143
+ self._blocks = lookup[self._blocks]
144
+
145
+ self._palette = palette
146
+ self._palette_map = palette_map
147
+
148
+ def _bits_per_state(self) -> int:
149
+ return max(2, (len(self._palette) - 1).bit_length())
150
+
151
+ def _decode_block_states(
152
+ self,
153
+ data: nbtlib.LongArray,
154
+ ) -> np.ndarray[int]:
155
+ states = bitunpack(
156
+ chunks=[twos.to_unsigned(x, 64) for x in data],
157
+ field_width=self._bits_per_state(),
158
+ chunk_width=64,
159
+ )
160
+ states = list(states)[:self.volume]
161
+ shape = (abs(self.height), abs(self.length), abs(self.width))
162
+ states = np.asarray(states, dtype=int).reshape(shape) # y,z,x
163
+ return states.transpose(2, 0, 1) # x,y,z
164
+
165
+ def _encode_block_states(self) -> nbtlib.LongArray:
166
+ states = self._blocks.transpose(1, 2, 0).ravel() # x,y,z to y,z,x
167
+ chunks = bitpack(
168
+ states.tolist(),
169
+ field_width=self._bits_per_state(),
170
+ chunk_width=64,
171
+ )
172
+ return nbtlib.LongArray([twos.to_signed(x, 64) for x in chunks])
173
+
174
+ @staticmethod
175
+ def inclusive_end(
176
+ size: Size3D,
177
+ pos: BlockPosition = BlockPosition(0, 0, 0),
178
+ ) -> BlockPosition:
179
+ return pos + (size - np.sign(size))
180
+ # return pos + np.where(size > 0, size - 1, size + 1)
181
+
182
+ @property
183
+ def size(self) -> Size3D:
184
+ return self._size
185
+
186
+ @cached_property
187
+ def sign(self) -> Size3D:
188
+ return Size3D(*np.sign(self._size))
189
+
190
+ @property
191
+ def origin(self) -> BlockPosition:
192
+ return self._origin
193
+
194
+ @cached_property
195
+ def limit(self) -> BlockPosition:
196
+ return self.inclusive_end(pos=self._origin, size=self._size)
197
+
198
+ @cached_property
199
+ def start(self) -> BlockPosition:
200
+ return BlockPosition(0, 0, 0)
201
+
202
+ @cached_property
203
+ def end(self) -> BlockPosition:
204
+ return self.inclusive_end(pos=self.start, size=self._size)
205
+
206
+ @property
207
+ def width(self) -> int:
208
+ return self._size.width
209
+
210
+ @property
211
+ def height(self) -> int:
212
+ return self._size.height
213
+
214
+ @property
215
+ def length(self) -> int:
216
+ return self._size.length
217
+
218
+ @property
219
+ def volume(self) -> int:
220
+ return np.prod(self.shape).item()
221
+
222
+ @property
223
+ def block_count(self) -> int:
224
+ # TODO: Add filter BlockState and rename to count()
225
+ return np.count_nonzero(self._blocks)
226
+
227
+ @property
228
+ def shape(self) -> tuple[int, int, int]:
229
+ return self._blocks.shape
230
+
231
+ @cached_property
232
+ def lower(self) -> BlockPosition:
233
+ return BlockPosition(*np.min((self.start, self.end), axis=0))
234
+
235
+ @cached_property
236
+ def upper(self) -> BlockPosition:
237
+ return BlockPosition(*np.max((self.start, self.end), axis=0))
238
+
239
+ @cached_property
240
+ def bounds(self) -> tuple[BlockPosition, BlockPosition]:
241
+ return self.lower, self.upper
242
+
243
+ @cached_property
244
+ def global_lower(self) -> BlockPosition:
245
+ return BlockPosition(*np.min((self.origin, self.limit), axis=0))
246
+
247
+ @cached_property
248
+ def global_upper(self) -> BlockPosition:
249
+ return BlockPosition(*np.max((self.origin, self.limit), axis=0))
250
+
251
+ @cached_property
252
+ def global_bounds(self) -> tuple[BlockPosition, BlockPosition]:
253
+ return self.global_lower, self.global_upper
254
+
255
+ def blocks(
256
+ self,
257
+ include: BlockState | list[BlockState] | None = None,
258
+ exclude: BlockState | list[BlockState] | None = None,
259
+ ignore_props: bool = False,
260
+ ) -> Iterator[tuple[BlockPosition, BlockState]]:
261
+ if isinstance(include, BlockState):
262
+ include = [include]
263
+ if isinstance(exclude, BlockState):
264
+ exclude = [exclude]
265
+
266
+ for z, y, x in np.ndindex(self.shape[::-1]):
267
+ pos = BlockPosition(x, y, z) * self.sign
268
+ state = self[pos]
269
+
270
+ if exclude:
271
+ if not ignore_props:
272
+ if state in exclude:
273
+ continue
274
+ else:
275
+ if any(state.id == ex.id for ex in exclude):
276
+ continue
277
+
278
+ if include:
279
+ if not ignore_props:
280
+ if state not in include:
281
+ continue
282
+ else:
283
+ if not any(state.id == s.id for s in include):
284
+ continue
285
+
286
+ yield pos, state
287
+
288
+ def to_global(self, local_pos: BlockPosition) -> BlockPosition:
289
+ return self._origin + local_pos
290
+
291
+ def to_local(self, global_pos: BlockPosition) -> BlockPosition:
292
+ return global_pos - self._origin
293
+
294
+ def to_nbt(self) -> nbtlib.Compound:
295
+ nbt = nbtlib.Compound()
296
+
297
+ nbt["Position"] = self._origin.to_nbt()
298
+ nbt["Size"] = self._size.to_nbt()
299
+
300
+ pal = [block.to_nbt() for block in self._palette]
301
+ nbt["BlockStatePalette"] = nbtlib.List[nbtlib.Compound](pal)
302
+ nbt["BlockStates"] = self._encode_block_states()
303
+
304
+ nbt["Entities"] = self._entities
305
+ nbt["TileEntities"] = self._tile_entities
306
+ nbt["PendingBlockTicks"] = self._block_ticks
307
+ nbt["PendingFluidTicks"] = self._fluid_ticks
308
+
309
+ return nbt
310
+
311
+ @staticmethod
312
+ def from_nbt(nbt: nbtlib.Compound) -> Region:
313
+ pos = BlockPosition.from_nbt(nbt["Position"])
314
+ size = Size3D.from_nbt(nbt["Size"])
315
+
316
+ region = Region(origin=pos, size=size)
317
+
318
+ region._palette = [
319
+ BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
320
+ region._palette_map = {bl: i for i, bl in enumerate(region._palette)}
321
+ region._blocks = region._decode_block_states(nbt["BlockStates"])
322
+
323
+ region._entities = nbt["Entities"]
324
+ region._tile_entities = nbt["TileEntities"]
325
+ region._block_ticks = nbt["PendingBlockTicks"]
326
+ region._fluid_ticks = nbt["PendingFluidTicks"]
327
+
328
+ return region
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import nbtlib
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ NAMESPACE_REGEX: str = r"[a-z0-9_.-]+"
10
+ NAMESPACE_PATTERN: re.Pattern = re.compile(NAMESPACE_REGEX)
11
+ DEFAULT_NAMESPACE: str = "minecraft"
12
+
13
+ PATH_REGEX: str = r"[a-z0-9_.-][a-z0-9_./-]*"
14
+ PATH_PATTERN: re.Pattern = re.compile(PATH_REGEX)
15
+
16
+ LOCATION_PATTERN: re.Pattern = re.compile(
17
+ rf"(?:(?P<namespace>{NAMESPACE_REGEX})?\:)?(?P<path>{PATH_REGEX})")
18
+
19
+ @dataclass(frozen=True, order=True)
20
+ class ResourceLocation:
21
+
22
+ path: str
23
+ namespace: str = ""
24
+
25
+ def __post_init__(self) -> None:
26
+ if not PATH_PATTERN.fullmatch(self.path):
27
+ raise ValueError(f"Invalid resource location path {self.path!r}")
28
+
29
+ if not self.namespace:
30
+ object.__setattr__(self, "namespace", DEFAULT_NAMESPACE)
31
+ elif not NAMESPACE_PATTERN.fullmatch(self.namespace):
32
+ raise ValueError(
33
+ f"Invalid resource location namespace {self.namespace!r}")
34
+
35
+ def __eq__(self, other: Any) -> bool:
36
+ if isinstance(other, ResourceLocation):
37
+ return (self.namespace, self.path) == (other.namespace, other.path)
38
+ elif isinstance(other, str):
39
+ try:
40
+ other = ResourceLocation.from_string(other)
41
+ return self == other
42
+ except ValueError:
43
+ return False
44
+ else:
45
+ return NotImplemented
46
+
47
+ def __str__(self) -> str:
48
+ return f"{self.namespace}:{self.path}"
49
+
50
+ def to_string(self) -> str:
51
+ return str(self)
52
+
53
+ @staticmethod
54
+ def from_string(string: str) -> ResourceLocation:
55
+ match = LOCATION_PATTERN.fullmatch(string)
56
+ if not match:
57
+ raise ValueError(f"Invalid resource location string {string!r}")
58
+
59
+ namespace = match.group("namespace")
60
+ path = match.group("path")
61
+
62
+ return ResourceLocation(path=path, namespace=namespace)
63
+
64
+ def to_nbt(self) -> nbtlib.String:
65
+ return nbtlib.String(self)
66
+
67
+ @staticmethod
68
+ def from_nbt(nbt: nbtlib.String) -> ResourceLocation:
69
+ return ResourceLocation.from_string(str(nbt))
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ import nbtlib
5
+ import numpy as np
6
+ import pathlib
7
+ import time
8
+ import twos
9
+ from typing import Iterator
10
+
11
+ from .geometry import BlockPosition, Size3D
12
+ from .region import Region
13
+
14
+
15
+ DEFAULT_VERSION_MAJOR: int = 7
16
+ DEFAULT_VERSION_MINOR: int = 1
17
+ DEFAULT_MC_VERSION: int = 4325
18
+
19
+ PREVIEW_CHANNELS: int = 4
20
+ PREVIEW_BIT_DEPTH: int = 8
21
+
22
+
23
+ def decode_image_data(data: nbtlib.IntArray) -> np.ndarray[int]:
24
+ bit_mask = (1 << PREVIEW_BIT_DEPTH) - 1
25
+
26
+ data = np.vectorize(twos.to_unsigned)(
27
+ data.unpack(), PREVIEW_CHANNELS * PREVIEW_BIT_DEPTH)
28
+ size = int(np.sqrt(len(data))) # TODO: Handle non-squares
29
+ shape = (size, size, PREVIEW_CHANNELS)
30
+ pixels = np.zeros(data.shape + (PREVIEW_CHANNELS,), dtype=int)
31
+ for i in range(PREVIEW_CHANNELS):
32
+ pixels[..., i] = (data >> (i * PREVIEW_BIT_DEPTH) & bit_mask)
33
+ pixels = np.asarray(pixels, dtype=np.uint32).reshape(shape)
34
+ return pixels[:, :, [2, 1, 0, 3]] # BGRA -> RGBA
35
+
36
+
37
+ def encode_image_data(img: np.ndarray[int]) -> nbtlib.IntArray:
38
+ bit_mask = (1 << PREVIEW_BIT_DEPTH) - 1
39
+
40
+ # TODO: Handle images w/o alpha channel
41
+ img = img[:, :, [2, 1, 0, 3]] # RGBA -> BGRA
42
+ img = img.reshape(-1, PREVIEW_CHANNELS)
43
+ data = np.zeros(len(img), dtype=int)
44
+ for i in range(PREVIEW_CHANNELS):
45
+ data |= ((img[..., i] & bit_mask )<< (i * PREVIEW_BIT_DEPTH))
46
+ data = np.vectorize(twos.to_signed)(
47
+ data, PREVIEW_CHANNELS * PREVIEW_BIT_DEPTH)
48
+ return nbtlib.IntArray(data)
49
+
50
+
51
+ class Schematic:
52
+ def __init__(
53
+ self,
54
+ name: str,
55
+ author: str = "",
56
+ description: str = "",
57
+ regions: dict[str, Region] = {},
58
+ preview: np.ndarray[int] | None = None,
59
+ version_major: int = DEFAULT_VERSION_MAJOR,
60
+ version_minor: int | None = DEFAULT_VERSION_MINOR,
61
+ mc_version: int = DEFAULT_MC_VERSION,
62
+ ) -> None:
63
+ self.name = name
64
+ self.description = description
65
+ self.author = author
66
+ self._preview = preview
67
+
68
+ self._regions: dict[str, Region] = regions
69
+
70
+ self._created_at = round(time.time() * 1000)
71
+ self._modified_at = self._created_at
72
+
73
+ self.version_major = version_major
74
+ self.version_minor = version_minor
75
+ self.mc_version = mc_version
76
+
77
+ self.modified: bool = True
78
+
79
+ def __getitem__(self, key):
80
+ return self._regions[key]
81
+
82
+ def __setitem__(self, key, value):
83
+ if not isinstance(value, Region):
84
+ raise TypeError(
85
+ f"Can only add Region objects, got {type(value).__name__}")
86
+ self._regions[key] = value
87
+
88
+ @property
89
+ def width(self) -> int:
90
+ return self.size.width
91
+
92
+ @property
93
+ def height(self) -> int:
94
+ return self.size.height
95
+
96
+ @property
97
+ def length(self) -> int:
98
+ return self.size.length
99
+
100
+ @property
101
+ def size(self) -> Size3D:
102
+ lower, upper = self.bounds
103
+ return Size3D(*abs(upper - lower)) + 1
104
+
105
+ @property
106
+ def bounds(self) -> tuple[BlockPosition, BlockPosition]:
107
+ lowers = []
108
+ uppers = []
109
+ for reg in self._regions.values():
110
+ lower, upper = reg.global_bounds
111
+ lowers.append(lower)
112
+ uppers.append(upper)
113
+ return (
114
+ BlockPosition(*np.min(lowers, axis=0)),
115
+ BlockPosition(*np.max(uppers, axis=0)),
116
+ )
117
+
118
+ @property
119
+ def volume(self) -> int:
120
+ return sum(reg.volume for reg in self._regions.values())
121
+
122
+ @property
123
+ def blocks(self) -> int:
124
+ return sum(reg.block_count for reg in self._regions.values())
125
+
126
+ @property
127
+ def region_count(self) -> int:
128
+ return len(self._regions)
129
+
130
+ @property
131
+ def version(self) -> str:
132
+ if self.version_minor is None:
133
+ return str(self.version_major)
134
+ return f"{self.version_major}.{self.version_minor}"
135
+
136
+ @property
137
+ def preview(self) -> np.ndarray[int] | None:
138
+ return self._preview
139
+
140
+ def regions(self) -> Iterator[tuple[str, Region]]:
141
+ return self._regions.items()
142
+
143
+ def add_region(self, name: str, region: Region) -> None:
144
+ self._regions[name] = region
145
+ self._update()
146
+
147
+ def remove_region(self, name: str) -> Region:
148
+ return self._regions.pop(name)
149
+
150
+ @property
151
+ def created_at(self) -> datetime:
152
+ return datetime.fromtimestamp(int(self._created_at / 1000))
153
+
154
+ @property
155
+ def modified_at(self) -> datetime:
156
+ return datetime.fromtimestamp(int(self._modified_at / 1000))
157
+
158
+ def save(self, path: pathlib.Path | str) -> None:
159
+ file = nbtlib.File(self.to_nbt())
160
+ file.save(path, gzipped=True, byteorder="big")
161
+
162
+ @staticmethod
163
+ def load(path: pathlib.Path | str) -> Schematic:
164
+ if isinstance(path, str):
165
+ path = pathlib.Path(path)
166
+ nbt = nbtlib.File.load(path.expanduser(), True)
167
+ return Schematic.from_nbt(nbt)
168
+
169
+ def to_nbt(self) -> nbtlib.Compound:
170
+ # self._update()
171
+ nbt = nbtlib.Compound()
172
+
173
+ # meta data
174
+ meta = nbtlib.Compound()
175
+ meta["Name"] = nbtlib.String(self.name)
176
+ meta["Author"] = nbtlib.String(self.author)
177
+ meta["Description"] = nbtlib.String(self.description)
178
+
179
+ meta["TimeCreated"] = nbtlib.Long(self._created_at)
180
+ meta["TimeModified"] = nbtlib.Long(self._modified_at)
181
+
182
+ meta["RegionCount"] = nbtlib.Int(self.region_count)
183
+ if self._preview is not None:
184
+ meta["PreviewImageData"] = encode_image_data(self._preview)
185
+
186
+ meta["EnclosingSize"] = self.size.to_nbt()
187
+ meta["TotalVolume"] = nbtlib.Long(self.volume)
188
+ meta["TotalBlocks"] = nbtlib.Long(self.blocks)
189
+ nbt["Metadata"] = meta
190
+
191
+ # regions
192
+ regions = nbtlib.Compound()
193
+ for name, region in self.regions():
194
+ region.compact_palette()
195
+ regions[name] = region.to_nbt()
196
+ nbt["Regions"] = regions
197
+
198
+ # versions
199
+ nbt["Version"] = nbtlib.Int(self.version_major)
200
+ if self.version_minor is not None:
201
+ nbt["SubVersion"] = nbtlib.Int(self.version_minor)
202
+ nbt["MinecraftDataVersion"] = nbtlib.Int(self.mc_version)
203
+
204
+ return nbt
205
+
206
+ @staticmethod
207
+ def from_nbt(nbt: nbtlib.Compound) -> Schematic:
208
+ # meta data
209
+ try:
210
+ meta = nbt["Metadata"]
211
+ except KeyError as exc:
212
+ raise KeyError(
213
+ "Can't load from NBT without 'Metadata' entry") from exc
214
+
215
+ name = meta["Name"].unpack()
216
+ author = meta["Author"].unpack()
217
+ description = meta["Description"].unpack()
218
+
219
+ preview = meta.get("PreviewImageData")
220
+ if preview is not None:
221
+ preview = decode_image_data(preview)
222
+
223
+ created_at = meta["TimeCreated"].unpack()
224
+ modified_at = meta["TimeModified"].unpack()
225
+
226
+ # regions
227
+ try:
228
+ regions = nbt["Regions"]
229
+ except KeyError as exc:
230
+ raise KeyError(
231
+ "Can't load from NBT without 'Regions' entry") from exc
232
+ regions = {name: Region.from_nbt(nbt) for name, nbt in regions.items()}
233
+
234
+ # version(s)
235
+ try:
236
+ major = nbt["Version"].unpack()
237
+ except KeyError as exc:
238
+ raise KeyError(
239
+ "Can't load from NBT without 'Version' entry") from exc
240
+
241
+ try:
242
+ minor = nbt["SubVersion"].unpack()
243
+ except KeyError:
244
+ minor = None
245
+
246
+ mc_version = nbt.get("MinecraftDataVersion")
247
+
248
+ schem = Schematic(
249
+ name=name,
250
+ author=author,
251
+ description=description,
252
+ regions=regions,
253
+ preview=preview,
254
+ version_major=major,
255
+ version_minor=minor,
256
+ mc_version=mc_version,
257
+ )
258
+
259
+ schem._created_at = created_at
260
+ schem._modified_at = modified_at
261
+
262
+ return schem
pylitematic/test.py ADDED
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+ import pathlib
3
+ from pylitematic import BlockPosition, BlockState, ResourceLocation, Schematic
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.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
+ for pos, state in reg.blocks(exclude=BlockState("air")):
32
+ print(pos, reg._to_internal(pos), state)
33
+ for pos, state in reg.blocks(include=BlockState("air")):
34
+ reg[pos] = BlockState("minecraft:netherrack")
35
+ print(BlockState("oak_log", axis="x") in reg)
36
+ print(BlockPosition(1, 1, 0) in reg)
37
+ print(ResourceLocation("birch_log") in reg)
38
+ # print(reg[0,:,2])
39
+ s.save("/mnt/d/minecraft/schematics/Litematica/test/aaa.litematic")
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitematic
3
- Version: 0.0.0
3
+ Version: 0.0.2
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
7
- Project-URL: Homepage, https://github.com/nieswand/pylitematic
8
- Project-URL: Repository, https://github.com/nieswand/pylitematic
9
- Project-URL: Issues, https://github.com/nieswand/pylitematic/issues
7
+ Project-URL: Homepage, https://github.com/boscawinks/pylitematic
8
+ Project-URL: Repository, https://github.com/boscawinks/pylitematic
9
+ Project-URL: Issues, https://github.com/boscawinks/pylitematic/issues
10
10
  Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.7
11
+ Requires-Python: >=3.10.12
12
12
  Description-Content-Type: text/markdown
13
13
  Requires-Dist: bitpacking>=0.1.0
14
- Requires-Dist: nbtlib>=1.6
15
- Requires-Dist: numpy>=1.21
14
+ Requires-Dist: nbtlib>=2.0.4
15
+ Requires-Dist: numpy>=2.2.6
16
16
 
17
17
  # pylitematic
18
18
 
@@ -25,5 +25,5 @@ Load, modify, and save [Litematica](https://litematica.org/) schematics
25
25
 
26
26
  `pylitematic` is available on pypi:
27
27
  ```bash
28
- pip install --upgrade pylitematic
28
+ python3 -m pip install --upgrade pylitematic
29
29
  ```
@@ -0,0 +1,12 @@
1
+ pylitematic/__init__.py,sha256=ycco-z7gJlUnBUJuJRo9qzu5pSzXSZLPkqOVLy8OsEI,211
2
+ pylitematic/block_property.py,sha256=eV9YbMcuttX-WPdbqN6dMmhb4jK_8_zRv33wNaVd8_o,9887
3
+ pylitematic/block_state.py,sha256=hC8ptOTR6XKokn-m2Nlxpi44AvL-vwlgive3KQ4VtUg,3328
4
+ pylitematic/geometry.py,sha256=n4Kk413FFhWHFgRRRQCYB2UdDBnbUMqwCEBP85YK1vI,5009
5
+ pylitematic/region.py,sha256=15elHo9Li_nw7_VeM9GP92mcK0cYuJtONKnDyj_SnxY,10918
6
+ pylitematic/resource_location.py,sha256=kVv9-4WVu_Ak24Um05bucKG-mcXymnylwHMha-ORLoo,2143
7
+ pylitematic/schematic.py,sha256=Zc85oT6jdHVdQtAgcZbj9qFXXXWow1GfKXc1TV10CqQ,7879
8
+ pylitematic/test.py,sha256=6IrD4t3f7puWIkgsZVfy-epDgKWFQIOGMizQFF7O7u0,1886
9
+ pylitematic-0.0.2.dist-info/METADATA,sha256=f0AzgTmhaCPTVVjpnoovf_q_2vG9x_KToOT4kh7bQDM,948
10
+ pylitematic-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ pylitematic-0.0.2.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
12
+ pylitematic-0.0.2.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- pylitematic/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
2
- pylitematic-0.0.0.dist-info/METADATA,sha256=lWoUEdP2KWSKCQMTdYU-bdctHPwuFTAnO43GHQlh_mA,924
3
- pylitematic-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- pylitematic-0.0.0.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
5
- pylitematic-0.0.0.dist-info/RECORD,,