pylitematic 0.0.0__py3-none-any.whl → 0.0.1__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 +7 -1
- pylitematic/block_property.py +325 -0
- pylitematic/block_state.py +112 -0
- pylitematic/geometry.py +169 -0
- pylitematic/region.py +214 -0
- pylitematic/resource_location.py +69 -0
- pylitematic/schematic.py +262 -0
- pylitematic/test.py +23 -0
- {pylitematic-0.0.0.dist-info → pylitematic-0.0.1.dist-info}/METADATA +8 -8
- pylitematic-0.0.1.dist-info/RECORD +12 -0
- {pylitematic-0.0.0.dist-info → pylitematic-0.0.1.dist-info}/top_level.txt +0 -0
- pylitematic-0.0.0.dist-info/RECORD +0 -5
- {pylitematic-0.0.0.dist-info → pylitematic-0.0.1.dist-info}/WHEEL +0 -0
pylitematic/__init__.py
CHANGED
@@ -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)
|
pylitematic/geometry.py
ADDED
@@ -0,0 +1,169 @@
|
|
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 __add__(self, other) -> Vec3i:
|
23
|
+
arr = np.array(self)
|
24
|
+
other_arr = self._to_array(other)
|
25
|
+
try:
|
26
|
+
result = arr + other_arr
|
27
|
+
except Exception:
|
28
|
+
return NotImplemented
|
29
|
+
return type(self)(*result.astype(int))
|
30
|
+
|
31
|
+
def __radd__(self, other) -> Vec3i:
|
32
|
+
return self.__add__(other)
|
33
|
+
|
34
|
+
def __sub__(self, other) -> Vec3i:
|
35
|
+
arr = np.array(self)
|
36
|
+
other_arr = self._to_array(other)
|
37
|
+
try:
|
38
|
+
result = arr - other_arr
|
39
|
+
except Exception:
|
40
|
+
return NotImplemented
|
41
|
+
return type(self)(*result.astype(int))
|
42
|
+
|
43
|
+
def __rsub__(self, other) -> Vec3i:
|
44
|
+
arr = np.array(self)
|
45
|
+
try:
|
46
|
+
result = other - arr
|
47
|
+
except Exception:
|
48
|
+
return NotImplemented
|
49
|
+
return type(self)(*result.astype(int))
|
50
|
+
|
51
|
+
def __mul__(self, scalar: int) -> Vec3i:
|
52
|
+
return type(self)(
|
53
|
+
self._a * scalar, self._b * scalar, self._c * scalar)
|
54
|
+
|
55
|
+
def __floordiv__(self, scalar: int) -> Vec3i:
|
56
|
+
return type(self)(
|
57
|
+
self._a // scalar, self._b // scalar, self._c // scalar)
|
58
|
+
|
59
|
+
def __neg__(self) -> Vec3i:
|
60
|
+
return type(self)(-self._a, -self._b, -self._c)
|
61
|
+
|
62
|
+
def __getitem__(self, index: int) -> int:
|
63
|
+
return self.to_tuple()[index]
|
64
|
+
|
65
|
+
def __iter__(self):
|
66
|
+
return iter(self.to_tuple())
|
67
|
+
|
68
|
+
def __abs__(self) -> Vec3i:
|
69
|
+
return type(self)(*np.abs(self))
|
70
|
+
|
71
|
+
def __lt__(self, other):
|
72
|
+
return np.array(self) < self._to_array(other)
|
73
|
+
|
74
|
+
def __le__(self, other):
|
75
|
+
return np.array(self) <= self._to_array(other)
|
76
|
+
|
77
|
+
def __gt__(self, other):
|
78
|
+
return np.array(self) > self._to_array(other)
|
79
|
+
|
80
|
+
def __ge__(self, other):
|
81
|
+
return np.array(self) >= self._to_array(other)
|
82
|
+
|
83
|
+
def __eq__(self, other):
|
84
|
+
return np.array(self) == self._to_array(other)
|
85
|
+
|
86
|
+
def __ne__(self, other):
|
87
|
+
return np.array(self) != self._to_array(other)
|
88
|
+
|
89
|
+
def __array__(self, dtype: type | None = None, copy: bool = True):
|
90
|
+
arr = np.array([self._a, self._b, self._c], dtype=dtype)
|
91
|
+
if copy:
|
92
|
+
return arr.copy()
|
93
|
+
else:
|
94
|
+
return arr
|
95
|
+
|
96
|
+
def _to_array(self, other):
|
97
|
+
if isinstance(other, Vec3i):
|
98
|
+
return np.array(other)
|
99
|
+
else:
|
100
|
+
return other
|
101
|
+
|
102
|
+
@staticmethod
|
103
|
+
def _to_int(value) -> int:
|
104
|
+
if isinstance(value, (int, np.integer)):
|
105
|
+
return int(value)
|
106
|
+
elif isinstance(value, float):
|
107
|
+
if value.is_integer():
|
108
|
+
return int(value)
|
109
|
+
raise TypeError(
|
110
|
+
f"{type(value).__name__} value {value!r} is not"
|
111
|
+
" int, numpy integer, or whole float")
|
112
|
+
|
113
|
+
def to_tuple(self) -> tuple[int, int, int]:
|
114
|
+
return (self._a, self._b, self._c)
|
115
|
+
|
116
|
+
@classmethod
|
117
|
+
def from_tuple(cls, t: tuple[int, int, int]) -> Vec3i:
|
118
|
+
return cls(*t)
|
119
|
+
|
120
|
+
def to_nbt(self) -> nbtlib.Compound:
|
121
|
+
return nbtlib.Compound({
|
122
|
+
"x": nbtlib.Int(self._a),
|
123
|
+
"y": nbtlib.Int(self._b),
|
124
|
+
"z": nbtlib.Int(self._c),
|
125
|
+
})
|
126
|
+
|
127
|
+
@classmethod
|
128
|
+
def from_nbt(cls, nbt: nbtlib.Compound) -> Vec3i:
|
129
|
+
return cls(int(nbt["x"]), int(nbt["y"]), int(nbt["z"]))
|
130
|
+
|
131
|
+
|
132
|
+
@dataclass(frozen=True)
|
133
|
+
class BlockPosition(Vec3i):
|
134
|
+
|
135
|
+
@property
|
136
|
+
def x(self) -> int:
|
137
|
+
return self._a
|
138
|
+
|
139
|
+
@property
|
140
|
+
def y(self) -> int:
|
141
|
+
return self._b
|
142
|
+
|
143
|
+
@property
|
144
|
+
def z(self) -> int:
|
145
|
+
return self._c
|
146
|
+
|
147
|
+
def __repr__(self) -> str:
|
148
|
+
return f"{type(self).__name__}(x={self.x}, y={self.y}, z={self.z})"
|
149
|
+
|
150
|
+
|
151
|
+
@dataclass(frozen=True)
|
152
|
+
class Size3D(Vec3i):
|
153
|
+
|
154
|
+
@property
|
155
|
+
def width(self) -> int:
|
156
|
+
return self._a
|
157
|
+
|
158
|
+
@property
|
159
|
+
def height(self) -> int:
|
160
|
+
return self._b
|
161
|
+
|
162
|
+
@property
|
163
|
+
def length(self) -> int:
|
164
|
+
return self._c
|
165
|
+
|
166
|
+
def __repr__(self) -> str:
|
167
|
+
return (
|
168
|
+
f"{type(self).__name__}("
|
169
|
+
f"width={self.width}, height={self.height}, length={self.length})")
|
pylitematic/region.py
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from bitpacking import bitpack, bitunpack
|
4
|
+
import nbtlib
|
5
|
+
import numpy as np
|
6
|
+
import twos
|
7
|
+
|
8
|
+
from .block_state import BlockState
|
9
|
+
from .geometry import BlockPosition, Size3D
|
10
|
+
|
11
|
+
|
12
|
+
AIR = BlockState("air")
|
13
|
+
|
14
|
+
|
15
|
+
class Region:
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
size: tuple[int, int, int] | Size3D,
|
19
|
+
origin: tuple[int, int, int] | BlockPosition = (0, 0, 0),
|
20
|
+
):
|
21
|
+
self._origin: BlockPosition = BlockPosition(*origin)
|
22
|
+
self._size: Size3D = Size3D(*size)
|
23
|
+
|
24
|
+
self._palette: list[BlockState] = [AIR]
|
25
|
+
self._palette_map: dict[BlockState, int] = {AIR: 0} # FIXME
|
26
|
+
self._blocks = np.zeros(abs(self._size))
|
27
|
+
self._entities: list[nbtlib.Compound] = []
|
28
|
+
self._tile_entities: list[nbtlib.Compound] = []
|
29
|
+
self._block_ticks: list[nbtlib.Compound] = []
|
30
|
+
self._fluid_ticks: list[nbtlib.Compound] = []
|
31
|
+
|
32
|
+
def __getitem__(self, key):
|
33
|
+
if isinstance(key, BlockPosition):
|
34
|
+
index = self._blocks[key.x, key.y, key.z]
|
35
|
+
return self._palette[index]
|
36
|
+
|
37
|
+
indices = self._blocks[key]
|
38
|
+
if np.isscalar(indices):
|
39
|
+
return self._palette[indices]
|
40
|
+
|
41
|
+
return np.array(self._palette, dtype=object)[indices]
|
42
|
+
|
43
|
+
def __setitem__(self, key, value) -> None:
|
44
|
+
if isinstance(key, BlockPosition):
|
45
|
+
key = key.to_tuple()
|
46
|
+
|
47
|
+
if isinstance(value, list):
|
48
|
+
value = np.array(value, dtype=object)
|
49
|
+
|
50
|
+
if isinstance(value, BlockState):
|
51
|
+
# assign single BlockState to slice
|
52
|
+
if value not in self._palette_map:
|
53
|
+
self._palette_map[value] = len(self._palette)
|
54
|
+
self._palette.append(value)
|
55
|
+
index = self._palette_map[value]
|
56
|
+
self._blocks[key] = index
|
57
|
+
|
58
|
+
elif isinstance(value, np.ndarray):
|
59
|
+
if value.shape != self._blocks[key].shape:
|
60
|
+
raise ValueError(
|
61
|
+
"Shape mismatch between assigned array and target slice")
|
62
|
+
|
63
|
+
# look up (or add) indices for all BlockStates
|
64
|
+
unique_states, xdi = np.unique(value, return_inverse=True)
|
65
|
+
idx = []
|
66
|
+
for state in unique_states:
|
67
|
+
if state not in self._palette_map:
|
68
|
+
self._palette_map[state] = len(self._palette)
|
69
|
+
self._palette.append(state)
|
70
|
+
idx.append(self._palette_map[state])
|
71
|
+
index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
|
72
|
+
self._blocks[key] = index_array
|
73
|
+
else:
|
74
|
+
raise TypeError(
|
75
|
+
"Value must be a BlockState or a list of BlockStates")
|
76
|
+
|
77
|
+
def compact_palette(self) -> None:
|
78
|
+
idx = np.unique(self._blocks)
|
79
|
+
# always include minecraft:air in a palette
|
80
|
+
if 0 not in idx:
|
81
|
+
idx = np.insert(idx, 0, 0)
|
82
|
+
index_map = {old: new for new, old in enumerate(idx)}
|
83
|
+
|
84
|
+
# compacted palette and mapping
|
85
|
+
palette = np.array(self._palette, dtype=object)[idx].tolist()
|
86
|
+
palette_map = {res: idx for idx, res in enumerate(palette)}
|
87
|
+
|
88
|
+
lookup = np.full(max(index_map) + 1, -1, dtype=int)
|
89
|
+
for old, new in index_map.items():
|
90
|
+
lookup[old] = new
|
91
|
+
self._blocks = lookup[self._blocks]
|
92
|
+
|
93
|
+
self._palette = palette
|
94
|
+
self._palette_map = palette_map
|
95
|
+
|
96
|
+
def _bits_per_state(self) -> int:
|
97
|
+
return max(2, (len(self._palette) - 1).bit_length())
|
98
|
+
|
99
|
+
def _decode_block_states(
|
100
|
+
self,
|
101
|
+
data: nbtlib.LongArray,
|
102
|
+
) -> np.ndarray[int]:
|
103
|
+
states = bitunpack(
|
104
|
+
chunks=[twos.to_unsigned(x, 64) for x in data],
|
105
|
+
field_width=self._bits_per_state(),
|
106
|
+
chunk_width=64,
|
107
|
+
)
|
108
|
+
states = list(states)[:self.volume]
|
109
|
+
shape = (abs(self.height), abs(self.length), abs(self.width))
|
110
|
+
states = np.asarray(states, dtype=int).reshape(shape) # y,z,x
|
111
|
+
return states.transpose(2, 0, 1) # x,y,z
|
112
|
+
|
113
|
+
def _encode_block_states(self) -> nbtlib.LongArray:
|
114
|
+
states = self._blocks.transpose(1, 2, 0).ravel() # x,y,z to y,z,x
|
115
|
+
chunks = bitpack(
|
116
|
+
states.tolist(),
|
117
|
+
field_width=self._bits_per_state(),
|
118
|
+
chunk_width=64,
|
119
|
+
)
|
120
|
+
return nbtlib.LongArray([twos.to_signed(x, 64) for x in chunks])
|
121
|
+
|
122
|
+
@property
|
123
|
+
def size(self) -> Size3D:
|
124
|
+
return self._size
|
125
|
+
|
126
|
+
@property
|
127
|
+
def origin(self) -> BlockPosition:
|
128
|
+
return self._origin
|
129
|
+
|
130
|
+
@property
|
131
|
+
def end(self) -> BlockPosition:
|
132
|
+
return self._origin + np.where(
|
133
|
+
self._size > 0, self._size - 1, self._size + 1)
|
134
|
+
|
135
|
+
@property
|
136
|
+
def width(self) -> int:
|
137
|
+
return self._size.width
|
138
|
+
|
139
|
+
@property
|
140
|
+
def height(self) -> int:
|
141
|
+
return self._size.height
|
142
|
+
|
143
|
+
@property
|
144
|
+
def length(self) -> int:
|
145
|
+
return self._size.length
|
146
|
+
|
147
|
+
@property
|
148
|
+
def volume(self) -> int:
|
149
|
+
return np.prod(self.shape).item()
|
150
|
+
|
151
|
+
@property
|
152
|
+
def blocks(self) -> int: # TODO: rename
|
153
|
+
return np.count_nonzero(self._blocks)
|
154
|
+
|
155
|
+
@property
|
156
|
+
def shape(self) -> tuple[int, int, int]:
|
157
|
+
return self._blocks.shape
|
158
|
+
|
159
|
+
@property
|
160
|
+
def lower(self) -> BlockPosition:
|
161
|
+
return BlockPosition(*np.min((self.origin, self.end), axis=0))
|
162
|
+
|
163
|
+
@property
|
164
|
+
def upper(self) -> BlockPosition:
|
165
|
+
return BlockPosition(*np.max((self.origin, self.end), axis=0))
|
166
|
+
|
167
|
+
@property
|
168
|
+
def bounds(self) -> tuple[BlockPosition, BlockPosition]:
|
169
|
+
return self.lower, self.upper
|
170
|
+
|
171
|
+
# def count(self, block: BlockState, ignore_props: bool = False) -> int:
|
172
|
+
# ...
|
173
|
+
|
174
|
+
def global_position(self, pos: BlockPosition) -> BlockPosition:
|
175
|
+
return self._origin + pos
|
176
|
+
|
177
|
+
def local_position(self, pos: BlockPosition) -> BlockPosition:
|
178
|
+
return pos - self._origin
|
179
|
+
|
180
|
+
def to_nbt(self) -> nbtlib.Compound:
|
181
|
+
nbt = nbtlib.Compound()
|
182
|
+
|
183
|
+
nbt["Position"] = self._origin.to_nbt()
|
184
|
+
nbt["Size"] = self._size.to_nbt()
|
185
|
+
|
186
|
+
pal = [block.to_nbt() for block in self._palette]
|
187
|
+
nbt["BlockStatePalette"] = nbtlib.List[nbtlib.Compound](pal)
|
188
|
+
nbt["BlockStates"] = self._encode_block_states()
|
189
|
+
|
190
|
+
nbt["Entities"] = self._entities
|
191
|
+
nbt["TileEntities"] = self._tile_entities
|
192
|
+
nbt["PendingBlockTicks"] = self._block_ticks
|
193
|
+
nbt["PendingFluidTicks"] = self._fluid_ticks
|
194
|
+
|
195
|
+
return nbt
|
196
|
+
|
197
|
+
@staticmethod
|
198
|
+
def from_nbt(nbt: nbtlib.Compound) -> Region:
|
199
|
+
pos = BlockPosition.from_nbt(nbt["Position"])
|
200
|
+
size = Size3D.from_nbt(nbt["Size"])
|
201
|
+
|
202
|
+
region = Region(origin=pos, size=size)
|
203
|
+
|
204
|
+
region._palette = [
|
205
|
+
BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
|
206
|
+
region._palette_map = {bl: i for i, bl in enumerate(region._palette)}
|
207
|
+
region._blocks = region._decode_block_states(nbt["BlockStates"])
|
208
|
+
|
209
|
+
region._entities = nbt["Entities"]
|
210
|
+
region._tile_entities = nbt["TileEntities"]
|
211
|
+
region._block_ticks = nbt["PendingBlockTicks"]
|
212
|
+
region._fluid_ticks = nbt["PendingFluidTicks"]
|
213
|
+
|
214
|
+
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))
|
pylitematic/schematic.py
ADDED
@@ -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.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.blocks 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,23 @@
|
|
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")
|
@@ -1,18 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pylitematic
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.1
|
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/
|
8
|
-
Project-URL: Repository, https://github.com/
|
9
|
-
Project-URL: Issues, https://github.com/
|
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.
|
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>=
|
15
|
-
Requires-Dist: numpy>=
|
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=xVVYkyJ7nV4I2eZ-XAcDz7yP2wXQHA-DWcMYRy_UUyg,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=J_Hu4umWUKxN6MFrem0f1hxmAEJPJugxJ2v2oXIygVk,4432
|
5
|
+
pylitematic/region.py,sha256=86Nr3vPvzEYXLnrPaRHx62DsxJFWNCD9leUHJ6ThPNs,7009
|
6
|
+
pylitematic/resource_location.py,sha256=kVv9-4WVu_Ak24Um05bucKG-mcXymnylwHMha-ORLoo,2143
|
7
|
+
pylitematic/schematic.py,sha256=HuPxQBmnE_AswncG0LN-lxQYJHADKxEl6JPLYK2MRUY,7867
|
8
|
+
pylitematic/test.py,sha256=vJU2d3ncw1EFEWALbUPXzsofk16-zWBDOZuJm6qNS7I,1019
|
9
|
+
pylitematic-0.0.1.dist-info/METADATA,sha256=KI4k64vSJ2LaVsK8_P0p0ARDc3b_UAsYM4TKgVP3oVA,948
|
10
|
+
pylitematic-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
pylitematic-0.0.1.dist-info/top_level.txt,sha256=sYUxm6O7Dh5TzuP-kPFe2FHJWUuwHFO69vN2VBiEG4A,12
|
12
|
+
pylitematic-0.0.1.dist-info/RECORD,,
|
File without changes
|
@@ -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,,
|
File without changes
|