amulet-core 1.9.19__py3-none-any.whl → 1.9.20__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.

Potentially problematic release.


This version of amulet-core might be problematic. Click here for more details.

Files changed (198) hide show
  1. amulet/__init__.py +27 -27
  2. amulet/__pyinstaller/__init__.py +2 -2
  3. amulet/__pyinstaller/hook-amulet.py +4 -4
  4. amulet/_version.py +21 -21
  5. amulet/api/__init__.py +2 -2
  6. amulet/api/abstract_base_entity.py +128 -128
  7. amulet/api/block.py +630 -630
  8. amulet/api/block_entity.py +71 -71
  9. amulet/api/cache.py +107 -107
  10. amulet/api/chunk/__init__.py +6 -6
  11. amulet/api/chunk/biomes.py +207 -207
  12. amulet/api/chunk/block_entity_dict.py +175 -175
  13. amulet/api/chunk/blocks.py +46 -46
  14. amulet/api/chunk/chunk.py +389 -389
  15. amulet/api/chunk/entity_list.py +75 -75
  16. amulet/api/chunk/status.py +167 -167
  17. amulet/api/data_types/__init__.py +4 -4
  18. amulet/api/data_types/generic_types.py +4 -4
  19. amulet/api/data_types/operation_types.py +16 -16
  20. amulet/api/data_types/world_types.py +49 -49
  21. amulet/api/data_types/wrapper_types.py +71 -71
  22. amulet/api/entity.py +74 -74
  23. amulet/api/errors.py +119 -119
  24. amulet/api/history/__init__.py +36 -36
  25. amulet/api/history/base/__init__.py +3 -3
  26. amulet/api/history/base/base_history.py +26 -26
  27. amulet/api/history/base/history_manager.py +63 -63
  28. amulet/api/history/base/revision_manager.py +73 -73
  29. amulet/api/history/changeable.py +15 -15
  30. amulet/api/history/data_types.py +7 -7
  31. amulet/api/history/history_manager/__init__.py +3 -3
  32. amulet/api/history/history_manager/container.py +102 -102
  33. amulet/api/history/history_manager/database.py +279 -279
  34. amulet/api/history/history_manager/meta.py +93 -93
  35. amulet/api/history/history_manager/object.py +116 -116
  36. amulet/api/history/revision_manager/__init__.py +2 -2
  37. amulet/api/history/revision_manager/disk.py +33 -33
  38. amulet/api/history/revision_manager/ram.py +12 -12
  39. amulet/api/item.py +75 -75
  40. amulet/api/level/__init__.py +4 -4
  41. amulet/api/level/base_level/__init__.py +1 -1
  42. amulet/api/level/base_level/base_level.py +1035 -1026
  43. amulet/api/level/base_level/chunk_manager.py +227 -227
  44. amulet/api/level/base_level/clone.py +389 -389
  45. amulet/api/level/base_level/player_manager.py +101 -101
  46. amulet/api/level/immutable_structure/__init__.py +1 -1
  47. amulet/api/level/immutable_structure/immutable_structure.py +94 -94
  48. amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
  49. amulet/api/level/structure.py +22 -22
  50. amulet/api/level/world.py +19 -19
  51. amulet/api/partial_3d_array/__init__.py +2 -2
  52. amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
  53. amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
  54. amulet/api/partial_3d_array/data_types.py +15 -15
  55. amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
  56. amulet/api/partial_3d_array/util.py +152 -152
  57. amulet/api/player.py +65 -65
  58. amulet/api/registry/__init__.py +2 -2
  59. amulet/api/registry/base_registry.py +34 -34
  60. amulet/api/registry/biome_manager.py +153 -153
  61. amulet/api/registry/block_manager.py +156 -156
  62. amulet/api/selection/__init__.py +2 -2
  63. amulet/api/selection/abstract_selection.py +315 -315
  64. amulet/api/selection/box.py +805 -805
  65. amulet/api/selection/group.py +488 -488
  66. amulet/api/structure.py +37 -37
  67. amulet/api/wrapper/__init__.py +8 -8
  68. amulet/api/wrapper/chunk/interface.py +441 -441
  69. amulet/api/wrapper/chunk/translator.py +567 -567
  70. amulet/api/wrapper/format_wrapper.py +772 -772
  71. amulet/api/wrapper/structure_format_wrapper.py +116 -116
  72. amulet/api/wrapper/world_format_wrapper.py +63 -63
  73. amulet/level/__init__.py +1 -1
  74. amulet/level/formats/anvil_forge_world.py +40 -40
  75. amulet/level/formats/anvil_world/__init__.py +3 -3
  76. amulet/level/formats/anvil_world/_sector_manager.py +291 -384
  77. amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
  78. amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
  79. amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
  80. amulet/level/formats/anvil_world/dimension.py +177 -177
  81. amulet/level/formats/anvil_world/format.py +769 -769
  82. amulet/level/formats/anvil_world/region.py +384 -384
  83. amulet/level/formats/construction/__init__.py +3 -3
  84. amulet/level/formats/construction/format_wrapper.py +515 -515
  85. amulet/level/formats/construction/interface.py +134 -134
  86. amulet/level/formats/construction/section.py +60 -60
  87. amulet/level/formats/construction/util.py +165 -165
  88. amulet/level/formats/leveldb_world/__init__.py +3 -3
  89. amulet/level/formats/leveldb_world/chunk.py +33 -33
  90. amulet/level/formats/leveldb_world/dimension.py +385 -419
  91. amulet/level/formats/leveldb_world/format.py +659 -641
  92. amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
  93. amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
  94. amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
  95. amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
  96. amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
  97. amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
  98. amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
  99. amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
  100. amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
  101. amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
  102. amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
  103. amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
  104. amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
  105. amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
  106. amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
  107. amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
  108. amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
  109. amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
  110. amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
  111. amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
  112. amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
  113. amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
  114. amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
  115. amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
  116. amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
  117. amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
  118. amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
  119. amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
  120. amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
  121. amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
  122. amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
  123. amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
  124. amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
  125. amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
  126. amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
  127. amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
  128. amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
  129. amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
  130. amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
  131. amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
  132. amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
  133. amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
  134. amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
  135. amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
  136. amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
  137. amulet/level/formats/mcstructure/__init__.py +3 -3
  138. amulet/level/formats/mcstructure/chunk.py +50 -50
  139. amulet/level/formats/mcstructure/format_wrapper.py +408 -408
  140. amulet/level/formats/mcstructure/interface.py +175 -175
  141. amulet/level/formats/schematic/__init__.py +3 -3
  142. amulet/level/formats/schematic/chunk.py +55 -55
  143. amulet/level/formats/schematic/data_types.py +4 -4
  144. amulet/level/formats/schematic/format_wrapper.py +373 -373
  145. amulet/level/formats/schematic/interface.py +142 -142
  146. amulet/level/formats/sponge_schem/__init__.py +4 -4
  147. amulet/level/formats/sponge_schem/chunk.py +62 -62
  148. amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
  149. amulet/level/formats/sponge_schem/interface.py +118 -118
  150. amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
  151. amulet/level/formats/sponge_schem/varint/varint.py +87 -87
  152. amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
  153. amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
  154. amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
  155. amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
  156. amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
  157. amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
  158. amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
  159. amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
  160. amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
  161. amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
  162. amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
  163. amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
  164. amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
  165. amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
  166. amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
  167. amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
  168. amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
  169. amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
  170. amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
  171. amulet/level/load.py +59 -59
  172. amulet/level/loader.py +95 -95
  173. amulet/level/translators/chunk/bedrock/__init__.py +267 -267
  174. amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
  175. amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
  176. amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
  177. amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
  178. amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
  179. amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
  180. amulet/libs/leveldb/__init__.py +7 -7
  181. amulet/operations/__init__.py +5 -5
  182. amulet/operations/clone.py +18 -18
  183. amulet/operations/delete_chunk.py +32 -32
  184. amulet/operations/fill.py +30 -30
  185. amulet/operations/paste.py +65 -65
  186. amulet/operations/replace.py +58 -58
  187. amulet/utils/__init__.py +14 -14
  188. amulet/utils/format_utils.py +41 -41
  189. amulet/utils/generator.py +15 -15
  190. amulet/utils/matrix.py +243 -243
  191. amulet/utils/numpy_helpers.py +46 -46
  192. amulet/utils/world_utils.py +349 -349
  193. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
  194. amulet_core-1.9.20.dist-info/RECORD +208 -0
  195. amulet_core-1.9.19.dist-info/RECORD +0 -208
  196. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
  197. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
  198. {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/top_level.txt +0 -0
@@ -1,772 +1,772 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from typing import (
5
- Tuple,
6
- Any,
7
- Generator,
8
- Dict,
9
- List,
10
- Optional,
11
- TYPE_CHECKING,
12
- Iterable,
13
- Callable,
14
- Type,
15
- Union,
16
- TypeVar,
17
- Generic,
18
- )
19
- import copy
20
- import numpy
21
- import os
22
- import warnings
23
- import logging
24
-
25
- import PyMCTranslate
26
-
27
- from amulet.api import level as api_level, wrapper as api_wrapper
28
- from amulet.api.chunk import Chunk
29
- from amulet.api.registry import BlockManager
30
- from amulet.api.errors import (
31
- ChunkLoadError,
32
- ChunkDoesNotExist,
33
- ObjectReadError,
34
- ObjectReadWriteError,
35
- PlayerDoesNotExist,
36
- PlayerLoadError,
37
- EntryLoadError,
38
- EntryDoesNotExist,
39
- DimensionDoesNotExist,
40
- )
41
- from amulet.api.data_types import (
42
- AnyNDArray,
43
- VersionNumberAny,
44
- ChunkCoordinates,
45
- Dimension,
46
- PlatformType,
47
- )
48
- from amulet.api.selection import SelectionGroup, SelectionBox
49
- from amulet.api.player import Player
50
-
51
- if TYPE_CHECKING:
52
- from amulet.api.wrapper.chunk.translator import Translator
53
-
54
- log = logging.getLogger(__name__)
55
-
56
- DefaultSelection = SelectionGroup(
57
- SelectionBox((-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000))
58
- )
59
-
60
- VersionNumberT = TypeVar("VersionNumberT", int, Tuple[int, ...])
61
-
62
-
63
- class FormatWrapper(Generic[VersionNumberT], ABC):
64
- """
65
- The FormatWrapper class is a class that sits between the serialised world or structure data and the program using amulet-core.
66
-
67
- It is used to access data from the serialised source in the universal format and write them back again.
68
- """
69
-
70
- _platform: Optional[PlatformType]
71
- _version: Optional[VersionNumberT]
72
-
73
- def __init__(self, path: str):
74
- """
75
- Construct a new instance of :class:`FormatWrapper`.
76
-
77
- This should not be used directly. You should instead use :func:`amulet.load_format`.
78
-
79
- :param path: The file path to the serialised data.
80
- """
81
- self._path = path
82
- self._is_open = False
83
- self._has_lock = False
84
- self._translation_manager = None
85
- self._platform = None
86
- self._version = None
87
- self._bounds: Dict[Dimension, SelectionGroup] = {}
88
- self._changed: bool = False
89
-
90
- @property
91
- def sub_chunk_size(self) -> int:
92
- """
93
- The dimensions of a sub-chunk.
94
- """
95
- return 16
96
-
97
- @property
98
- def path(self) -> str:
99
- """The path to the data on disk."""
100
- return self._path
101
-
102
- @property
103
- @abstractmethod
104
- def level_name(self) -> str:
105
- """The name of the level."""
106
- raise NotImplementedError
107
-
108
- @property
109
- def translation_manager(self) -> PyMCTranslate.TranslationManager:
110
- """The translation manager attached to the world."""
111
- if self._translation_manager is None:
112
- self._translation_manager = PyMCTranslate.new_translation_manager()
113
- return self._translation_manager
114
-
115
- @translation_manager.setter
116
- def translation_manager(self, value: PyMCTranslate.TranslationManager):
117
- # TODO: this should not be settable.
118
- self._translation_manager = value
119
-
120
- @property
121
- def exists(self) -> bool:
122
- """Does some data exist at the specified path."""
123
- return os.path.exists(self.path)
124
-
125
- @staticmethod
126
- @abstractmethod
127
- def is_valid(path: str) -> bool:
128
- """
129
- Returns whether this format wrapper is able to load the given data.
130
-
131
- :param path: The path of the data to load.
132
- :return: True if the world can be loaded by this format wrapper, False otherwise.
133
- """
134
- raise NotImplementedError
135
-
136
- @property
137
- @abstractmethod
138
- def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
139
- """
140
- The valid platform and version combinations that this object can accept.
141
-
142
- This is used when setting the platform and version in the create_and_open method
143
- to verify that the platform and version are valid.
144
-
145
- :return: A dictionary mapping the platform to a tuple of two booleans to determine if numerical and blockstate are valid respectively.
146
- """
147
- raise NotImplementedError
148
-
149
- @property
150
- def platform(self) -> PlatformType:
151
- """Platform string the data is stored in (eg "bedrock" / "java" / ...)"""
152
- if self._platform is None:
153
- raise Exception(
154
- "Cannot access the game platform until the level has been loaded."
155
- )
156
- return self._platform
157
-
158
- @property
159
- def version(self) -> VersionNumberT:
160
- """The version number for the given platform the data is stored in eg (1, 16, 2)"""
161
- if self._version is None:
162
- raise Exception(
163
- "Cannot access the game version until the level has been loaded."
164
- )
165
- return self._version
166
-
167
- @property
168
- def max_world_version(self) -> Tuple[PlatformType, VersionNumberT]:
169
- """
170
- The version the world was last opened in.
171
-
172
- This should be greater than or equal to the chunk versions found within.
173
- """
174
- return self.platform, self.version
175
-
176
- @property
177
- def changed(self) -> bool:
178
- """Has any data been pushed to the format wrapper that has not been saved to disk."""
179
- return self._changed
180
-
181
- @property
182
- @abstractmethod
183
- def dimensions(self) -> List[Dimension]:
184
- """A list of all the dimensions contained in the world."""
185
- raise NotImplementedError
186
-
187
- @property
188
- @abstractmethod
189
- def can_add_dimension(self) -> bool:
190
- """
191
- Can external code register a new dimension.
192
-
193
- If False :meth:`register_dimension` will have no effect.
194
- """
195
- raise NotImplementedError
196
-
197
- @abstractmethod
198
- def register_dimension(self, dimension_identifier: Any):
199
- """
200
- Register a new dimension.
201
-
202
- :param dimension_identifier: The identifier for the dimension.
203
- """
204
- raise NotImplementedError
205
-
206
- @property
207
- def requires_selection(self) -> bool:
208
- """Does this object require that a selection be defined when creating it from scratch?"""
209
- return False
210
-
211
- @property
212
- def multi_selection(self) -> bool:
213
- """
214
- Does this object support having multiple selection boxes.
215
-
216
- If False it will be given exactly 1 selection.
217
-
218
- If True can be given 0 or more.
219
- """
220
- return False
221
-
222
- @property
223
- def selection(self) -> SelectionGroup:
224
- """The area that all chunk data must fit within."""
225
- warnings.warn(
226
- "FormatWrapper.selection is depreciated and will be removed in the future. Please use FormatWrapper.bounds(dimension) instead",
227
- DeprecationWarning,
228
- )
229
- return self.bounds(self.dimensions[0])
230
-
231
- def bounds(self, dimension: Dimension) -> SelectionGroup:
232
- if dimension not in self._bounds:
233
- if dimension in self.dimensions:
234
- raise Exception(
235
- f'The dimension exists but there is no selection registered for it. Please report this to a developer "{dimension}" {self}'
236
- )
237
- else:
238
- raise DimensionDoesNotExist
239
- return self._bounds[dimension]
240
-
241
- @abstractmethod
242
- def _get_interface(
243
- self, raw_chunk_data: Optional[Any] = None
244
- ) -> api_wrapper.Interface:
245
- raise NotImplementedError
246
-
247
- def _get_interface_and_translator(
248
- self, raw_chunk_data=None
249
- ) -> Tuple[api_wrapper.Interface, "Translator", VersionNumberAny]:
250
- interface = self._get_interface(raw_chunk_data)
251
- translator, version_identifier = interface.get_translator(
252
- self.max_world_version, raw_chunk_data
253
- )
254
- return interface, translator, version_identifier
255
-
256
- def create_and_open(
257
- self,
258
- platform: PlatformType,
259
- version: VersionNumberAny,
260
- bounds: Union[
261
- SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
262
- ] = None,
263
- overwrite: bool = False,
264
- **kwargs,
265
- ):
266
- """
267
- Remove the data at the path and set up a new database.
268
-
269
- You might want to call :attr:`exists` to check if something exists at the path
270
- and warn the user they are going to overwrite existing data before calling this method.
271
-
272
- :param platform: The platform the data should use.
273
- :param version: The version the data should use.
274
- :param bounds: The bounds for each dimension. If one :class:`SelectionGroup` is given it will be applied to all dimensions.
275
- :param overwrite: Should an existing database be overwritten. If this is False and one exists and error will be thrown.
276
- :param kwargs: Extra arguments as each implementation requires.
277
- :return:
278
- """
279
- if self.is_open:
280
- raise ObjectReadError(f"Cannot open {self} because it was already opened.")
281
-
282
- if (
283
- platform not in self.valid_formats or len(self.valid_formats[platform]) < 2
284
- ): # check that the platform and version are valid
285
- raise ObjectReadError(
286
- f"{platform} is not a valid platform for this wrapper."
287
- )
288
- translator_version = self.translation_manager.get_version(platform, version)
289
- if translator_version.has_abstract_format: # numerical
290
- if not self.valid_formats[platform][0]:
291
- raise ObjectReadError(
292
- f"The version given ({version}) is from the numerical format but this wrapper does not support the numerical format."
293
- )
294
- else:
295
- if not self.valid_formats[platform][1]:
296
- raise ObjectReadError(
297
- f"The version given ({version}) is from the blockstate format but this wrapper does not support the blockstate format."
298
- )
299
-
300
- self._platform = translator_version.platform
301
- self._version = translator_version.version_number
302
- self._create(overwrite, bounds, **kwargs)
303
-
304
- def _clean_selection(self, selection: SelectionGroup) -> SelectionGroup:
305
- if self.multi_selection:
306
- return selection
307
- else:
308
- if selection:
309
- return SelectionGroup(
310
- sorted(
311
- selection.selection_boxes,
312
- reverse=True,
313
- key=lambda b: b.volume,
314
- )[0]
315
- )
316
- else:
317
- raise ObjectReadError(
318
- "A single selection was required but none were given."
319
- )
320
-
321
- @abstractmethod
322
- def _create(
323
- self,
324
- overwrite: bool,
325
- bounds: Union[
326
- SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
327
- ] = None,
328
- **kwargs,
329
- ):
330
- """Set up the database from scratch."""
331
- raise NotImplementedError
332
-
333
- def open(self):
334
- """Open the database for reading and writing."""
335
- if self.is_open:
336
- raise ObjectReadError(f"Cannot open {self} because it was already opened.")
337
- self._open()
338
-
339
- @abstractmethod
340
- def _open(self):
341
- raise NotImplementedError
342
-
343
- @property
344
- def is_open(self) -> bool:
345
- """Has the object been opened."""
346
- return self._is_open
347
-
348
- @property
349
- def has_lock(self) -> bool:
350
- """Verify that the world database can be read and written"""
351
- return self._has_lock
352
-
353
- def _verify_has_lock(self):
354
- """
355
- Ensure that the FormatWrapper is open and has a lock on the object.
356
-
357
- :raises:
358
- ObjectReadWriteError: if the FormatWrapper does not have a lock on the object.
359
- """
360
- if not self.is_open:
361
- raise ObjectReadWriteError(
362
- f"The object {self} was never opened. Call .open or .create_and_open to open it before accessing data."
363
- )
364
- elif not self.has_lock:
365
- raise ObjectReadWriteError(
366
- f"The lock on the object {self} has been lost. It was probably opened somewhere else."
367
- )
368
-
369
- @staticmethod
370
- def pre_save_operation(level: api_level.BaseLevel) -> Generator[float, None, bool]:
371
- """
372
- Logic to run before saving. Eg recalculating height maps or lighting.
373
-
374
- Must be a generator that yields a number and returns a bool.
375
-
376
- The yielded number is the progress from 0 to 1.
377
-
378
- The returned bool is if changes have been made.
379
-
380
- :param level: The level to apply modifications to.
381
- :return: Have any modifications been made.
382
- """
383
- yield 1
384
- return False
385
-
386
- def save(self):
387
- """Save the data back to the level."""
388
- self._verify_has_lock()
389
- self._save()
390
- self._changed = False
391
-
392
- @abstractmethod
393
- def _save(self):
394
- raise NotImplementedError
395
-
396
- def close(self):
397
- """Close the level."""
398
- if self.is_open:
399
- self._is_open = False
400
- self._has_lock = False
401
- self._close()
402
-
403
- @abstractmethod
404
- def _close(self):
405
- raise NotImplementedError
406
-
407
- @abstractmethod
408
- def unload(self):
409
- """Unload data stored in the FormatWrapper class"""
410
- raise NotImplementedError
411
-
412
- @abstractmethod
413
- def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
414
- """A generator of all chunk coords in the given dimension."""
415
- raise NotImplementedError
416
-
417
- @abstractmethod
418
- def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
419
- """
420
- Does the chunk exist in the world database?
421
-
422
- :param cx: The x coordinate of the chunk.
423
- :param cz: The z coordinate of the chunk.
424
- :param dimension: The dimension to load the chunk from.
425
- :return: True if the chunk exists. Calling load_chunk on this chunk may still throw ChunkLoadError
426
- """
427
- raise NotImplementedError
428
-
429
- def _safe_load(
430
- self,
431
- meth: Callable,
432
- args: Tuple[Any, ...],
433
- msg: str,
434
- load_error: Type[EntryLoadError],
435
- does_not_exist_error: Type[EntryDoesNotExist],
436
- ):
437
- try:
438
- self._verify_has_lock()
439
- except ObjectReadWriteError as e:
440
- raise does_not_exist_error(e)
441
- try:
442
- return meth(*args)
443
- except does_not_exist_error as e:
444
- raise e
445
- except Exception as e:
446
- log.error(msg.format(*args), exc_info=True)
447
- raise load_error(e) from e
448
-
449
- def load_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
450
- """
451
- Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
452
-
453
- :param cx: The x coordinate of the chunk.
454
- :param cz: The z coordinate of the chunk.
455
- :param dimension: The dimension to load the chunk from.
456
- :return: The chunk at the given coordinates.
457
- :raises:
458
- ChunkDoesNotExist: If the chunk does not exist (was deleted or never created)
459
- ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
460
- """
461
- return self._safe_load(
462
- self._load_chunk,
463
- (cx, cz, dimension),
464
- "Error loading chunk {} {} {}",
465
- ChunkLoadError,
466
- ChunkDoesNotExist,
467
- )
468
-
469
- def _load_chunk(
470
- self, cx: int, cz: int, dimension: Dimension, recurse: bool = True
471
- ) -> Chunk:
472
- """
473
- Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
474
-
475
- :param cx: The x coordinate of the chunk.
476
- :param cz: The z coordinate of the chunk.
477
- :param dimension: The dimension to load the chunk from.
478
- :param recurse: bool: look in boundary chunks if required to fully define data
479
- :return: The chunk at the given coordinates.
480
- """
481
-
482
- # Gets an interface (the code that actually reads the chunk data)
483
- raw_chunk_data = self._get_raw_chunk_data(cx, cz, dimension)
484
- interface, translator, game_version = self._get_interface_and_translator(
485
- raw_chunk_data
486
- )
487
-
488
- # decode the raw chunk data into the universal format
489
- chunk, block_palette = self._decode(
490
- interface, dimension, cx, cz, raw_chunk_data
491
- )
492
- block_palette: AnyNDArray
493
- chunk = self._unpack(translator, game_version, chunk, block_palette)
494
- return self._convert_to_load(
495
- chunk, translator, game_version, dimension, recurse=recurse
496
- )
497
-
498
- @staticmethod
499
- def _decode(
500
- interface: api_wrapper.Interface,
501
- dimension: Dimension,
502
- cx: int,
503
- cz: int,
504
- raw_chunk_data: Any,
505
- ) -> Tuple[Chunk, AnyNDArray]:
506
- return interface.decode(cx, cz, raw_chunk_data)
507
-
508
- def _unpack(
509
- self,
510
- translator: "Translator",
511
- game_version: VersionNumberAny,
512
- chunk: Chunk,
513
- block_palette: AnyNDArray,
514
- ) -> Chunk:
515
- return translator.unpack(
516
- game_version, self.translation_manager, chunk, block_palette
517
- )
518
-
519
- def _convert_to_load(
520
- self,
521
- chunk: Chunk,
522
- translator: "Translator",
523
- game_version: VersionNumberAny,
524
- dimension: Dimension,
525
- recurse: bool = True,
526
- ) -> Chunk:
527
- # set up a callback that translator can use to get chunk data
528
- cx, cz = chunk.cx, chunk.cz
529
- if recurse:
530
- chunk_cache: Dict[ChunkCoordinates, Chunk] = {}
531
-
532
- def get_chunk_callback(x: int, z: int) -> Chunk:
533
- cx_, cz_ = cx + x, cz + z
534
- if (cx_, cz_) not in chunk_cache:
535
- chunk_cache[(cx_, cz_)] = self._load_chunk(
536
- cx_, cz_, dimension, recurse=False
537
- )
538
- return chunk_cache[(cx_, cz_)]
539
-
540
- else:
541
- get_chunk_callback = None
542
-
543
- # translate the data to universal format
544
- chunk = translator.to_universal(
545
- game_version, self.translation_manager, chunk, get_chunk_callback, recurse
546
- )
547
-
548
- chunk.changed = False
549
- return chunk
550
-
551
- def commit_chunk(self, chunk: Chunk, dimension: Dimension):
552
- """
553
- Save a universal format chunk to the FormatWrapper database (not the level database)
554
-
555
- Call save method to write changed chunks back to the level.
556
-
557
- :param chunk: The chunk object to translate and save.
558
- :param dimension: The dimension to commit the chunk to.
559
- """
560
- try:
561
- self._verify_has_lock()
562
- except ObjectReadWriteError as e:
563
- log.error(e)
564
- try:
565
- self._commit_chunk(copy.deepcopy(chunk), dimension)
566
- except Exception:
567
- log.error(f"Error saving chunk {chunk}", exc_info=True)
568
- self._changed = True
569
-
570
- def _commit_chunk(self, chunk: Chunk, dimension: Dimension, recurse: bool = True):
571
- """
572
- Saves a universal :class:`~amulet.api.chunk.Chunk` object.
573
-
574
- Calls the interface then the translator.
575
-
576
- It then calls _put_chunk_data to store the data returned by the interface
577
- """
578
- # get the coordinates for later
579
- cx, cz = chunk.cx, chunk.cz
580
-
581
- # Gets an interface, translator and most recent chunk version for the game version.
582
- interface, translator, chunk_version = self._get_interface_and_translator()
583
-
584
- chunk = self._convert_to_save(chunk, chunk_version, translator, recurse)
585
- chunk, chunk_palette = self._pack(chunk, translator, chunk_version)
586
- raw_chunk_data = self._encode(interface, chunk, dimension, chunk_palette)
587
-
588
- self._put_raw_chunk_data(cx, cz, raw_chunk_data, dimension)
589
-
590
- def _convert_to_save(
591
- self,
592
- chunk: Chunk,
593
- chunk_version: VersionNumberAny,
594
- translator: "Translator",
595
- recurse: bool = True,
596
- ) -> Chunk:
597
- """Convert the Chunk in Universal format to a Chunk in the version specific format."""
598
- # create a new streamlined block block_palette and remap the data
599
- palette: List[numpy.ndarray] = []
600
- palette_len = 0
601
- for cy in chunk.blocks.sub_chunks:
602
- sub_chunk_palette, sub_chunk = numpy.unique(
603
- chunk.blocks.get_sub_chunk(cy), return_inverse=True
604
- )
605
- chunk.blocks.add_sub_chunk(
606
- cy, sub_chunk.astype(numpy.uint32).reshape((16, 16, 16)) + palette_len
607
- )
608
- palette_len += len(sub_chunk_palette)
609
- palette.append(sub_chunk_palette)
610
-
611
- if palette:
612
- chunk_palette, lut = numpy.unique(
613
- numpy.concatenate(palette), return_inverse=True
614
- )
615
- for cy in chunk.blocks.sub_chunks:
616
- chunk.blocks.add_sub_chunk(
617
- cy, lut.astype(numpy.uint32)[chunk.blocks.get_sub_chunk(cy)]
618
- )
619
- chunk._block_palette = BlockManager(
620
- numpy.vectorize(chunk.block_palette.__getitem__)(chunk_palette)
621
- )
622
- else:
623
- chunk._block_palette = BlockManager()
624
-
625
- def get_chunk_callback(_: int, __: int) -> Chunk:
626
- # conversion from universal should not require any data outside the block
627
- return chunk
628
-
629
- # translate from universal format to version format
630
- return translator.from_universal(
631
- chunk_version, self.translation_manager, chunk, get_chunk_callback, recurse
632
- )
633
-
634
- def _pack(
635
- self, chunk: Chunk, translator: "Translator", chunk_version: VersionNumberAny
636
- ) -> Tuple[Chunk, AnyNDArray]:
637
- """Pack the chunk data into the format required by the encoder.
638
- This includes converting the string names to numerical formats for the versions that require it.
639
- """
640
- return translator.pack(chunk_version, self.translation_manager, chunk)
641
-
642
- def _encode(
643
- self,
644
- interface: api_wrapper.Interface,
645
- chunk: Chunk,
646
- dimension: Dimension,
647
- chunk_palette: AnyNDArray,
648
- ) -> Any:
649
- """Encode the data to the raw format as saved on disk."""
650
- raise NotImplementedError
651
-
652
- def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
653
- """
654
- Delete the given chunk from the level.
655
-
656
- :param cx: The x coordinate of the chunk.
657
- :param cz: The z coordinate of the chunk.
658
- :param dimension: The dimension to load the data from.
659
- """
660
- self._delete_chunk(cx, cz, dimension)
661
- self._changed = True
662
-
663
- @abstractmethod
664
- def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
665
- raise NotImplementedError
666
-
667
- def put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
668
- """
669
- Commit the raw chunk data to the FormatWrapper cache.
670
-
671
- Call :meth:`save` to push all the cache data to the level.
672
-
673
- :param cx: The x coordinate of the chunk.
674
- :param cz: The z coordinate of the chunk.
675
- :param data: The raw data to commit to the level.
676
- :param dimension: The dimension to load the data from.
677
- """
678
- self._verify_has_lock()
679
- self._put_raw_chunk_data(cx, cz, data, dimension)
680
-
681
- @abstractmethod
682
- def _put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
683
- raise NotImplementedError
684
-
685
- def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
686
- """
687
- Return the raw data as loaded from disk.
688
-
689
- :param cx: The x coordinate of the chunk.
690
- :param cz: The z coordinate of the chunk.
691
- :param dimension: The dimension to load the data from.
692
- :return: The raw chunk data.
693
- """
694
- return self._safe_load(
695
- self._get_raw_chunk_data,
696
- (cx, cz, dimension),
697
- "Error loading chunk {} {} {}",
698
- ChunkLoadError,
699
- ChunkDoesNotExist,
700
- )
701
-
702
- @abstractmethod
703
- def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
704
- """
705
- Return the raw data as loaded from disk.
706
-
707
- :param cx: The x coordinate of the chunk.
708
- :param cz: The z coordinate of the chunk.
709
- :param dimension: The dimension to load the data from.
710
- :return: The raw chunk data.
711
- """
712
- raise NotImplementedError
713
-
714
- @abstractmethod
715
- def all_player_ids(self) -> Iterable[str]:
716
- """
717
- Returns a set of all player ids that are present in the level
718
- """
719
- return NotImplemented
720
-
721
- @abstractmethod
722
- def has_player(self, player_id: str) -> bool:
723
- """
724
- Test if a player id is present in the level.
725
- """
726
- return NotImplemented
727
-
728
- def load_player(self, player_id: str) -> "Player":
729
- """
730
- Gets the :class:`Player` object that belongs to the specified player id
731
-
732
- If no parameter is supplied, the data of the local player should be returned
733
-
734
- :param player_id: The desired player id
735
- :return: A Player instance
736
- """
737
- return self._safe_load(
738
- self._load_player,
739
- (player_id,),
740
- "Error loading player {}",
741
- PlayerLoadError,
742
- PlayerDoesNotExist,
743
- )
744
-
745
- @abstractmethod
746
- def _load_player(self, player_id: str) -> "Player":
747
- """
748
- Get the raw player data and unpack it into a Player class.
749
-
750
- :param player_id: The id of the player to get.
751
- :return:
752
- """
753
- raise NotImplementedError
754
-
755
- def get_raw_player_data(self, player_id: str) -> Any:
756
- """
757
- Get the player data in the lowest level form.
758
-
759
- :param player_id: The id of the player to get.
760
- :return:
761
- """
762
- return self._safe_load(
763
- self._get_raw_player_data,
764
- (player_id,),
765
- "Error loading player {}",
766
- PlayerLoadError,
767
- PlayerDoesNotExist,
768
- )
769
-
770
- @abstractmethod
771
- def _get_raw_player_data(self, player_id: str) -> Any:
772
- raise NotImplementedError
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import (
5
+ Tuple,
6
+ Any,
7
+ Generator,
8
+ Dict,
9
+ List,
10
+ Optional,
11
+ TYPE_CHECKING,
12
+ Iterable,
13
+ Callable,
14
+ Type,
15
+ Union,
16
+ TypeVar,
17
+ Generic,
18
+ )
19
+ import copy
20
+ import numpy
21
+ import os
22
+ import warnings
23
+ import logging
24
+
25
+ import PyMCTranslate
26
+
27
+ from amulet.api import level as api_level, wrapper as api_wrapper
28
+ from amulet.api.chunk import Chunk
29
+ from amulet.api.registry import BlockManager
30
+ from amulet.api.errors import (
31
+ ChunkLoadError,
32
+ ChunkDoesNotExist,
33
+ ObjectReadError,
34
+ ObjectReadWriteError,
35
+ PlayerDoesNotExist,
36
+ PlayerLoadError,
37
+ EntryLoadError,
38
+ EntryDoesNotExist,
39
+ DimensionDoesNotExist,
40
+ )
41
+ from amulet.api.data_types import (
42
+ AnyNDArray,
43
+ VersionNumberAny,
44
+ ChunkCoordinates,
45
+ Dimension,
46
+ PlatformType,
47
+ )
48
+ from amulet.api.selection import SelectionGroup, SelectionBox
49
+ from amulet.api.player import Player
50
+
51
+ if TYPE_CHECKING:
52
+ from amulet.api.wrapper.chunk.translator import Translator
53
+
54
+ log = logging.getLogger(__name__)
55
+
56
+ DefaultSelection = SelectionGroup(
57
+ SelectionBox((-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000))
58
+ )
59
+
60
+ VersionNumberT = TypeVar("VersionNumberT", int, Tuple[int, ...])
61
+
62
+
63
+ class FormatWrapper(Generic[VersionNumberT], ABC):
64
+ """
65
+ The FormatWrapper class is a class that sits between the serialised world or structure data and the program using amulet-core.
66
+
67
+ It is used to access data from the serialised source in the universal format and write them back again.
68
+ """
69
+
70
+ _platform: Optional[PlatformType]
71
+ _version: Optional[VersionNumberT]
72
+
73
+ def __init__(self, path: str):
74
+ """
75
+ Construct a new instance of :class:`FormatWrapper`.
76
+
77
+ This should not be used directly. You should instead use :func:`amulet.load_format`.
78
+
79
+ :param path: The file path to the serialised data.
80
+ """
81
+ self._path = path
82
+ self._is_open = False
83
+ self._has_lock = False
84
+ self._translation_manager = None
85
+ self._platform = None
86
+ self._version = None
87
+ self._bounds: Dict[Dimension, SelectionGroup] = {}
88
+ self._changed: bool = False
89
+
90
+ @property
91
+ def sub_chunk_size(self) -> int:
92
+ """
93
+ The dimensions of a sub-chunk.
94
+ """
95
+ return 16
96
+
97
+ @property
98
+ def path(self) -> str:
99
+ """The path to the data on disk."""
100
+ return self._path
101
+
102
+ @property
103
+ @abstractmethod
104
+ def level_name(self) -> str:
105
+ """The name of the level."""
106
+ raise NotImplementedError
107
+
108
+ @property
109
+ def translation_manager(self) -> PyMCTranslate.TranslationManager:
110
+ """The translation manager attached to the world."""
111
+ if self._translation_manager is None:
112
+ self._translation_manager = PyMCTranslate.new_translation_manager()
113
+ return self._translation_manager
114
+
115
+ @translation_manager.setter
116
+ def translation_manager(self, value: PyMCTranslate.TranslationManager):
117
+ # TODO: this should not be settable.
118
+ self._translation_manager = value
119
+
120
+ @property
121
+ def exists(self) -> bool:
122
+ """Does some data exist at the specified path."""
123
+ return os.path.exists(self.path)
124
+
125
+ @staticmethod
126
+ @abstractmethod
127
+ def is_valid(path: str) -> bool:
128
+ """
129
+ Returns whether this format wrapper is able to load the given data.
130
+
131
+ :param path: The path of the data to load.
132
+ :return: True if the world can be loaded by this format wrapper, False otherwise.
133
+ """
134
+ raise NotImplementedError
135
+
136
+ @property
137
+ @abstractmethod
138
+ def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
139
+ """
140
+ The valid platform and version combinations that this object can accept.
141
+
142
+ This is used when setting the platform and version in the create_and_open method
143
+ to verify that the platform and version are valid.
144
+
145
+ :return: A dictionary mapping the platform to a tuple of two booleans to determine if numerical and blockstate are valid respectively.
146
+ """
147
+ raise NotImplementedError
148
+
149
+ @property
150
+ def platform(self) -> PlatformType:
151
+ """Platform string the data is stored in (eg "bedrock" / "java" / ...)"""
152
+ if self._platform is None:
153
+ raise Exception(
154
+ "Cannot access the game platform until the level has been loaded."
155
+ )
156
+ return self._platform
157
+
158
+ @property
159
+ def version(self) -> VersionNumberT:
160
+ """The version number for the given platform the data is stored in eg (1, 16, 2)"""
161
+ if self._version is None:
162
+ raise Exception(
163
+ "Cannot access the game version until the level has been loaded."
164
+ )
165
+ return self._version
166
+
167
+ @property
168
+ def max_world_version(self) -> Tuple[PlatformType, VersionNumberT]:
169
+ """
170
+ The version the world was last opened in.
171
+
172
+ This should be greater than or equal to the chunk versions found within.
173
+ """
174
+ return self.platform, self.version
175
+
176
+ @property
177
+ def changed(self) -> bool:
178
+ """Has any data been pushed to the format wrapper that has not been saved to disk."""
179
+ return self._changed
180
+
181
+ @property
182
+ @abstractmethod
183
+ def dimensions(self) -> List[Dimension]:
184
+ """A list of all the dimensions contained in the world."""
185
+ raise NotImplementedError
186
+
187
+ @property
188
+ @abstractmethod
189
+ def can_add_dimension(self) -> bool:
190
+ """
191
+ Can external code register a new dimension.
192
+
193
+ If False :meth:`register_dimension` will have no effect.
194
+ """
195
+ raise NotImplementedError
196
+
197
+ @abstractmethod
198
+ def register_dimension(self, dimension_identifier: Any):
199
+ """
200
+ Register a new dimension.
201
+
202
+ :param dimension_identifier: The identifier for the dimension.
203
+ """
204
+ raise NotImplementedError
205
+
206
+ @property
207
+ def requires_selection(self) -> bool:
208
+ """Does this object require that a selection be defined when creating it from scratch?"""
209
+ return False
210
+
211
+ @property
212
+ def multi_selection(self) -> bool:
213
+ """
214
+ Does this object support having multiple selection boxes.
215
+
216
+ If False it will be given exactly 1 selection.
217
+
218
+ If True can be given 0 or more.
219
+ """
220
+ return False
221
+
222
+ @property
223
+ def selection(self) -> SelectionGroup:
224
+ """The area that all chunk data must fit within."""
225
+ warnings.warn(
226
+ "FormatWrapper.selection is depreciated and will be removed in the future. Please use FormatWrapper.bounds(dimension) instead",
227
+ DeprecationWarning,
228
+ )
229
+ return self.bounds(self.dimensions[0])
230
+
231
+ def bounds(self, dimension: Dimension) -> SelectionGroup:
232
+ if dimension not in self._bounds:
233
+ if dimension in self.dimensions:
234
+ raise Exception(
235
+ f'The dimension exists but there is no selection registered for it. Please report this to a developer "{dimension}" {self}'
236
+ )
237
+ else:
238
+ raise DimensionDoesNotExist
239
+ return self._bounds[dimension]
240
+
241
+ @abstractmethod
242
+ def _get_interface(
243
+ self, raw_chunk_data: Optional[Any] = None
244
+ ) -> api_wrapper.Interface:
245
+ raise NotImplementedError
246
+
247
+ def _get_interface_and_translator(
248
+ self, raw_chunk_data=None
249
+ ) -> Tuple[api_wrapper.Interface, "Translator", VersionNumberAny]:
250
+ interface = self._get_interface(raw_chunk_data)
251
+ translator, version_identifier = interface.get_translator(
252
+ self.max_world_version, raw_chunk_data
253
+ )
254
+ return interface, translator, version_identifier
255
+
256
+ def create_and_open(
257
+ self,
258
+ platform: PlatformType,
259
+ version: VersionNumberAny,
260
+ bounds: Union[
261
+ SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
262
+ ] = None,
263
+ overwrite: bool = False,
264
+ **kwargs,
265
+ ):
266
+ """
267
+ Remove the data at the path and set up a new database.
268
+
269
+ You might want to call :attr:`exists` to check if something exists at the path
270
+ and warn the user they are going to overwrite existing data before calling this method.
271
+
272
+ :param platform: The platform the data should use.
273
+ :param version: The version the data should use.
274
+ :param bounds: The bounds for each dimension. If one :class:`SelectionGroup` is given it will be applied to all dimensions.
275
+ :param overwrite: Should an existing database be overwritten. If this is False and one exists and error will be thrown.
276
+ :param kwargs: Extra arguments as each implementation requires.
277
+ :return:
278
+ """
279
+ if self.is_open:
280
+ raise ObjectReadError(f"Cannot open {self} because it was already opened.")
281
+
282
+ if (
283
+ platform not in self.valid_formats or len(self.valid_formats[platform]) < 2
284
+ ): # check that the platform and version are valid
285
+ raise ObjectReadError(
286
+ f"{platform} is not a valid platform for this wrapper."
287
+ )
288
+ translator_version = self.translation_manager.get_version(platform, version)
289
+ if translator_version.has_abstract_format: # numerical
290
+ if not self.valid_formats[platform][0]:
291
+ raise ObjectReadError(
292
+ f"The version given ({version}) is from the numerical format but this wrapper does not support the numerical format."
293
+ )
294
+ else:
295
+ if not self.valid_formats[platform][1]:
296
+ raise ObjectReadError(
297
+ f"The version given ({version}) is from the blockstate format but this wrapper does not support the blockstate format."
298
+ )
299
+
300
+ self._platform = translator_version.platform
301
+ self._version = translator_version.version_number
302
+ self._create(overwrite, bounds, **kwargs)
303
+
304
+ def _clean_selection(self, selection: SelectionGroup) -> SelectionGroup:
305
+ if self.multi_selection:
306
+ return selection
307
+ else:
308
+ if selection:
309
+ return SelectionGroup(
310
+ sorted(
311
+ selection.selection_boxes,
312
+ reverse=True,
313
+ key=lambda b: b.volume,
314
+ )[0]
315
+ )
316
+ else:
317
+ raise ObjectReadError(
318
+ "A single selection was required but none were given."
319
+ )
320
+
321
+ @abstractmethod
322
+ def _create(
323
+ self,
324
+ overwrite: bool,
325
+ bounds: Union[
326
+ SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
327
+ ] = None,
328
+ **kwargs,
329
+ ):
330
+ """Set up the database from scratch."""
331
+ raise NotImplementedError
332
+
333
+ def open(self):
334
+ """Open the database for reading and writing."""
335
+ if self.is_open:
336
+ raise ObjectReadError(f"Cannot open {self} because it was already opened.")
337
+ self._open()
338
+
339
+ @abstractmethod
340
+ def _open(self):
341
+ raise NotImplementedError
342
+
343
+ @property
344
+ def is_open(self) -> bool:
345
+ """Has the object been opened."""
346
+ return self._is_open
347
+
348
+ @property
349
+ def has_lock(self) -> bool:
350
+ """Verify that the world database can be read and written"""
351
+ return self._has_lock
352
+
353
+ def _verify_has_lock(self):
354
+ """
355
+ Ensure that the FormatWrapper is open and has a lock on the object.
356
+
357
+ :raises:
358
+ ObjectReadWriteError: if the FormatWrapper does not have a lock on the object.
359
+ """
360
+ if not self.is_open:
361
+ raise ObjectReadWriteError(
362
+ f"The object {self} was never opened. Call .open or .create_and_open to open it before accessing data."
363
+ )
364
+ elif not self.has_lock:
365
+ raise ObjectReadWriteError(
366
+ f"The lock on the object {self} has been lost. It was probably opened somewhere else."
367
+ )
368
+
369
+ @staticmethod
370
+ def pre_save_operation(level: api_level.BaseLevel) -> Generator[float, None, bool]:
371
+ """
372
+ Logic to run before saving. Eg recalculating height maps or lighting.
373
+
374
+ Must be a generator that yields a number and returns a bool.
375
+
376
+ The yielded number is the progress from 0 to 1.
377
+
378
+ The returned bool is if changes have been made.
379
+
380
+ :param level: The level to apply modifications to.
381
+ :return: Have any modifications been made.
382
+ """
383
+ yield 1
384
+ return False
385
+
386
+ def save(self):
387
+ """Save the data back to the level."""
388
+ self._verify_has_lock()
389
+ self._save()
390
+ self._changed = False
391
+
392
+ @abstractmethod
393
+ def _save(self):
394
+ raise NotImplementedError
395
+
396
+ def close(self):
397
+ """Close the level."""
398
+ if self.is_open:
399
+ self._is_open = False
400
+ self._has_lock = False
401
+ self._close()
402
+
403
+ @abstractmethod
404
+ def _close(self):
405
+ raise NotImplementedError
406
+
407
+ @abstractmethod
408
+ def unload(self):
409
+ """Unload data stored in the FormatWrapper class"""
410
+ raise NotImplementedError
411
+
412
+ @abstractmethod
413
+ def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
414
+ """A generator of all chunk coords in the given dimension."""
415
+ raise NotImplementedError
416
+
417
+ @abstractmethod
418
+ def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
419
+ """
420
+ Does the chunk exist in the world database?
421
+
422
+ :param cx: The x coordinate of the chunk.
423
+ :param cz: The z coordinate of the chunk.
424
+ :param dimension: The dimension to load the chunk from.
425
+ :return: True if the chunk exists. Calling load_chunk on this chunk may still throw ChunkLoadError
426
+ """
427
+ raise NotImplementedError
428
+
429
+ def _safe_load(
430
+ self,
431
+ meth: Callable,
432
+ args: Tuple[Any, ...],
433
+ msg: str,
434
+ load_error: Type[EntryLoadError],
435
+ does_not_exist_error: Type[EntryDoesNotExist],
436
+ ):
437
+ try:
438
+ self._verify_has_lock()
439
+ except ObjectReadWriteError as e:
440
+ raise does_not_exist_error(e)
441
+ try:
442
+ return meth(*args)
443
+ except does_not_exist_error as e:
444
+ raise e
445
+ except Exception as e:
446
+ log.error(msg.format(*args), exc_info=True)
447
+ raise load_error(e) from e
448
+
449
+ def load_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
450
+ """
451
+ Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
452
+
453
+ :param cx: The x coordinate of the chunk.
454
+ :param cz: The z coordinate of the chunk.
455
+ :param dimension: The dimension to load the chunk from.
456
+ :return: The chunk at the given coordinates.
457
+ :raises:
458
+ ChunkDoesNotExist: If the chunk does not exist (was deleted or never created)
459
+ ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
460
+ """
461
+ return self._safe_load(
462
+ self._load_chunk,
463
+ (cx, cz, dimension),
464
+ "Error loading chunk {} {} {}",
465
+ ChunkLoadError,
466
+ ChunkDoesNotExist,
467
+ )
468
+
469
+ def _load_chunk(
470
+ self, cx: int, cz: int, dimension: Dimension, recurse: bool = True
471
+ ) -> Chunk:
472
+ """
473
+ Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
474
+
475
+ :param cx: The x coordinate of the chunk.
476
+ :param cz: The z coordinate of the chunk.
477
+ :param dimension: The dimension to load the chunk from.
478
+ :param recurse: bool: look in boundary chunks if required to fully define data
479
+ :return: The chunk at the given coordinates.
480
+ """
481
+
482
+ # Gets an interface (the code that actually reads the chunk data)
483
+ raw_chunk_data = self._get_raw_chunk_data(cx, cz, dimension)
484
+ interface, translator, game_version = self._get_interface_and_translator(
485
+ raw_chunk_data
486
+ )
487
+
488
+ # decode the raw chunk data into the universal format
489
+ chunk, block_palette = self._decode(
490
+ interface, dimension, cx, cz, raw_chunk_data
491
+ )
492
+ block_palette: AnyNDArray
493
+ chunk = self._unpack(translator, game_version, chunk, block_palette)
494
+ return self._convert_to_load(
495
+ chunk, translator, game_version, dimension, recurse=recurse
496
+ )
497
+
498
+ @staticmethod
499
+ def _decode(
500
+ interface: api_wrapper.Interface,
501
+ dimension: Dimension,
502
+ cx: int,
503
+ cz: int,
504
+ raw_chunk_data: Any,
505
+ ) -> Tuple[Chunk, AnyNDArray]:
506
+ return interface.decode(cx, cz, raw_chunk_data)
507
+
508
+ def _unpack(
509
+ self,
510
+ translator: "Translator",
511
+ game_version: VersionNumberAny,
512
+ chunk: Chunk,
513
+ block_palette: AnyNDArray,
514
+ ) -> Chunk:
515
+ return translator.unpack(
516
+ game_version, self.translation_manager, chunk, block_palette
517
+ )
518
+
519
+ def _convert_to_load(
520
+ self,
521
+ chunk: Chunk,
522
+ translator: "Translator",
523
+ game_version: VersionNumberAny,
524
+ dimension: Dimension,
525
+ recurse: bool = True,
526
+ ) -> Chunk:
527
+ # set up a callback that translator can use to get chunk data
528
+ cx, cz = chunk.cx, chunk.cz
529
+ if recurse:
530
+ chunk_cache: Dict[ChunkCoordinates, Chunk] = {}
531
+
532
+ def get_chunk_callback(x: int, z: int) -> Chunk:
533
+ cx_, cz_ = cx + x, cz + z
534
+ if (cx_, cz_) not in chunk_cache:
535
+ chunk_cache[(cx_, cz_)] = self._load_chunk(
536
+ cx_, cz_, dimension, recurse=False
537
+ )
538
+ return chunk_cache[(cx_, cz_)]
539
+
540
+ else:
541
+ get_chunk_callback = None
542
+
543
+ # translate the data to universal format
544
+ chunk = translator.to_universal(
545
+ game_version, self.translation_manager, chunk, get_chunk_callback, recurse
546
+ )
547
+
548
+ chunk.changed = False
549
+ return chunk
550
+
551
+ def commit_chunk(self, chunk: Chunk, dimension: Dimension):
552
+ """
553
+ Save a universal format chunk to the FormatWrapper database (not the level database)
554
+
555
+ Call save method to write changed chunks back to the level.
556
+
557
+ :param chunk: The chunk object to translate and save.
558
+ :param dimension: The dimension to commit the chunk to.
559
+ """
560
+ try:
561
+ self._verify_has_lock()
562
+ except ObjectReadWriteError as e:
563
+ log.error(e)
564
+ try:
565
+ self._commit_chunk(copy.deepcopy(chunk), dimension)
566
+ except Exception:
567
+ log.error(f"Error saving chunk {chunk}", exc_info=True)
568
+ self._changed = True
569
+
570
+ def _commit_chunk(self, chunk: Chunk, dimension: Dimension, recurse: bool = True):
571
+ """
572
+ Saves a universal :class:`~amulet.api.chunk.Chunk` object.
573
+
574
+ Calls the interface then the translator.
575
+
576
+ It then calls _put_chunk_data to store the data returned by the interface
577
+ """
578
+ # get the coordinates for later
579
+ cx, cz = chunk.cx, chunk.cz
580
+
581
+ # Gets an interface, translator and most recent chunk version for the game version.
582
+ interface, translator, chunk_version = self._get_interface_and_translator()
583
+
584
+ chunk = self._convert_to_save(chunk, chunk_version, translator, recurse)
585
+ chunk, chunk_palette = self._pack(chunk, translator, chunk_version)
586
+ raw_chunk_data = self._encode(interface, chunk, dimension, chunk_palette)
587
+
588
+ self._put_raw_chunk_data(cx, cz, raw_chunk_data, dimension)
589
+
590
+ def _convert_to_save(
591
+ self,
592
+ chunk: Chunk,
593
+ chunk_version: VersionNumberAny,
594
+ translator: "Translator",
595
+ recurse: bool = True,
596
+ ) -> Chunk:
597
+ """Convert the Chunk in Universal format to a Chunk in the version specific format."""
598
+ # create a new streamlined block block_palette and remap the data
599
+ palette: List[numpy.ndarray] = []
600
+ palette_len = 0
601
+ for cy in chunk.blocks.sub_chunks:
602
+ sub_chunk_palette, sub_chunk = numpy.unique(
603
+ chunk.blocks.get_sub_chunk(cy), return_inverse=True
604
+ )
605
+ chunk.blocks.add_sub_chunk(
606
+ cy, sub_chunk.astype(numpy.uint32).reshape((16, 16, 16)) + palette_len
607
+ )
608
+ palette_len += len(sub_chunk_palette)
609
+ palette.append(sub_chunk_palette)
610
+
611
+ if palette:
612
+ chunk_palette, lut = numpy.unique(
613
+ numpy.concatenate(palette), return_inverse=True
614
+ )
615
+ for cy in chunk.blocks.sub_chunks:
616
+ chunk.blocks.add_sub_chunk(
617
+ cy, lut.astype(numpy.uint32)[chunk.blocks.get_sub_chunk(cy)]
618
+ )
619
+ chunk._block_palette = BlockManager(
620
+ numpy.vectorize(chunk.block_palette.__getitem__)(chunk_palette)
621
+ )
622
+ else:
623
+ chunk._block_palette = BlockManager()
624
+
625
+ def get_chunk_callback(_: int, __: int) -> Chunk:
626
+ # conversion from universal should not require any data outside the block
627
+ return chunk
628
+
629
+ # translate from universal format to version format
630
+ return translator.from_universal(
631
+ chunk_version, self.translation_manager, chunk, get_chunk_callback, recurse
632
+ )
633
+
634
+ def _pack(
635
+ self, chunk: Chunk, translator: "Translator", chunk_version: VersionNumberAny
636
+ ) -> Tuple[Chunk, AnyNDArray]:
637
+ """Pack the chunk data into the format required by the encoder.
638
+ This includes converting the string names to numerical formats for the versions that require it.
639
+ """
640
+ return translator.pack(chunk_version, self.translation_manager, chunk)
641
+
642
+ def _encode(
643
+ self,
644
+ interface: api_wrapper.Interface,
645
+ chunk: Chunk,
646
+ dimension: Dimension,
647
+ chunk_palette: AnyNDArray,
648
+ ) -> Any:
649
+ """Encode the data to the raw format as saved on disk."""
650
+ raise NotImplementedError
651
+
652
+ def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
653
+ """
654
+ Delete the given chunk from the level.
655
+
656
+ :param cx: The x coordinate of the chunk.
657
+ :param cz: The z coordinate of the chunk.
658
+ :param dimension: The dimension to load the data from.
659
+ """
660
+ self._delete_chunk(cx, cz, dimension)
661
+ self._changed = True
662
+
663
+ @abstractmethod
664
+ def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
665
+ raise NotImplementedError
666
+
667
+ def put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
668
+ """
669
+ Commit the raw chunk data to the FormatWrapper cache.
670
+
671
+ Call :meth:`save` to push all the cache data to the level.
672
+
673
+ :param cx: The x coordinate of the chunk.
674
+ :param cz: The z coordinate of the chunk.
675
+ :param data: The raw data to commit to the level.
676
+ :param dimension: The dimension to load the data from.
677
+ """
678
+ self._verify_has_lock()
679
+ self._put_raw_chunk_data(cx, cz, data, dimension)
680
+
681
+ @abstractmethod
682
+ def _put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
683
+ raise NotImplementedError
684
+
685
+ def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
686
+ """
687
+ Return the raw data as loaded from disk.
688
+
689
+ :param cx: The x coordinate of the chunk.
690
+ :param cz: The z coordinate of the chunk.
691
+ :param dimension: The dimension to load the data from.
692
+ :return: The raw chunk data.
693
+ """
694
+ return self._safe_load(
695
+ self._get_raw_chunk_data,
696
+ (cx, cz, dimension),
697
+ "Error loading chunk {} {} {}",
698
+ ChunkLoadError,
699
+ ChunkDoesNotExist,
700
+ )
701
+
702
+ @abstractmethod
703
+ def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
704
+ """
705
+ Return the raw data as loaded from disk.
706
+
707
+ :param cx: The x coordinate of the chunk.
708
+ :param cz: The z coordinate of the chunk.
709
+ :param dimension: The dimension to load the data from.
710
+ :return: The raw chunk data.
711
+ """
712
+ raise NotImplementedError
713
+
714
+ @abstractmethod
715
+ def all_player_ids(self) -> Iterable[str]:
716
+ """
717
+ Returns a set of all player ids that are present in the level
718
+ """
719
+ return NotImplemented
720
+
721
+ @abstractmethod
722
+ def has_player(self, player_id: str) -> bool:
723
+ """
724
+ Test if a player id is present in the level.
725
+ """
726
+ return NotImplemented
727
+
728
+ def load_player(self, player_id: str) -> "Player":
729
+ """
730
+ Gets the :class:`Player` object that belongs to the specified player id
731
+
732
+ If no parameter is supplied, the data of the local player should be returned
733
+
734
+ :param player_id: The desired player id
735
+ :return: A Player instance
736
+ """
737
+ return self._safe_load(
738
+ self._load_player,
739
+ (player_id,),
740
+ "Error loading player {}",
741
+ PlayerLoadError,
742
+ PlayerDoesNotExist,
743
+ )
744
+
745
+ @abstractmethod
746
+ def _load_player(self, player_id: str) -> "Player":
747
+ """
748
+ Get the raw player data and unpack it into a Player class.
749
+
750
+ :param player_id: The id of the player to get.
751
+ :return:
752
+ """
753
+ raise NotImplementedError
754
+
755
+ def get_raw_player_data(self, player_id: str) -> Any:
756
+ """
757
+ Get the player data in the lowest level form.
758
+
759
+ :param player_id: The id of the player to get.
760
+ :return:
761
+ """
762
+ return self._safe_load(
763
+ self._get_raw_player_data,
764
+ (player_id,),
765
+ "Error loading player {}",
766
+ PlayerLoadError,
767
+ PlayerDoesNotExist,
768
+ )
769
+
770
+ @abstractmethod
771
+ def _get_raw_player_data(self, player_id: str) -> Any:
772
+ raise NotImplementedError