pylitematic 0.0.2__py3-none-any.whl → 0.0.4__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/region.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from bitpacking import bitpack, bitunpack
4
5
  from functools import cached_property
6
+ from itertools import product
5
7
  import nbtlib
6
8
  import numpy as np
7
9
  import twos
@@ -9,7 +11,7 @@ from typing import Iterator
9
11
 
10
12
  from .block_state import BlockState
11
13
  from .geometry import BlockPosition, Size3D
12
- from .resource_location import ResourceLocation
14
+ from .resource_location import BlockId
13
15
 
14
16
 
15
17
  AIR = BlockState("air")
@@ -21,115 +23,63 @@ class Region:
21
23
  size: tuple[int, int, int] | Size3D,
22
24
  origin: tuple[int, int, int] | BlockPosition = (0, 0, 0),
23
25
  ):
24
- self._origin: BlockPosition = BlockPosition(*origin)
25
- self._size: Size3D = Size3D(*size)
26
+ if not isinstance(size, Size3D):
27
+ size = Size3D(*size)
28
+ self._size: Size3D = size
26
29
 
27
- self._palette: list[BlockState] = [AIR]
28
- self._palette_map: dict[BlockState, int] = {AIR: 0}
30
+ if not isinstance(origin, BlockPosition):
31
+ origin = BlockPosition(*origin)
32
+ self._origin: BlockPosition = origin
33
+
34
+ self._palette: list[BlockState] = [AIR] # TODO: add clear method
35
+ self._palette_map: dict[BlockState, int] = {AIR: 0} # TODO: bind tighter to _palette
29
36
  self._blocks = np.zeros(abs(self._size), dtype=int)
30
37
 
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] = []
38
+ # TODO: Add support for (tile) entities and ticks
39
+ self._entities = nbtlib.List[nbtlib.Compound]()
40
+ self._tile_entities = nbtlib.List[nbtlib.Compound]()
41
+ self._block_ticks = nbtlib.List[nbtlib.Compound]()
42
+ self._fluid_ticks = nbtlib.List[nbtlib.Compound]()
35
43
 
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
44
+ self._local = LocalRegionView(self)
45
+ self._world = WorldRegionView(self)
46
+ self._numpy = NumpyRegionView(self)
50
47
 
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
48
+ @property
49
+ def local(self) -> LocalRegionView:
50
+ return self._local
81
51
 
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)
52
+ @property
53
+ def world(self) -> WorldRegionView:
54
+ return self._world
88
55
 
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]
56
+ @property
57
+ def numpy(self) -> NumpyRegionView:
58
+ return self._numpy
94
59
 
95
- return np.array(self._palette, dtype=object)[indices]
60
+ def __contains__(self, item) -> bool:
61
+ return item in self.local
96
62
 
97
- def __setitem__(self, key, value) -> None:
98
- index = self._key_to_index(key)
63
+ def __eq__(self, other) -> bool:
64
+ return self.local == other
99
65
 
100
- if isinstance(value, list):
101
- value = np.array(value, dtype=object)
66
+ def __ne__(self, other) -> bool:
67
+ return self.local != other
102
68
 
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]
69
+ def __getitem__(self, key):
70
+ return self.local[key]
109
71
 
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")
72
+ def __setitem__(self, key, value) -> None:
73
+ self.local[key] = value
114
74
 
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")
75
+ def __iter__(self) -> tuple[BlockPosition, BlockState]:
76
+ return iter(self.local)
128
77
 
129
78
  def compact_palette(self) -> None:
79
+ # TODO: determine all appropriate places to call this method
130
80
  idx = np.unique(self._blocks)
131
- # always include minecraft:air in a palette
132
81
  if 0 not in idx:
82
+ # always include minecraft:air as the first entry in the palette
133
83
  idx = np.insert(idx, 0, 0)
134
84
  index_map = {old: new for new, old in enumerate(idx)}
135
85
 
@@ -145,6 +95,7 @@ class Region:
145
95
  self._palette = palette
146
96
  self._palette_map = palette_map
147
97
 
98
+ # block state en- / decoding (NBT)
148
99
  def _bits_per_state(self) -> int:
149
100
  return max(2, (len(self._palette) - 1).bit_length())
150
101
 
@@ -171,126 +122,76 @@ class Region:
171
122
  )
172
123
  return nbtlib.LongArray([twos.to_signed(x, 64) for x in chunks])
173
124
 
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
125
  @property
183
126
  def size(self) -> Size3D:
184
127
  return self._size
185
128
 
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
129
  @property
207
130
  def width(self) -> int:
208
- return self._size.width
131
+ return self.size.width
209
132
 
210
133
  @property
211
134
  def height(self) -> int:
212
- return self._size.height
135
+ return self.size.height
213
136
 
214
137
  @property
215
138
  def length(self) -> int:
216
- return self._size.length
139
+ return self.size.length
217
140
 
218
141
  @property
219
142
  def volume(self) -> int:
220
- return np.prod(self.shape).item()
143
+ return self.size.volume
221
144
 
222
145
  @property
223
- def block_count(self) -> int:
224
- # TODO: Add filter BlockState and rename to count()
225
- return np.count_nonzero(self._blocks)
146
+ def origin(self) -> BlockPosition:
147
+ return self._origin
226
148
 
227
149
  @property
228
- def shape(self) -> tuple[int, int, int]:
229
- return self._blocks.shape
150
+ def block_count(self) -> int:
151
+ # TODO: Add filter BlockStates / BlockIds and rename to count()
152
+ return np.sum(self != AIR).item()
230
153
 
231
- @cached_property
154
+ @property
232
155
  def lower(self) -> BlockPosition:
233
- return BlockPosition(*np.min((self.start, self.end), axis=0))
156
+ return self.local.lower
234
157
 
235
- @cached_property
158
+ @property
236
159
  def upper(self) -> BlockPosition:
237
- return BlockPosition(*np.max((self.start, self.end), axis=0))
160
+ return self.local.upper
238
161
 
239
- @cached_property
162
+ @property
240
163
  def bounds(self) -> tuple[BlockPosition, BlockPosition]:
241
- return self.lower, self.upper
164
+ return self.local.bounds
242
165
 
243
- @cached_property
244
- def global_lower(self) -> BlockPosition:
245
- return BlockPosition(*np.min((self.origin, self.limit), axis=0))
166
+ def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
167
+ return self.local.items()
246
168
 
247
- @cached_property
248
- def global_upper(self) -> BlockPosition:
249
- return BlockPosition(*np.max((self.origin, self.limit), axis=0))
169
+ def positions(self) -> Iterator[BlockPosition]:
170
+ return self.local.positions()
250
171
 
251
- @cached_property
252
- def global_bounds(self) -> tuple[BlockPosition, BlockPosition]:
253
- return self.global_lower, self.global_upper
172
+ def blocks(self) -> Iterator[BlockState]:
173
+ return self.local.blocks()
254
174
 
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
175
+ # block position transformations
176
+ def world_to_local(self, world: BlockPosition) -> BlockPosition:
177
+ return world - self._origin
277
178
 
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
179
+ def local_to_world(self, local: BlockPosition) -> BlockPosition:
180
+ return self._origin + local
181
+
182
+ def local_to_numpy(self, local: BlockPosition) -> BlockPosition:
183
+ return BlockPosition(*self.local.position_to_index(local))
285
184
 
286
- yield pos, state
185
+ def numpy_to_local(self, index: BlockPosition) -> BlockPosition:
186
+ return self.local.index_to_position(tuple(index))
287
187
 
288
- def to_global(self, local_pos: BlockPosition) -> BlockPosition:
289
- return self._origin + local_pos
188
+ def world_to_numpy(self, world: BlockPosition) -> BlockPosition:
189
+ return BlockPosition(*self.world.position_to_index(world))
290
190
 
291
- def to_local(self, global_pos: BlockPosition) -> BlockPosition:
292
- return global_pos - self._origin
191
+ def numpy_to_world(self, index: BlockPosition) -> BlockPosition:
192
+ return self.world.index_to_position(tuple(index))
293
193
 
194
+ # NBT conversion
294
195
  def to_nbt(self) -> nbtlib.Compound:
295
196
  nbt = nbtlib.Compound()
296
197
 
@@ -308,12 +209,12 @@ class Region:
308
209
 
309
210
  return nbt
310
211
 
311
- @staticmethod
312
- def from_nbt(nbt: nbtlib.Compound) -> Region:
212
+ @classmethod
213
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Region:
313
214
  pos = BlockPosition.from_nbt(nbt["Position"])
314
215
  size = Size3D.from_nbt(nbt["Size"])
315
216
 
316
- region = Region(origin=pos, size=size)
217
+ region = cls(origin=pos, size=size)
317
218
 
318
219
  region._palette = [
319
220
  BlockState.from_nbt(block) for block in nbt["BlockStatePalette"]]
@@ -326,3 +227,259 @@ class Region:
326
227
  region._fluid_ticks = nbt["PendingFluidTicks"]
327
228
 
328
229
  return region
230
+
231
+
232
+ class _RegionView(ABC):
233
+
234
+ def __init__(self, region: Region) -> None:
235
+ self.region = region
236
+ # TODO: add support for (tile) entities and ticks
237
+
238
+ @property
239
+ def _blocks(self) -> np.ndarray[int]:
240
+ return self.region._blocks
241
+
242
+ @property
243
+ def _palette(self) -> list[BlockState]:
244
+ return self.region._palette
245
+
246
+ @property
247
+ def _palette_map(self) -> dict[BlockState, int]:
248
+ return self.region._palette_map
249
+
250
+ @abstractmethod
251
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
252
+ ...
253
+
254
+ @abstractmethod
255
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
256
+ ...
257
+
258
+ @abstractmethod
259
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
260
+ ...
261
+
262
+ @abstractmethod
263
+ def _transform_index(self, index):
264
+ ...
265
+
266
+ def __getitem__(self, key):
267
+ if isinstance(key, BlockPosition):
268
+ # return self.at(key) # TODO
269
+ key = tuple(key)
270
+ index = self._transform_index(key)
271
+
272
+ indices = self._blocks[index]
273
+ if np.isscalar(indices):
274
+ return self._palette[indices]
275
+ else:
276
+ return np.array(self._palette, dtype=object)[indices]
277
+
278
+ def __setitem__(self, key, value):
279
+ if isinstance(key, BlockPosition):
280
+ # return self.set_at(key, value) # TODO
281
+ key = tuple(key)
282
+ index = self._transform_index(key)
283
+
284
+ if isinstance(value, list):
285
+ value = np.array(value, dtype=object)
286
+
287
+ if isinstance(value, BlockState):
288
+ # assign single BlockState to slice
289
+ if value not in self._palette_map:
290
+ self._palette_map[value] = len(self._palette)
291
+ self._palette.append(value)
292
+ self._blocks[index] = self._palette_map[value]
293
+
294
+ elif isinstance(value, np.ndarray):
295
+ if value.shape != self._blocks[index].shape:
296
+ # TODO: allow casting
297
+ raise ValueError(
298
+ "Shape mismatch between assigned array and target slice")
299
+
300
+ # look up (or add) indices for all BlockStates
301
+ unique_states, xdi = np.unique(value, return_inverse=True)
302
+ idx = []
303
+ for state in unique_states:
304
+ if state not in self._palette_map:
305
+ self._palette_map[state] = len(self._palette)
306
+ self._palette.append(state)
307
+ idx.append(self._palette_map[state])
308
+ index_array = np.array(idx, dtype=int)[xdi].reshape(value.shape)
309
+ self._blocks[index] = index_array
310
+ else:
311
+ raise TypeError(
312
+ "Value must be a BlockState or a list of BlockStates")
313
+
314
+ def __contains__(self, item) -> bool:
315
+ if isinstance(item, BlockPosition):
316
+ return all(self.lower <= item) and all(item <= self.upper)
317
+
318
+ elif isinstance(item, BlockState):
319
+ index = self._palette_map.get(item)
320
+ if index is None:
321
+ return False
322
+ return index in self._blocks
323
+ # return np.any(self._blocks == index).item()
324
+
325
+ elif isinstance(item, BlockId):
326
+ return any(
327
+ # bs.id == item and np.any(self._blocks == idx)
328
+ bs.id == item and idx in self._blocks
329
+ for bs, idx in self._palette_map.items())
330
+
331
+ else:
332
+ return False
333
+
334
+ def __iter__(self) -> Iterator[tuple[BlockPosition, BlockState]]:
335
+ return self.items()
336
+
337
+ def items(self) -> Iterator[tuple[BlockPosition, BlockState]]:
338
+ for pos, block in zip(self.positions(), self.blocks()):
339
+ yield pos, block
340
+
341
+ def positions(self) -> Iterator[BlockPosition]:
342
+ ranges = [
343
+ range(start, stop, step)
344
+ for start, stop, step
345
+ in zip(self.origin, self.origin + self.size, self.size.sign)
346
+ ]
347
+ for z, y, x in product(*reversed(ranges)):
348
+ yield BlockPosition(x, y, z)
349
+
350
+ def blocks(self) -> Iterator[BlockState]:
351
+ indices = self._align_array(self._blocks).transpose(2, 1, 0).ravel()
352
+ palette = np.array(self._palette, dtype=object)
353
+ for block in palette[indices]:
354
+ yield block
355
+
356
+ def __eq__(self, other) -> np.ndarray[bool]:
357
+ palette = np.array(self._palette, dtype=object)
358
+
359
+ if isinstance(other, BlockState):
360
+ matches = np.array([state == other for state in palette])
361
+ mask = matches[self._blocks]
362
+
363
+ elif isinstance(other, BlockId):
364
+ matches = np.array([state.id == other for state in palette])
365
+ mask = matches[self._blocks]
366
+
367
+ else:
368
+ return NotImplemented
369
+
370
+ return self._align_array(mask)
371
+
372
+ def __ne__(self, other) -> np.ndarray[bool]:
373
+ return np.invert(self.__eq__(other))
374
+
375
+ property
376
+ @abstractmethod
377
+ def origin(self) -> BlockPosition:
378
+ ...
379
+
380
+ @property
381
+ @abstractmethod
382
+ def size(self) -> Size3D:
383
+ ...
384
+
385
+ @cached_property
386
+ def limit(self) -> BlockPosition:
387
+ return self.origin + self.size.limit
388
+
389
+ @cached_property
390
+ def lower(self) -> BlockPosition:
391
+ return BlockPosition(*np.min((self.origin, self.limit), axis=0))
392
+
393
+ @cached_property
394
+ def upper(self) -> BlockPosition:
395
+ return BlockPosition(*np.max((self.origin, self.limit), axis=0))
396
+
397
+ @property
398
+ def bounds(self) -> tuple[BlockPosition, BlockPosition]:
399
+ return self.lower, self.upper
400
+
401
+
402
+ class NumpyRegionView(_RegionView):
403
+
404
+ @property
405
+ def origin(self) -> BlockPosition:
406
+ return BlockPosition(0, 0, 0)
407
+
408
+ @property
409
+ def size(self) -> Size3D:
410
+ return abs(self.region._size)
411
+
412
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
413
+ return tuple(pos)
414
+
415
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
416
+ return BlockPosition(*index)
417
+
418
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
419
+ return arr
420
+
421
+ def _transform_index(self, index):
422
+ return index
423
+
424
+
425
+ class _OrientedView(_RegionView):
426
+
427
+ @property
428
+ def size(self) -> Size3D:
429
+ return self.region._size
430
+
431
+ @cached_property
432
+ def negative_axes(self) -> tuple[int,...]:
433
+ return tuple(np.argwhere(self.size < 0).flatten().tolist())
434
+
435
+ def position_to_index(self, pos: BlockPosition) -> tuple[int, int, int]:
436
+ return pos - self.lower
437
+
438
+ def index_to_position(self, index: tuple[int, int, int]) -> BlockPosition:
439
+ return self.lower + index
440
+
441
+ def _align_array(self, arr: np.ndarray) -> np.ndarray:
442
+ return np.flip(arr, axis=self.negative_axes)
443
+
444
+ def _transform_index(self, key):
445
+ if isinstance(key, (int, np.integer, slice, type(Ellipsis))):
446
+ key = (key,)
447
+
448
+ if isinstance(key, tuple):
449
+ key = list(key)
450
+ for i, k in enumerate(key):
451
+ offset = self.lower[i]
452
+ if isinstance(k, (int, np.integer)):
453
+ key[i] = k - offset
454
+ elif isinstance(k, slice):
455
+ start = k.start - offset if k.start is not None else None
456
+ stop = k.stop - offset if k.stop is not None else None
457
+ key[i] = slice(start, stop, k.step)
458
+ else:
459
+ # Ellipsis
460
+ key[i] = k
461
+ return tuple(key)
462
+
463
+ elif isinstance(key, np.ndarray) and key.dtype == bool:
464
+ # boolean indexing
465
+ key = self._align_array(key)
466
+ if key.shape != self._blocks.shape:
467
+ raise IndexError("Boolean index must match region shape.")
468
+ return key
469
+
470
+ else:
471
+ return key
472
+
473
+
474
+ class LocalRegionView(_OrientedView):
475
+
476
+ @property
477
+ def origin(self) -> BlockPosition:
478
+ return BlockPosition(0, 0, 0)
479
+
480
+
481
+ class WorldRegionView(_OrientedView):
482
+
483
+ @property
484
+ def origin(self) -> BlockPosition:
485
+ return self.region._origin
@@ -50,8 +50,8 @@ class ResourceLocation:
50
50
  def to_string(self) -> str:
51
51
  return str(self)
52
52
 
53
- @staticmethod
54
- def from_string(string: str) -> ResourceLocation:
53
+ @classmethod
54
+ def from_string(cls, string: str) -> ResourceLocation:
55
55
  match = LOCATION_PATTERN.fullmatch(string)
56
56
  if not match:
57
57
  raise ValueError(f"Invalid resource location string {string!r}")
@@ -59,11 +59,15 @@ class ResourceLocation:
59
59
  namespace = match.group("namespace")
60
60
  path = match.group("path")
61
61
 
62
- return ResourceLocation(path=path, namespace=namespace)
62
+ return cls(path=path, namespace=namespace)
63
63
 
64
64
  def to_nbt(self) -> nbtlib.String:
65
65
  return nbtlib.String(self)
66
66
 
67
- @staticmethod
68
- def from_nbt(nbt: nbtlib.String) -> ResourceLocation:
69
- return ResourceLocation.from_string(str(nbt))
67
+ @classmethod
68
+ def from_nbt(cls, nbt: nbtlib.String) -> ResourceLocation:
69
+ return cls.from_string(str(nbt))
70
+
71
+
72
+ class BlockId(ResourceLocation):
73
+ ...