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.
Files changed (21) hide show
  1. {pylitematic-0.0.1 → pylitematic-0.0.3}/PKG-INFO +4 -1
  2. {pylitematic-0.0.1 → pylitematic-0.0.3}/pyproject.toml +8 -2
  3. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/__init__.py +2 -2
  4. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/block_property.py +0 -66
  5. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/block_state.py +12 -5
  6. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/geometry.py +48 -10
  7. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/region.py +153 -32
  8. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/resource_location.py +4 -0
  9. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic/schematic.py +9 -7
  10. pylitematic-0.0.3/src/pylitematic/test.py +71 -0
  11. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/PKG-INFO +4 -1
  12. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/SOURCES.txt +2 -1
  13. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/requires.txt +4 -0
  14. {pylitematic-0.0.1 → pylitematic-0.0.3}/tests/test_block_property.py +0 -42
  15. pylitematic-0.0.3/tests/test_schematic.py +40 -0
  16. pylitematic-0.0.1/src/pylitematic/test.py +0 -23
  17. {pylitematic-0.0.1 → pylitematic-0.0.3}/README.md +0 -0
  18. {pylitematic-0.0.1 → pylitematic-0.0.3}/setup.cfg +0 -0
  19. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/dependency_links.txt +0 -0
  20. {pylitematic-0.0.1 → pylitematic-0.0.3}/src/pylitematic.egg-info/top_level.txt +0 -0
  21. {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.1
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.1"
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"
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 ResourceLocation
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 ResourceLocation
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
- self._id: ResourceLocation = ResourceLocation.from_string(_id)
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) -> str:
62
- return str(self._id)
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
- other_arr = self._to_array(other)
27
+ other = self._to_array(other)
25
28
  try:
26
- result = arr + other_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
- other_arr = self._to_array(other)
39
+ other = self._to_array(other)
37
40
  try:
38
- result = arr - other_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, scalar: int) -> Vec3i:
52
- return type(self)(
53
- self._a * scalar, self._b * scalar, self._c * scalar)
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 __floordiv__(self, scalar: int) -> Vec3i:
56
- return type(self)(
57
- self._a // scalar, self._b // scalar, self._c // scalar)
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} # 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] = []
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 __getitem__(self, key):
33
- if isinstance(key, BlockPosition):
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
- indices = self._blocks[key]
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
- if isinstance(key, BlockPosition):
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[key].shape:
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[key] = index_array
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
- @property
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._origin + np.where(
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 blocks(self) -> int: # TODO: rename
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
- @property
238
+ @cached_property
160
239
  def lower(self) -> BlockPosition:
161
- return BlockPosition(*np.min((self.origin, self.end), axis=0))
240
+ return BlockPosition(*np.min((self.start, self.end), axis=0))
162
241
 
163
- @property
242
+ @cached_property
164
243
  def upper(self) -> BlockPosition:
165
- return BlockPosition(*np.max((self.origin, self.end), axis=0))
244
+ return BlockPosition(*np.max((self.start, self.end), axis=0))
166
245
 
167
- @property
246
+ @cached_property
168
247
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
169
248
  return self.lower, self.upper
170
249
 
171
- # def count(self, block: BlockState, ignore_props: bool = False) -> int:
172
- # ...
250
+ @cached_property
251
+ def global_lower(self) -> BlockPosition:
252
+ return BlockPosition(*np.min((self.origin, self.limit), axis=0))
173
253
 
174
- def global_position(self, pos: BlockPosition) -> BlockPosition:
175
- return self._origin + pos
254
+ @cached_property
255
+ def global_upper(self) -> BlockPosition:
256
+ return BlockPosition(*np.max((self.origin, self.limit), axis=0))
176
257
 
177
- def local_position(self, pos: BlockPosition) -> BlockPosition:
178
- return pos - self._origin
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()
@@ -67,3 +67,7 @@ class ResourceLocation:
67
67
  @staticmethod
68
68
  def from_nbt(nbt: nbtlib.String) -> ResourceLocation:
69
69
  return ResourceLocation.from_string(str(nbt))
70
+
71
+
72
+ class BlockId(ResourceLocation):
73
+ ...
@@ -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.bounds
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.blocks for reg in self._regions.values())
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
- description = meta["Description"].unpack()
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=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.1
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
 
@@ -14,4 +14,5 @@ src/pylitematic.egg-info/dependency_links.txt
14
14
  src/pylitematic.egg-info/requires.txt
15
15
  src/pylitematic.egg-info/top_level.txt
16
16
  tests/test_block_property.py
17
- tests/test_resource_location.py
17
+ tests/test_resource_location.py
18
+ tests/test_schematic.py
@@ -1,3 +1,7 @@
1
1
  bitpacking>=0.1.0
2
2
  nbtlib>=2.0.4
3
3
  numpy>=2.2.6
4
+ twos>=0.0.1
5
+
6
+ [dev]
7
+ pytest
@@ -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