pylitematic 0.0.3__tar.gz → 0.0.5__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 (26) hide show
  1. {pylitematic-0.0.3 → pylitematic-0.0.5}/PKG-INFO +4 -1
  2. {pylitematic-0.0.3 → pylitematic-0.0.5}/pyproject.toml +4 -1
  3. pylitematic-0.0.5/src/pylitematic/__init__.py +6 -0
  4. pylitematic-0.0.5/src/pylitematic/block_palette.py +129 -0
  5. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic/block_property.py +13 -13
  6. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic/block_state.py +34 -12
  7. pylitematic-0.0.5/src/pylitematic/geometry.py +171 -0
  8. pylitematic-0.0.5/src/pylitematic/property_cache.py +52 -0
  9. pylitematic-0.0.5/src/pylitematic/region.py +634 -0
  10. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic/resource_location.py +6 -10
  11. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic/schematic.py +24 -13
  12. pylitematic-0.0.5/src/pylitematic/test.py +79 -0
  13. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic.egg-info/PKG-INFO +4 -1
  14. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic.egg-info/SOURCES.txt +2 -0
  15. {pylitematic-0.0.3 → pylitematic-0.0.5}/tests/test_schematic.py +2 -2
  16. pylitematic-0.0.3/src/pylitematic/__init__.py +0 -7
  17. pylitematic-0.0.3/src/pylitematic/geometry.py +0 -207
  18. pylitematic-0.0.3/src/pylitematic/region.py +0 -335
  19. pylitematic-0.0.3/src/pylitematic/test.py +0 -71
  20. {pylitematic-0.0.3 → pylitematic-0.0.5}/README.md +0 -0
  21. {pylitematic-0.0.3 → pylitematic-0.0.5}/setup.cfg +0 -0
  22. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic.egg-info/dependency_links.txt +0 -0
  23. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic.egg-info/requires.txt +0 -0
  24. {pylitematic-0.0.3 → pylitematic-0.0.5}/src/pylitematic.egg-info/top_level.txt +0 -0
  25. {pylitematic-0.0.3 → pylitematic-0.0.5}/tests/test_block_property.py +0 -0
  26. {pylitematic-0.0.3 → pylitematic-0.0.5}/tests/test_resource_location.py +0 -0
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylitematic
3
- Version: 0.0.3
3
+ Version: 0.0.5
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
7
  Project-URL: Homepage, https://github.com/boscawinks/pylitematic
8
8
  Project-URL: Repository, https://github.com/boscawinks/pylitematic
9
9
  Project-URL: Issues, https://github.com/boscawinks/pylitematic/issues
10
+ Classifier: Development Status :: 2 - Pre-Alpha
10
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Utilities
11
14
  Requires-Python: >=3.10.12
12
15
  Description-Content-Type: text/markdown
13
16
  Requires-Dist: bitpacking>=0.1.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pylitematic"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  description = "Load, modify, and save Litematica schematics"
9
9
  authors = [
10
10
  { name="Boscawinks", email="bosca.winks@gmx.de" }
@@ -19,7 +19,10 @@ readme = "README.md"
19
19
  requires-python = ">=3.10.12"
20
20
  license = { text = "GPL-3.0-only" }
21
21
  classifiers = [
22
+ "Development Status :: 2 - Pre-Alpha",
22
23
  "Programming Language :: Python :: 3",
24
+ "Operating System :: OS Independent",
25
+ "Topic :: Utilities"
23
26
  ]
24
27
 
25
28
  [project.optional-dependencies]
@@ -0,0 +1,6 @@
1
+ __version__ = "0.0.5"
2
+
3
+ from .block_state import AIR, BlockId, BlockState
4
+ from .geometry import BlockPosition, Direction, Size3D
5
+ from .region import Region
6
+ from .schematic import Schematic
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import nbtlib
5
+ import numpy as np
6
+ from typing import Iterator
7
+
8
+ from .block_state import AIR, BlockId, BlockState
9
+
10
+
11
+ class BlockPalette:
12
+ def __init__(self):
13
+ self._states: list[BlockState] = []
14
+ self._map: dict[BlockState, int] = {}
15
+ self.add_state(AIR)
16
+
17
+ def __len__(self):
18
+ return len(self._states)
19
+
20
+ def __str__(self) -> str:
21
+ return str({str(s): i for s, i in self._map.items()})
22
+
23
+ def __repr__(self) -> str:
24
+ return f"{type(self).__name__}({self._map})"
25
+
26
+ def __contains__(self, key) -> bool:
27
+ # TODO: add BlockId comparison?
28
+ if not isinstance(key, BlockState):
29
+ return NotImplemented
30
+ return key in self._map
31
+
32
+ def __eq__(self, other) -> bool | np.ndarray[bool]:
33
+ if not isinstance(other, (BlockState, BlockId)):
34
+ return NotImplemented
35
+ return np.array(self) == other
36
+
37
+ def __iter__(self) -> Iterator[tuple[BlockState, int]]:
38
+ return self.items()
39
+
40
+ def __array__(self, dtype: type | None = None, copy: bool = True):
41
+ arr = np.array(self._states, dtype=object)
42
+ return arr.copy() if copy else arr
43
+
44
+ def states(self) -> Iterator[BlockState]:
45
+ for state in self._map.keys():
46
+ yield state
47
+
48
+ def indices(self) -> Iterator[int]:
49
+ for index in self._map.values():
50
+ yield index
51
+
52
+ def items(self) -> Iterator[tuple[BlockState, int]]:
53
+ for state, index in self._map.items():
54
+ yield state, index
55
+
56
+ @property
57
+ def bits_per_state(self) -> int:
58
+ return max(2, (len(self) - 1).bit_length())
59
+
60
+ def copy(self) -> BlockPalette:
61
+ pal = BlockPalette()
62
+ pal._states = copy.deepcopy(self._states)
63
+ pal._map = copy.deepcopy(self._map)
64
+ return pal
65
+
66
+ def clear(self) -> None:
67
+ self._states.clear()
68
+ self._map.clear()
69
+ self.add_state(AIR)
70
+
71
+ def add_state(self, state: BlockState) -> int:
72
+ if state in self:
73
+ return self._map[state]
74
+ else:
75
+ index = len(self)
76
+ self._states.append(state)
77
+ self._map[state] = index
78
+ return index
79
+
80
+ def get_state(
81
+ self,
82
+ index: int | np.ndarray[int],
83
+ ) -> BlockState | np.ndarray[BlockState]:
84
+ return np.array(self._states, dtype=object)[np.array(index, dtype=int)]
85
+
86
+ def get_index(
87
+ self,
88
+ state: BlockState | np.ndarray[BlockState],
89
+ add_missing: bool = False,
90
+ ) -> int | np.ndarray[int]:
91
+ state = np.asarray(state, dtype=object)
92
+ unique_states, xdi = np.unique(state, return_inverse=True)
93
+ idx = []
94
+ for block in unique_states:
95
+ if block not in self and not add_missing:
96
+ raise KeyError(f"BlockState '{block!s}' not found in palette.")
97
+ idx.append(self.add_state(block))
98
+
99
+ index = np.array(idx, dtype=int)[xdi].reshape(state.shape)
100
+ return index.item() if np.isscalar(index) else index
101
+
102
+ def reduce(self, indices: np.ndarray[int]) -> None:
103
+ if not (isinstance(indices, np.ndarray) and indices.dtype == int):
104
+ raise TypeError("'indices' has to be a numpy array of integers")
105
+
106
+ unique_idx = np.unique(indices)
107
+ if 0 not in unique_idx:
108
+ # always include minecraft:air as the first entry in the palette
109
+ unique_idx = np.insert(unique_idx, 0, 0)
110
+ self._states = np.array(self._states, dtype=object)[unique_idx].tolist()
111
+ self._map = {state: i for i, state in enumerate(self._states)}
112
+
113
+ old_new_map = {old: new for new, old in enumerate(unique_idx)}
114
+ lookup = np.full(max(old_new_map) + 1, -1, dtype=int)
115
+ for old, new in old_new_map.items():
116
+ lookup[old] = new
117
+ return lookup[indices]
118
+
119
+ def to_nbt(self) -> nbtlib.List[nbtlib.Compound]:
120
+ pal = [state.to_nbt() for state in self._states]
121
+ return nbtlib.List[nbtlib.Compound](pal)
122
+
123
+ @classmethod
124
+ def from_nbt(cls, nbt: nbtlib.List[nbtlib.Compound]) -> BlockPalette:
125
+ states = [BlockState.from_nbt(block) for block in nbt]
126
+ pal = cls()
127
+ pal._states = states
128
+ pal._map = {state: i for i, state in enumerate(states)}
129
+ return pal
@@ -94,10 +94,10 @@ class Properties(dict):
94
94
  def to_string(self) -> str:
95
95
  return str(self)
96
96
 
97
- @staticmethod
98
- def from_string(string: str) -> Properties:
97
+ @classmethod
98
+ def from_string(cls, string: str) -> Properties:
99
99
  if string in ("", "[]"):
100
- return Properties()
100
+ return cls()
101
101
 
102
102
  if not (string.startswith("[") and string.endswith("]")):
103
103
  raise ValueError(f"Invalid properties string {string!r}")
@@ -113,18 +113,18 @@ class Properties(dict):
113
113
  ValueError(f"Duplicate property name {name!r}")
114
114
  props[name] = PropertyValue.from_string(string=val_str).get()
115
115
 
116
- return Properties(props)
116
+ return cls(props)
117
117
 
118
118
  def to_nbt(self) -> nbtlib.Compound:
119
119
  return nbtlib.Compound(
120
120
  {name: value.to_nbt() for name, value in sorted(super().items())})
121
121
 
122
- @staticmethod
123
- def from_nbt(nbt: nbtlib.Compound) -> Properties:
122
+ @classmethod
123
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Properties:
124
124
  props = {}
125
125
  for name, value in nbt.items():
126
126
  props[name] = PropertyValue.from_nbt(nbt=value).get()
127
- return Properties(props)
127
+ return cls(props)
128
128
 
129
129
 
130
130
  class PropertyValue(ABC):
@@ -204,20 +204,20 @@ class PropertyValue(ABC):
204
204
  def to_string(self) -> str:
205
205
  return str(self)
206
206
 
207
- @staticmethod
208
- def from_string(string: str) -> PropertyValue:
207
+ @classmethod
208
+ def from_string(cls, string: str) -> PropertyValue:
209
209
  try:
210
210
  value = json.loads(string)
211
211
  except json.JSONDecodeError:
212
212
  value = string
213
- return PropertyValue.value_factory(value)
213
+ return cls.value_factory(value)
214
214
 
215
215
  def to_nbt(self) -> nbtlib.String:
216
216
  return nbtlib.String(self)
217
217
 
218
- @staticmethod
219
- def from_nbt(nbt: nbtlib.String) -> PropertyValue:
220
- return PropertyValue.from_string(str(nbt))
218
+ @classmethod
219
+ def from_nbt(cls, nbt: nbtlib.String) -> PropertyValue:
220
+ return cls.from_string(str(nbt))
221
221
 
222
222
 
223
223
  class BooleanValue(PropertyValue):
@@ -1,11 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from copy import deepcopy
4
+ from dataclasses import dataclass
4
5
  from nbtlib import Compound
5
6
  from typing import Any, Iterator
6
7
 
7
- from .resource_location import BlockId
8
8
  from .block_property import Properties
9
+ from .resource_location import ResourceLocation
10
+
11
+
12
+ @dataclass(frozen=True, order=True)
13
+ class BlockId(ResourceLocation):
14
+
15
+ def __eq__(self, other: Any) -> bool:
16
+ if isinstance(other, BlockState):
17
+ return self == other.id
18
+ else:
19
+ return super().__eq__(other)
9
20
 
10
21
 
11
22
  class BlockState:
@@ -42,10 +53,18 @@ class BlockState:
42
53
 
43
54
  def __eq__(self, other: Any) -> bool:
44
55
  if isinstance(other, str):
45
- other = BlockState.from_string(other)
46
- elif not isinstance(other, BlockState):
56
+ try:
57
+ other = BlockState.from_string(other)
58
+ return self == other
59
+ except ValueError:
60
+ return False
61
+
62
+ if isinstance(other, BlockId):
63
+ return self.id == other
64
+ elif isinstance(other, BlockState):
65
+ return (self.id, self._props) == (other.id, other._props)
66
+ else:
47
67
  return NotImplemented
48
- return (self.id, self._props) == (other.id, other._props)
49
68
 
50
69
  def __lt__(self, other: Any) -> bool:
51
70
  if not isinstance(other, BlockState):
@@ -74,15 +93,15 @@ class BlockState:
74
93
  def to_string(self) -> str:
75
94
  return str(self)
76
95
 
77
- @staticmethod
78
- def from_string(string: str) -> BlockState:
96
+ @classmethod
97
+ def from_string(cls, string: str) -> BlockState:
79
98
  idx = string.find("[") # basic parsing to separate block:id[name=value]
80
99
  if idx == -1:
81
100
  id, props = string, ""
82
101
  else:
83
102
  id, props = string[:idx], string[idx:]
84
103
 
85
- state = BlockState(id)
104
+ state = cls(id)
86
105
  state._props = Properties.from_string(props)
87
106
  return state
88
107
 
@@ -93,19 +112,19 @@ class BlockState:
93
112
  nbt["Properties"] = self._props.to_nbt()
94
113
  return nbt
95
114
 
96
- @staticmethod
97
- def from_nbt(nbt: Compound) -> BlockState:
98
- state = BlockState(str(nbt["Name"]))
115
+ @classmethod
116
+ def from_nbt(cls, nbt: Compound) -> BlockState:
117
+ state = cls(str(nbt["Name"]))
99
118
  state._props = Properties.from_nbt(nbt.get("Properties", Compound()))
100
119
  return state
101
120
 
102
121
  def with_id(self, id: str) -> BlockState:
103
- state = BlockState(id)
122
+ state = type(self)(id)
104
123
  state._props = deepcopy(self._props)
105
124
  return state
106
125
 
107
126
  def with_props(self, **props: Any) -> BlockState:
108
- state = BlockState(self.id)
127
+ state = type(self)(self.id)
109
128
  new_props = deepcopy(self._props)
110
129
  for name, value in props.items():
111
130
  if value is None:
@@ -117,3 +136,6 @@ class BlockState:
117
136
 
118
137
  def without_props(self) -> BlockState:
119
138
  return BlockState(self.id)
139
+
140
+
141
+ AIR = BlockState("air")
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import enum
5
+ import nbtlib
6
+ import numpy as np
7
+ from typing import Iterator
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Vec3i:
12
+ _a: int
13
+ _b: int
14
+ _c: int
15
+
16
+ def __post_init__(self) -> None:
17
+ object.__setattr__(self, "_a", int(self._a))
18
+ object.__setattr__(self, "_b", int(self._b))
19
+ object.__setattr__(self, "_c", int(self._c))
20
+
21
+ def __getitem__(self, index: int) -> int:
22
+ return tuple(self)[index]
23
+
24
+ def __str__(self) -> str:
25
+ return str(list(self))
26
+
27
+ def __repr__(self) -> str:
28
+ return (
29
+ f"{type(self).__name__}(a={self._a}, b={self._b}, c={self._c})")
30
+
31
+ def __len__(self) -> int:
32
+ return 3
33
+
34
+ def __iter__(self) -> Iterator[int]:
35
+ return iter((self._a, self._b, self._c))
36
+
37
+ def __neg__(self) -> Vec3i:
38
+ return type(self)(*(-i for i in self))
39
+
40
+ def __abs__(self) -> Vec3i:
41
+ return type(self)(*(abs(i) for i in self))
42
+
43
+ def __array__(self, dtype: type | None = None, copy: bool = True):
44
+ arr = np.array(tuple(self), dtype=dtype)
45
+ return arr.copy() if copy else arr
46
+
47
+ def __add__(self, other) -> Vec3i:
48
+ return type(self)(*(np.array(self) + other))
49
+
50
+ def __radd__(self, other) -> Vec3i:
51
+ return self.__add__(other)
52
+
53
+ def __sub__(self, other) -> Vec3i:
54
+ return type(self)(*(np.array(self) - other))
55
+
56
+ def __rsub__(self, other) -> Vec3i:
57
+ return -self.__sub__(other)
58
+
59
+ def __mul__(self, other) -> Vec3i:
60
+ return type(self)(*(np.array(self) * other))
61
+
62
+ def __rmul__(self, other) -> Vec3i:
63
+ return self.__mul__(other)
64
+
65
+ def __floordiv__(self, other) -> Vec3i:
66
+ return type(self)(*(np.array(self) // other))
67
+
68
+ def __rfloordiv__(self, other) -> Vec3i:
69
+ return type(self)(*(other // np.array(self)))
70
+
71
+ def __truediv__(self, other) -> Vec3i:
72
+ return self.__floordiv__(other)
73
+
74
+ def __rtruediv__(self, other) -> Vec3i:
75
+ return self.__rfloordiv__(other)
76
+
77
+ def __mod__(self, other) -> Vec3i:
78
+ return type(self)(*(np.array(self) % other))
79
+
80
+ def __rmod__(self, other) -> Vec3i:
81
+ return type(self)(*(other % np.array(self)))
82
+
83
+ def __eq__(self, other):
84
+ return np.array(self) == other
85
+
86
+ def __ne__(self, other):
87
+ return np.invert(self.__eq__(other))
88
+
89
+ def __lt__(self, other):
90
+ return np.array(self) < other
91
+
92
+ def __le__(self, other):
93
+ return np.array(self) <= other
94
+
95
+ def __gt__(self, other):
96
+ return np.array(self) > other
97
+
98
+ def __ge__(self, other):
99
+ return np.array(self) >= other
100
+
101
+ def to_nbt(self) -> nbtlib.Compound:
102
+ return nbtlib.Compound({
103
+ "x": nbtlib.Int(self._a),
104
+ "y": nbtlib.Int(self._b),
105
+ "z": nbtlib.Int(self._c),
106
+ })
107
+
108
+ @classmethod
109
+ def from_nbt(cls, nbt: nbtlib.Compound) -> Vec3i:
110
+ return cls(int(nbt["x"]), int(nbt["y"]), int(nbt["z"]))
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class BlockPosition(Vec3i):
115
+
116
+ @property
117
+ def x(self) -> int:
118
+ return self._a
119
+
120
+ @property
121
+ def y(self) -> int:
122
+ return self._b
123
+
124
+ @property
125
+ def z(self) -> int:
126
+ return self._c
127
+
128
+ def __repr__(self) -> str:
129
+ return f"{type(self).__name__}(x={self.x}, y={self.y}, z={self.z})"
130
+
131
+
132
+ class Direction(BlockPosition, enum.Enum):
133
+ NORTH = 0, 0, -1
134
+ SOUTH = 0, 0, 1
135
+ WEST = -1, 0, 0
136
+ EAST = 1, 0, 0
137
+ UP = 0, 1, 0
138
+ DOWN = 0, -1, 0
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class Size3D(Vec3i):
143
+
144
+ @property
145
+ def width(self) -> int:
146
+ return self._a
147
+
148
+ @property
149
+ def height(self) -> int:
150
+ return self._b
151
+
152
+ @property
153
+ def length(self) -> int:
154
+ return self._c
155
+
156
+ def __repr__(self) -> str:
157
+ return (
158
+ f"{type(self).__name__}("
159
+ f"width={self.width}, height={self.height}, length={self.length})")
160
+
161
+ @property
162
+ def volume(self) -> int:
163
+ return abs(self.width * self.height * self.length)
164
+
165
+ @property
166
+ def limit(self) -> BlockPosition:
167
+ return BlockPosition(*(self - self.sign))
168
+
169
+ @property
170
+ def sign(self) -> BlockPosition:
171
+ return BlockPosition(*np.sign(self))
@@ -0,0 +1,52 @@
1
+ import functools
2
+
3
+
4
+ class cached_property:
5
+ def __init__(self, func):
6
+ self.func = func
7
+ self.name = func.__name__
8
+ functools.update_wrapper(self, func)
9
+
10
+ def __get__(self, instance, owner=None):
11
+ if instance is None:
12
+ return self
13
+
14
+ if not hasattr(instance, "_cache"):
15
+ raise AttributeError("Instance must have a '_cache' attribute")
16
+
17
+ cache = instance._cache
18
+ if self.name not in cache:
19
+ cache[self.name] = self.func(instance)
20
+ return cache[self.name]
21
+
22
+
23
+ def clears_cache(func):
24
+ @functools.wraps(func)
25
+ def wrapper(self, *args, **kwargs):
26
+ if hasattr(self, "_cache"):
27
+ self._cache.clear()
28
+ return func(self, *args, **kwargs)
29
+ return wrapper
30
+
31
+
32
+ class PropertyCache(dict):
33
+ def __getattr__(self, name):
34
+ try:
35
+ return self[name]
36
+ except KeyError:
37
+ raise AttributeError(f"No cached value for {name!r}")
38
+
39
+ def __setattr__(self, name, value):
40
+ self[name] = value
41
+
42
+ def __delattr__(self, name):
43
+ try:
44
+ del self[name]
45
+ except KeyError:
46
+ raise AttributeError(f"No cached value named {name!r}")
47
+
48
+
49
+ class PropertyCacheMixin:
50
+ def __init__(self, *args, **kwargs) -> None:
51
+ super().__init__(*args, **kwargs)
52
+ self._cache = PropertyCache()