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,641 +1,659 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import struct
5
- import warnings
6
- from typing import Tuple, Dict, Union, Optional, List, BinaryIO, Iterable, Any
7
- from io import BytesIO
8
- import shutil
9
- import traceback
10
- import time
11
-
12
- from amulet_nbt import (
13
- AbstractBaseTag,
14
- load as load_nbt,
15
- NamedTag,
16
- CompoundTag,
17
- StringTag,
18
- ByteTag,
19
- ShortTag,
20
- IntTag,
21
- ListTag,
22
- LongTag,
23
- FloatTag,
24
- utf8_escape_decoder,
25
- utf8_escape_encoder,
26
- ReadContext,
27
- )
28
- from amulet.api.player import Player, LOCAL_PLAYER
29
- from amulet.api.chunk import Chunk
30
- from amulet.api.selection import SelectionBox, SelectionGroup
31
-
32
- from leveldb import LevelDB, LevelDBException, LevelDBEncrypted
33
- from amulet.utils.format_utils import check_all_exist
34
- from amulet.api.data_types import (
35
- ChunkCoordinates,
36
- VersionNumberTuple,
37
- PlatformType,
38
- Dimension,
39
- AnyNDArray,
40
- )
41
- from amulet.api.wrapper import WorldFormatWrapper
42
- from amulet.api.errors import ObjectWriteError, ObjectReadError, PlayerDoesNotExist
43
-
44
- from .interface.chunk.leveldb_chunk_versions import (
45
- game_to_chunk_version,
46
- )
47
- from .dimension import (
48
- LevelDBDimensionManager,
49
- ChunkData,
50
- OVERWORLD,
51
- THE_NETHER,
52
- THE_END,
53
- )
54
- from .interface.chunk import BaseLevelDBInterface, get_interface
55
-
56
- InternalDimension = Optional[int]
57
-
58
-
59
- class BedrockLevelDAT(NamedTag):
60
- _path: str
61
- _level_dat_version: int
62
-
63
- def __init__(
64
- self, tag=None, name: str = "", path: str = None, level_dat_version: int = None
65
- ):
66
- if isinstance(tag, str):
67
- warnings.warn(
68
- "You must use BedrockLevelDAT.from_file to load from a file.",
69
- FutureWarning,
70
- )
71
- super().__init__()
72
- self._path = path = tag
73
- self._level_dat_version = 8
74
- if os.path.isfile(path):
75
- self.load_from(path)
76
- return
77
- else:
78
- if not (isinstance(path, str) and isinstance(level_dat_version, int)):
79
- raise TypeError(
80
- "path and level_dat_version must be specified when constructing a BedrockLevelDAT instance."
81
- )
82
- super().__init__(tag, name)
83
- self._path = path
84
- self._level_dat_version = level_dat_version
85
-
86
- @classmethod
87
- def from_file(cls, path: str):
88
- level_dat_version, name, tag = cls._read_from(path)
89
- return cls(tag, name, path, level_dat_version)
90
-
91
- @property
92
- def path(self) -> Optional[str]:
93
- return self._path
94
-
95
- @staticmethod
96
- def _read_from(path: str) -> Tuple[int, str, AbstractBaseTag]:
97
- with open(path, "rb") as f:
98
- level_dat_version = struct.unpack("<i", f.read(4))[0]
99
- if 4 <= level_dat_version <= 10:
100
- data_length = struct.unpack("<i", f.read(4))[0]
101
- root_tag = load_nbt(
102
- f.read(data_length),
103
- compressed=False,
104
- little_endian=True,
105
- string_decoder=utf8_escape_decoder,
106
- )
107
- name = root_tag.name
108
- value = root_tag.tag
109
- else:
110
- # TODO: handle other versions
111
- raise ObjectReadError(
112
- f"Unsupported level.dat version {level_dat_version}"
113
- )
114
- return level_dat_version, name, value
115
-
116
- def load_from(self, path: str):
117
- self._level_dat_version, self.name, self.tag = self._read_from(path)
118
-
119
- def reload(self):
120
- self.load_from(self.path)
121
-
122
- def save(self, path: str = None):
123
- self.save_to(path or self._path)
124
-
125
- def save_to(
126
- self,
127
- filename_or_buffer: Union[str, BinaryIO] = None,
128
- *,
129
- compressed=False,
130
- little_endian=True,
131
- string_encoder=utf8_escape_encoder,
132
- ) -> Optional[bytes]:
133
- payload = super().save_to(
134
- compressed=compressed,
135
- little_endian=little_endian,
136
- string_encoder=string_encoder,
137
- )
138
- buffer = BytesIO()
139
- buffer.write(struct.pack("<ii", self._level_dat_version, len(payload)))
140
- buffer.write(payload)
141
- if filename_or_buffer is None:
142
- return buffer.getvalue()
143
- elif isinstance(filename_or_buffer, str):
144
- with open(filename_or_buffer, "wb") as f:
145
- f.write(buffer.getvalue())
146
- else:
147
- filename_or_buffer.write(buffer.getvalue())
148
-
149
-
150
- class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
151
- """
152
- This FormatWrapper class exists to interface with the Bedrock world format.
153
- """
154
-
155
- # The leveldb database. Access it through the public property `level_db`
156
- _db: Optional[LevelDB]
157
- # A class to manage dimension data. This is private
158
- _dimension_manager: Optional[LevelDBDimensionManager]
159
-
160
- _root_tag: BedrockLevelDAT
161
-
162
- def __init__(self, path: str):
163
- """
164
- Construct a new instance of :class:`LevelDBFormat`.
165
-
166
- This should not be used directly. You should instead use :func:`amulet.load_format`.
167
-
168
- :param path: The file path to the serialised data.
169
- """
170
- super().__init__(path)
171
- self._platform = "bedrock"
172
- dat_path = os.path.join(path, "level.dat")
173
- if os.path.isfile(dat_path):
174
- self._root_tag = BedrockLevelDAT.from_file(dat_path)
175
- else:
176
- # TODO: handle level creation better
177
- self._root_tag = BedrockLevelDAT(path=dat_path, level_dat_version=9)
178
- self._db = None
179
- self._dimension_manager = None
180
- self._shallow_load()
181
-
182
- def _shallow_load(self):
183
- try:
184
- self._load_level_dat()
185
- except:
186
- pass
187
-
188
- def _load_level_dat(self):
189
- """Load the level.dat file and check the image file"""
190
- if os.path.isfile(os.path.join(self.path, "world_icon.jpeg")):
191
- self._world_image_path = os.path.join(self.path, "world_icon.jpeg")
192
- self.root_tag = BedrockLevelDAT.from_file(os.path.join(self.path, "level.dat"))
193
-
194
- @staticmethod
195
- def is_valid(path: str):
196
- return check_all_exist(path, "db", "level.dat", "levelname.txt")
197
-
198
- @property
199
- def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
200
- return {"bedrock": (True, True)}
201
-
202
- @property
203
- def version(self) -> VersionNumberTuple:
204
- if self._version is None:
205
- self._version = self._get_version()
206
- return self._version
207
-
208
- def _get_version(self) -> VersionNumberTuple:
209
- """
210
- The version the world was last opened in.
211
-
212
- This should be greater than or equal to the chunk versions found within
213
-
214
- For this format wrapper it returns a tuple of 3/4 ints (the game version number)
215
- """
216
- try:
217
- return tuple(
218
- [
219
- t.py_int
220
- for t in self.root_tag.compound.get_list("lastOpenedWithVersion")
221
- ]
222
- )
223
- except Exception:
224
- return 1, 2, 0
225
-
226
- @property
227
- def root_tag(self) -> BedrockLevelDAT:
228
- """The level.dat data for the level."""
229
- return self._root_tag
230
-
231
- @root_tag.setter
232
- def root_tag(self, root_tag: Union[NamedTag, CompoundTag, BedrockLevelDAT]):
233
- if isinstance(root_tag, CompoundTag):
234
- self._root_tag.tag = root_tag
235
- elif isinstance(root_tag, NamedTag):
236
- self._root_tag.name = root_tag.name
237
- self._root_tag.tag = root_tag.compound
238
- else:
239
- raise ValueError(
240
- "root_tag must be a CompoundTag, NamedTag or BedrockLevelDAT"
241
- )
242
-
243
- @property
244
- def level_name(self) -> str:
245
- return self.root_tag.compound.get_string("LevelName", StringTag()).py_str
246
-
247
- @level_name.setter
248
- def level_name(self, value: str):
249
- self.root_tag.compound["LevelName"] = StringTag(value)
250
-
251
- @property
252
- def last_played(self) -> int:
253
- return self.root_tag.compound.get_long("LastPlayed", LongTag()).py_int
254
-
255
- @property
256
- def game_version_string(self) -> str:
257
- try:
258
- return f'Bedrock {".".join(str(v.py_int) for v in self.root_tag.compound.get_list("lastOpenedWithVersion"))}'
259
- except Exception:
260
- return f"Bedrock Unknown Version"
261
-
262
- @property
263
- def dimensions(self) -> List["Dimension"]:
264
- self._verify_has_lock()
265
- return self._dimension_manager.dimensions
266
-
267
- # def register_dimension(
268
- # self, dimension_internal: int, dimension_name: Optional["Dimension"] = None
269
- # ):
270
- # """
271
- # Register a new dimension.
272
- #
273
- # :param dimension_internal: The internal integer representation of the dimension.
274
- # :param dimension_name: The name of the dimension shown to the user.
275
- # :return:
276
- # """
277
- # self._dimension_manager.register_dimension(dimension_internal, dimension_name)
278
-
279
- @property
280
- def level_db(self) -> LevelDB:
281
- """The raw leveldb database."""
282
- if self._db is None:
283
- raise Exception(
284
- "The world is not open. The leveldb database cannot be accessed."
285
- )
286
- return self._db
287
-
288
- @property
289
- def _level_manager(self) -> LevelDBDimensionManager:
290
- warnings.warn(
291
- "_level_manager attribute is depreciated. If you want to access the raw leveldb database it can be accessed through the level_db property."
292
- )
293
- return self._dimension_manager
294
-
295
- def _get_interface(
296
- self, raw_chunk_data: Optional[Any] = None
297
- ) -> BaseLevelDBInterface:
298
- return get_interface(self._get_interface_key(raw_chunk_data))
299
-
300
- def _get_interface_key(self, raw_chunk_data: Optional[ChunkData] = None) -> int:
301
- if raw_chunk_data:
302
- if b"," in raw_chunk_data:
303
- chunk_version = raw_chunk_data[b","][0]
304
- else:
305
- chunk_version = raw_chunk_data.get(b"v", b"\x00")[0]
306
- else:
307
- chunk_version = game_to_chunk_version(
308
- self.max_world_version[1],
309
- self.root_tag.compound.get_compound("experiments", CompoundTag())
310
- .get_byte("caves_and_cliffs", ByteTag())
311
- .py_int,
312
- )
313
- return chunk_version
314
-
315
- def _decode(
316
- self,
317
- interface: BaseLevelDBInterface,
318
- dimension: Dimension,
319
- cx: int,
320
- cz: int,
321
- raw_chunk_data: Any,
322
- ) -> Tuple[Chunk, AnyNDArray]:
323
- bounds = self.bounds(dimension).bounds
324
- return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
325
-
326
- def _encode(
327
- self,
328
- interface: BaseLevelDBInterface,
329
- chunk: Chunk,
330
- dimension: Dimension,
331
- chunk_palette: AnyNDArray,
332
- ) -> Any:
333
- bounds = self.bounds(dimension).bounds
334
- return interface.encode(
335
- chunk,
336
- chunk_palette,
337
- self.max_world_version,
338
- (bounds[0][1], bounds[1][1]),
339
- )
340
-
341
- def _reload_world(self):
342
- try:
343
- self.close()
344
- except:
345
- pass
346
- try:
347
- self._db = LevelDB(os.path.join(self.path, "db"))
348
- self._dimension_manager = LevelDBDimensionManager(self)
349
- self._is_open = True
350
- self._has_lock = True
351
- experiments = self.root_tag.compound.get_compound(
352
- "experiments", CompoundTag()
353
- )
354
- if (
355
- experiments.get_byte("caves_and_cliffs", ByteTag()).py_int
356
- or experiments.get_byte("caves_and_cliffs_internal", ByteTag()).py_int
357
- or self.version >= (1, 18)
358
- ):
359
- self._bounds[OVERWORLD] = SelectionGroup(
360
- SelectionBox(
361
- (-30_000_000, -64, -30_000_000), (30_000_000, 320, 30_000_000)
362
- )
363
- )
364
- else:
365
- self._bounds[OVERWORLD] = SelectionGroup(
366
- SelectionBox(
367
- (-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000)
368
- )
369
- )
370
- self._bounds[THE_NETHER] = SelectionGroup(
371
- SelectionBox(
372
- (-30_000_000, 0, -30_000_000), (30_000_000, 128, 30_000_000)
373
- )
374
- )
375
- self._bounds[THE_END] = SelectionGroup(
376
- SelectionBox(
377
- (-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000)
378
- )
379
- )
380
- if b"LevelChunkMetaDataDictionary" in self.level_db:
381
- data = self.level_db[b"LevelChunkMetaDataDictionary"]
382
- count, data = struct.unpack("<I", data[:4])[0], data[4:]
383
- for _ in range(count):
384
- key, data = data[:8], data[8:]
385
- context = ReadContext()
386
- value = load_nbt(
387
- data,
388
- little_endian=True,
389
- compressed=False,
390
- string_decoder=utf8_escape_decoder,
391
- read_context=context,
392
- ).compound
393
- data = data[context.offset :]
394
-
395
- try:
396
- dimension_name = value.get_string("DimensionName").py_str
397
- # The dimension names are stored differently TODO: split local and global names
398
- dimension_name = {
399
- "Overworld": "minecraft:overworld",
400
- "Nether": "minecraft:the_nether",
401
- "TheEnd": "minecraft:the_end",
402
- }.get(dimension_name, dimension_name)
403
-
404
- except KeyError:
405
- # Some entries seem to not have a dimension assigned to them. Is there a default? We will skip over these for now.
406
- # {'LastSavedBaseGameVersion': StringTag("1.19.81"), 'LastSavedDimensionHeightRange': CompoundTag({'max': ShortTag(320), 'min': ShortTag(-64)})}
407
- pass
408
- else:
409
- previous_bounds = self._bounds.get(
410
- dimension_name,
411
- SelectionGroup(
412
- SelectionBox(
413
- (-30_000_000, 0, -30_000_000),
414
- (30_000_000, 256, 30_000_000),
415
- )
416
- ),
417
- )
418
- min_y = min(
419
- value.get_compound(
420
- "LastSavedDimensionHeightRange", CompoundTag()
421
- )
422
- .get_short("min", ShortTag())
423
- .py_int,
424
- value.get_compound(
425
- "OriginalDimensionHeightRange", CompoundTag()
426
- )
427
- .get_short("min", ShortTag())
428
- .py_int,
429
- previous_bounds.min_y,
430
- )
431
- max_y = max(
432
- value.get_compound(
433
- "LastSavedDimensionHeightRange", CompoundTag()
434
- )
435
- .get_short("max", ShortTag())
436
- .py_int,
437
- value.get_compound(
438
- "OriginalDimensionHeightRange", CompoundTag()
439
- )
440
- .get_short("max", ShortTag())
441
- .py_int,
442
- previous_bounds.max_y,
443
- )
444
- self._bounds[dimension_name] = SelectionGroup(
445
- SelectionBox(
446
- (previous_bounds.min_x, min_y, previous_bounds.min_z),
447
- (previous_bounds.max_x, max_y, previous_bounds.max_z),
448
- )
449
- )
450
- except LevelDBEncrypted as e:
451
- self._is_open = self._has_lock = False
452
- raise LevelDBException(
453
- "It looks like this world is from the marketplace.\nThese worlds are encrypted and cannot be edited."
454
- ) from e
455
- except LevelDBException as e:
456
- msg = str(e)
457
- self._is_open = self._has_lock = False
458
- # I don't know if there is a better way of handling this.
459
- if msg.startswith("IO error:") and msg.endswith(": Permission denied"):
460
- traceback.print_exc()
461
- raise LevelDBException(
462
- f"Failed to load the database. The world may be open somewhere else.\n{msg}"
463
- ) from e
464
- else:
465
- raise e
466
-
467
- def _open(self):
468
- """Open the database for reading and writing"""
469
- self._reload_world()
470
-
471
- def _create(
472
- self,
473
- overwrite: bool,
474
- bounds: Union[
475
- SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
476
- ] = None,
477
- **kwargs,
478
- ):
479
- if os.path.isdir(self.path):
480
- if overwrite:
481
- shutil.rmtree(self.path)
482
- else:
483
- raise ObjectWriteError(
484
- f"A world already exists at the path {self.path}"
485
- )
486
-
487
- version = self.translation_manager.get_version(
488
- self.platform, self.version
489
- ).version_number
490
- self._version = version + (0,) * (5 - len(version))
491
-
492
- self.root_tag = root = CompoundTag()
493
- root["StorageVersion"] = IntTag(8)
494
- root["lastOpenedWithVersion"] = ListTag([IntTag(i) for i in self._version])
495
- root["Generator"] = IntTag(1)
496
- root["LastPlayed"] = LongTag(int(time.time()))
497
- root["LevelName"] = StringTag("World Created By Amulet")
498
-
499
- os.makedirs(self.path, exist_ok=True)
500
- self.root_tag.save()
501
-
502
- db = LevelDB(os.path.join(self.path, "db"), True)
503
- db.close()
504
-
505
- self._reload_world()
506
-
507
- @property
508
- def has_lock(self) -> bool:
509
- if self._has_lock:
510
- return True # TODO: implement a check to ensure access to the database
511
- return False
512
-
513
- def _save(self):
514
- os.makedirs(self.path, exist_ok=True)
515
- self.root_tag.save()
516
- with open(os.path.join(self.path, "levelname.txt"), "w", encoding="utf-8") as f:
517
- f.write(self.level_name)
518
-
519
- def _close(self):
520
- self._db.close()
521
- self._db = None
522
- self._dimension_manager = None
523
- self._actor_counter = None
524
-
525
- def unload(self):
526
- pass
527
-
528
- def all_chunk_coords(self, dimension: "Dimension") -> Iterable[ChunkCoordinates]:
529
- self._verify_has_lock()
530
- yield from self._dimension_manager.all_chunk_coords(dimension)
531
-
532
- def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
533
- return self._dimension_manager.has_chunk(cx, cz, dimension)
534
-
535
- def _delete_chunk(self, cx: int, cz: int, dimension: "Dimension"):
536
- self._dimension_manager.delete_chunk(cx, cz, dimension)
537
-
538
- def _put_raw_chunk_data(
539
- self, cx: int, cz: int, data: ChunkData, dimension: "Dimension"
540
- ):
541
- return self._dimension_manager.put_chunk_data(cx, cz, data, dimension)
542
-
543
- def _get_raw_chunk_data(
544
- self, cx: int, cz: int, dimension: "Dimension"
545
- ) -> ChunkData:
546
- """
547
- Return the raw data as loaded from disk.
548
-
549
- :param cx: The x coordinate of the chunk.
550
- :param cz: The z coordinate of the chunk.
551
- :param dimension: The dimension to load the data from.
552
- :return: The raw chunk data.
553
- """
554
- return self._dimension_manager.get_chunk_data(cx, cz, dimension)
555
-
556
- def all_player_ids(self) -> Iterable[str]:
557
- """
558
- Returns a generator of all player ids that are present in the level
559
- """
560
- yield from (
561
- pid[7:].decode("utf-8")
562
- for pid, _ in self._db.iterate(b"player_", b"player_\xFF")
563
- )
564
- if self.has_player(LOCAL_PLAYER):
565
- yield LOCAL_PLAYER
566
-
567
- def has_player(self, player_id: str) -> bool:
568
- if player_id != LOCAL_PLAYER:
569
- player_id = f"player_{player_id}"
570
- return player_id.encode("utf-8") in self._db
571
-
572
- def _load_player(self, player_id: str) -> Player:
573
- """
574
- Gets the :class:`Player` object that belongs to the specified player id
575
-
576
- If no parameter is supplied, the data of the local player will be returned
577
-
578
- :param player_id: The desired player id
579
- :return: A Player instance
580
- """
581
- player_nbt = self._get_raw_player_data(player_id).compound
582
- dimension = player_nbt["DimensionId"]
583
- if isinstance(dimension, IntTag) and IntTag(0) <= dimension <= IntTag(2):
584
- dimension_str = {
585
- 0: OVERWORLD,
586
- 1: THE_NETHER,
587
- 2: THE_END,
588
- }[dimension.py_int]
589
- else:
590
- dimension_str = OVERWORLD
591
-
592
- # get the players position
593
- pos_data = player_nbt.get("Pos")
594
- if (
595
- isinstance(pos_data, ListTag)
596
- and len(pos_data) == 3
597
- and pos_data.list_data_type == FloatTag.tag_id
598
- ):
599
- position = tuple(map(float, pos_data))
600
- position = tuple(
601
- p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
602
- )
603
- else:
604
- position = (0.0, 0.0, 0.0)
605
-
606
- # get the players rotation
607
- rot_data = player_nbt.get("Rotation")
608
- if (
609
- isinstance(rot_data, ListTag)
610
- and len(rot_data) == 2
611
- and rot_data.list_data_type == FloatTag.tag_id
612
- ):
613
- rotation = tuple(map(float, rot_data))
614
- rotation = tuple(
615
- p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
616
- )
617
- else:
618
- rotation = (0.0, 0.0)
619
-
620
- return Player(
621
- player_id,
622
- dimension_str,
623
- position,
624
- rotation,
625
- )
626
-
627
- def _get_raw_player_data(self, player_id: str) -> NamedTag:
628
- if player_id == LOCAL_PLAYER:
629
- key = player_id.encode("utf-8")
630
- else:
631
- key = f"player_{player_id}".encode("utf-8")
632
- try:
633
- data = self._db.get(key)
634
- except KeyError:
635
- raise PlayerDoesNotExist(f"Player {player_id} doesn't exist")
636
- return load_nbt(
637
- data,
638
- compressed=False,
639
- little_endian=True,
640
- string_decoder=utf8_escape_decoder,
641
- )
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import struct
5
+ import warnings
6
+ from typing import Tuple, Dict, Union, Optional, List, BinaryIO, Iterable, Any
7
+ from io import BytesIO
8
+ import shutil
9
+ import traceback
10
+ import time
11
+
12
+ from amulet_nbt import (
13
+ AbstractBaseTag,
14
+ load as load_nbt,
15
+ NamedTag,
16
+ CompoundTag,
17
+ StringTag,
18
+ ByteTag,
19
+ ShortTag,
20
+ IntTag,
21
+ ListTag,
22
+ LongTag,
23
+ FloatTag,
24
+ utf8_escape_decoder,
25
+ utf8_escape_encoder,
26
+ ReadContext,
27
+ )
28
+ from amulet.api.player import Player, LOCAL_PLAYER
29
+ from amulet.api.chunk import Chunk
30
+ from amulet.api.selection import SelectionBox, SelectionGroup
31
+
32
+ from leveldb import LevelDB, LevelDBException, LevelDBEncrypted
33
+ from amulet.utils.format_utils import check_all_exist
34
+ from amulet.api.data_types import (
35
+ ChunkCoordinates,
36
+ VersionNumberTuple,
37
+ PlatformType,
38
+ Dimension,
39
+ AnyNDArray,
40
+ )
41
+ from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
42
+ from amulet.api.errors import (
43
+ ObjectWriteError,
44
+ ObjectReadError,
45
+ PlayerDoesNotExist,
46
+ ChunkDoesNotExist,
47
+ )
48
+
49
+ from .interface.chunk.leveldb_chunk_versions import (
50
+ game_to_chunk_version,
51
+ )
52
+ from .dimension import LevelDBDimensionManager, ChunkData, InternalDimension
53
+ from .interface.chunk import BaseLevelDBInterface, get_interface
54
+
55
+ OVERWORLD = "minecraft:overworld"
56
+ THE_NETHER = "minecraft:the_nether"
57
+ THE_END = "minecraft:the_end"
58
+
59
+
60
+ class BedrockLevelDAT(NamedTag):
61
+ _path: str
62
+ _level_dat_version: int
63
+
64
+ def __init__(
65
+ self, tag=None, name: str = "", path: str = None, level_dat_version: int = None
66
+ ):
67
+ if isinstance(tag, str):
68
+ warnings.warn(
69
+ "You must use BedrockLevelDAT.from_file to load from a file.",
70
+ FutureWarning,
71
+ )
72
+ super().__init__()
73
+ self._path = path = tag
74
+ self._level_dat_version = 8
75
+ if os.path.isfile(path):
76
+ self.load_from(path)
77
+ return
78
+ else:
79
+ if not (isinstance(path, str) and isinstance(level_dat_version, int)):
80
+ raise TypeError(
81
+ "path and level_dat_version must be specified when constructing a BedrockLevelDAT instance."
82
+ )
83
+ super().__init__(tag, name)
84
+ self._path = path
85
+ self._level_dat_version = level_dat_version
86
+
87
+ @classmethod
88
+ def from_file(cls, path: str):
89
+ level_dat_version, name, tag = cls._read_from(path)
90
+ return cls(tag, name, path, level_dat_version)
91
+
92
+ @property
93
+ def path(self) -> Optional[str]:
94
+ return self._path
95
+
96
+ @staticmethod
97
+ def _read_from(path: str) -> Tuple[int, str, AbstractBaseTag]:
98
+ with open(path, "rb") as f:
99
+ level_dat_version = struct.unpack("<i", f.read(4))[0]
100
+ if 4 <= level_dat_version <= 10:
101
+ data_length = struct.unpack("<i", f.read(4))[0]
102
+ root_tag = load_nbt(
103
+ f.read(data_length),
104
+ compressed=False,
105
+ little_endian=True,
106
+ string_decoder=utf8_escape_decoder,
107
+ )
108
+ name = root_tag.name
109
+ value = root_tag.tag
110
+ else:
111
+ # TODO: handle other versions
112
+ raise ObjectReadError(
113
+ f"Unsupported level.dat version {level_dat_version}"
114
+ )
115
+ return level_dat_version, name, value
116
+
117
+ def load_from(self, path: str):
118
+ self._level_dat_version, self.name, self.tag = self._read_from(path)
119
+
120
+ def reload(self):
121
+ self.load_from(self.path)
122
+
123
+ def save(self, path: str = None):
124
+ self.save_to(path or self._path)
125
+
126
+ def save_to(
127
+ self,
128
+ filename_or_buffer: Union[str, BinaryIO] = None,
129
+ *,
130
+ compressed=False,
131
+ little_endian=True,
132
+ string_encoder=utf8_escape_encoder,
133
+ ) -> Optional[bytes]:
134
+ payload = super().save_to(
135
+ compressed=compressed,
136
+ little_endian=little_endian,
137
+ string_encoder=string_encoder,
138
+ )
139
+ buffer = BytesIO()
140
+ buffer.write(struct.pack("<ii", self._level_dat_version, len(payload)))
141
+ buffer.write(payload)
142
+ if filename_or_buffer is None:
143
+ return buffer.getvalue()
144
+ elif isinstance(filename_or_buffer, str):
145
+ with open(filename_or_buffer, "wb") as f:
146
+ f.write(buffer.getvalue())
147
+ else:
148
+ filename_or_buffer.write(buffer.getvalue())
149
+
150
+
151
+ class LevelDBFormat(WorldFormatWrapper[VersionNumberTuple]):
152
+ """
153
+ This FormatWrapper class exists to interface with the Bedrock world format.
154
+ """
155
+
156
+ # The leveldb database. Access it through the public property `level_db`
157
+ _db: Optional[LevelDB]
158
+ # A class to manage dimension data. This is private
159
+ _dimension_manager: Optional[LevelDBDimensionManager]
160
+
161
+ _root_tag: BedrockLevelDAT
162
+
163
+ def __init__(self, path: str):
164
+ """
165
+ Construct a new instance of :class:`LevelDBFormat`.
166
+
167
+ This should not be used directly. You should instead use :func:`amulet.load_format`.
168
+
169
+ :param path: The file path to the serialised data.
170
+ """
171
+ super().__init__(path)
172
+ self._platform = "bedrock"
173
+ dat_path = os.path.join(path, "level.dat")
174
+ if os.path.isfile(dat_path):
175
+ self._root_tag = BedrockLevelDAT.from_file(dat_path)
176
+ else:
177
+ # TODO: handle level creation better
178
+ self._root_tag = BedrockLevelDAT(path=dat_path, level_dat_version=9)
179
+ self._db = None
180
+ self._dimension_manager = None
181
+ self._dimension_to_internal: dict[Dimension, InternalDimension] = {}
182
+ self._shallow_load()
183
+
184
+ def _shallow_load(self):
185
+ try:
186
+ self._load_level_dat()
187
+ except:
188
+ pass
189
+
190
+ def _load_level_dat(self):
191
+ """Load the level.dat file and check the image file"""
192
+ if os.path.isfile(os.path.join(self.path, "world_icon.jpeg")):
193
+ self._world_image_path = os.path.join(self.path, "world_icon.jpeg")
194
+ self.root_tag = BedrockLevelDAT.from_file(os.path.join(self.path, "level.dat"))
195
+
196
+ @staticmethod
197
+ def is_valid(path: str):
198
+ return check_all_exist(path, "db", "level.dat", "levelname.txt")
199
+
200
+ @property
201
+ def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
202
+ return {"bedrock": (True, True)}
203
+
204
+ @property
205
+ def version(self) -> VersionNumberTuple:
206
+ if self._version is None:
207
+ self._version = self._get_version()
208
+ return self._version
209
+
210
+ def _get_version(self) -> VersionNumberTuple:
211
+ """
212
+ The version the world was last opened in.
213
+
214
+ This should be greater than or equal to the chunk versions found within
215
+
216
+ For this format wrapper it returns a tuple of 3/4 ints (the game version number)
217
+ """
218
+ try:
219
+ return tuple(
220
+ [
221
+ t.py_int
222
+ for t in self.root_tag.compound.get_list("lastOpenedWithVersion")
223
+ ]
224
+ )
225
+ except Exception:
226
+ return 1, 2, 0
227
+
228
+ @property
229
+ def root_tag(self) -> BedrockLevelDAT:
230
+ """The level.dat data for the level."""
231
+ return self._root_tag
232
+
233
+ @root_tag.setter
234
+ def root_tag(self, root_tag: Union[NamedTag, CompoundTag, BedrockLevelDAT]):
235
+ if isinstance(root_tag, CompoundTag):
236
+ self._root_tag.tag = root_tag
237
+ elif isinstance(root_tag, NamedTag):
238
+ self._root_tag.name = root_tag.name
239
+ self._root_tag.tag = root_tag.compound
240
+ else:
241
+ raise ValueError(
242
+ "root_tag must be a CompoundTag, NamedTag or BedrockLevelDAT"
243
+ )
244
+
245
+ @property
246
+ def level_name(self) -> str:
247
+ return self.root_tag.compound.get_string("LevelName", StringTag()).py_str
248
+
249
+ @level_name.setter
250
+ def level_name(self, value: str):
251
+ self.root_tag.compound["LevelName"] = StringTag(value)
252
+
253
+ @property
254
+ def last_played(self) -> int:
255
+ return self.root_tag.compound.get_long("LastPlayed", LongTag()).py_int
256
+
257
+ @property
258
+ def game_version_string(self) -> str:
259
+ try:
260
+ return f'Bedrock {".".join(str(v.py_int) for v in self.root_tag.compound.get_list("lastOpenedWithVersion"))}'
261
+ except Exception:
262
+ return f"Bedrock Unknown Version"
263
+
264
+ @property
265
+ def dimensions(self) -> List[Dimension]:
266
+ self._verify_has_lock()
267
+ return list(self._dimension_to_internal)
268
+
269
+ # def register_dimension(
270
+ # self, dimension_internal: int, dimension_name: Optional[Dimension] = None
271
+ # ):
272
+ # """
273
+ # Register a new dimension.
274
+ #
275
+ # :param dimension_internal: The internal integer representation of the dimension.
276
+ # :param dimension_name: The name of the dimension shown to the user.
277
+ # :return:
278
+ # """
279
+ # self._dimension_manager.register_dimension(dimension_internal, dimension_name)
280
+
281
+ @property
282
+ def level_db(self) -> LevelDB:
283
+ """The raw leveldb database."""
284
+ if self._db is None:
285
+ raise Exception(
286
+ "The world is not open. The leveldb database cannot be accessed."
287
+ )
288
+ return self._db
289
+
290
+ @property
291
+ def _level_manager(self) -> LevelDBDimensionManager:
292
+ warnings.warn(
293
+ "_level_manager attribute is depreciated. If you want to access the raw leveldb database it can be accessed through the level_db property."
294
+ )
295
+ return self._dimension_manager
296
+
297
+ def _get_interface(
298
+ self, raw_chunk_data: Optional[Any] = None
299
+ ) -> BaseLevelDBInterface:
300
+ return get_interface(self._get_interface_key(raw_chunk_data))
301
+
302
+ def _get_interface_key(self, raw_chunk_data: Optional[ChunkData] = None) -> int:
303
+ if raw_chunk_data:
304
+ if b"," in raw_chunk_data:
305
+ chunk_version = raw_chunk_data[b","][0]
306
+ else:
307
+ chunk_version = raw_chunk_data.get(b"v", b"\x00")[0]
308
+ else:
309
+ chunk_version = game_to_chunk_version(
310
+ self.max_world_version[1],
311
+ self.root_tag.compound.get_compound("experiments", CompoundTag())
312
+ .get_byte("caves_and_cliffs", ByteTag())
313
+ .py_int,
314
+ )
315
+ return chunk_version
316
+
317
+ def _decode(
318
+ self,
319
+ interface: BaseLevelDBInterface,
320
+ dimension: Dimension,
321
+ cx: int,
322
+ cz: int,
323
+ raw_chunk_data: Any,
324
+ ) -> Tuple[Chunk, AnyNDArray]:
325
+ bounds = self.bounds(dimension).bounds
326
+ return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
327
+
328
+ def _encode(
329
+ self,
330
+ interface: BaseLevelDBInterface,
331
+ chunk: Chunk,
332
+ dimension: Dimension,
333
+ chunk_palette: AnyNDArray,
334
+ ) -> Any:
335
+ bounds = self.bounds(dimension).bounds
336
+ return interface.encode(
337
+ chunk,
338
+ chunk_palette,
339
+ self.max_world_version,
340
+ (bounds[0][1], bounds[1][1]),
341
+ )
342
+
343
+ def _reload_world(self):
344
+ try:
345
+ self.close()
346
+ except:
347
+ pass
348
+ try:
349
+ self._db = LevelDB(os.path.join(self.path, "db"))
350
+ self._dimension_manager = LevelDBDimensionManager(self)
351
+ self._is_open = True
352
+ self._has_lock = True
353
+
354
+ self._dimension_to_internal.clear()
355
+ self._dimension_to_internal[OVERWORLD] = None
356
+ self._dimension_to_internal[THE_NETHER] = 1
357
+ self._dimension_to_internal[THE_END] = 2
358
+
359
+ experiments = self.root_tag.compound.get_compound(
360
+ "experiments", CompoundTag()
361
+ )
362
+ if (
363
+ experiments.get_byte("caves_and_cliffs", ByteTag()).py_int
364
+ or experiments.get_byte("caves_and_cliffs_internal", ByteTag()).py_int
365
+ or self.version >= (1, 18)
366
+ ):
367
+ self._bounds[OVERWORLD] = SelectionGroup(
368
+ SelectionBox(
369
+ (-30_000_000, -64, -30_000_000), (30_000_000, 320, 30_000_000)
370
+ )
371
+ )
372
+ else:
373
+ self._bounds[OVERWORLD] = DefaultSelection
374
+ self._bounds[THE_NETHER] = SelectionGroup(
375
+ SelectionBox(
376
+ (-30_000_000, 0, -30_000_000), (30_000_000, 128, 30_000_000)
377
+ )
378
+ )
379
+ self._bounds[THE_END] = DefaultSelection
380
+
381
+ if b"LevelChunkMetaDataDictionary" in self.level_db:
382
+ data = self.level_db[b"LevelChunkMetaDataDictionary"]
383
+ count, data = struct.unpack("<I", data[:4])[0], data[4:]
384
+ for _ in range(count):
385
+ key, data = data[:8], data[8:]
386
+ context = ReadContext()
387
+ value = load_nbt(
388
+ data,
389
+ little_endian=True,
390
+ compressed=False,
391
+ string_decoder=utf8_escape_decoder,
392
+ read_context=context,
393
+ ).compound
394
+ data = data[context.offset :]
395
+
396
+ try:
397
+ dimension_name = value.get_string("DimensionName").py_str
398
+ # The dimension names are stored differently TODO: split local and global names
399
+ dimension_name = {
400
+ "Overworld": OVERWORLD,
401
+ "Nether": THE_NETHER,
402
+ "TheEnd": THE_END,
403
+ }.get(dimension_name, dimension_name)
404
+
405
+ except KeyError:
406
+ # Some entries seem to not have a dimension assigned to them. Is there a default? We will skip over these for now.
407
+ # {'LastSavedBaseGameVersion': StringTag("1.19.81"), 'LastSavedDimensionHeightRange': CompoundTag({'max': ShortTag(320), 'min': ShortTag(-64)})}
408
+ pass
409
+ else:
410
+ previous_bounds = self._bounds.get(
411
+ dimension_name, DefaultSelection
412
+ )
413
+ min_y = min(
414
+ value.get_compound(
415
+ "LastSavedDimensionHeightRange", CompoundTag()
416
+ )
417
+ .get_short("min", ShortTag())
418
+ .py_int,
419
+ value.get_compound(
420
+ "OriginalDimensionHeightRange", CompoundTag()
421
+ )
422
+ .get_short("min", ShortTag())
423
+ .py_int,
424
+ previous_bounds.min_y,
425
+ )
426
+ max_y = max(
427
+ value.get_compound(
428
+ "LastSavedDimensionHeightRange", CompoundTag()
429
+ )
430
+ .get_short("max", ShortTag())
431
+ .py_int,
432
+ value.get_compound(
433
+ "OriginalDimensionHeightRange", CompoundTag()
434
+ )
435
+ .get_short("max", ShortTag())
436
+ .py_int,
437
+ previous_bounds.max_y,
438
+ )
439
+ self._bounds[dimension_name] = SelectionGroup(
440
+ SelectionBox(
441
+ (previous_bounds.min_x, min_y, previous_bounds.min_z),
442
+ (previous_bounds.max_x, max_y, previous_bounds.max_z),
443
+ )
444
+ )
445
+
446
+ # Give all other dimensions found an entry
447
+ known_dimensions = set(self._dimension_to_internal.values())
448
+ for internal_dimension in self._dimension_manager.dimensions:
449
+ if internal_dimension not in known_dimensions:
450
+ dimension_name = f"DIM{internal_dimension}"
451
+ self._dimension_to_internal[dimension_name] = internal_dimension
452
+ self._bounds[dimension_name] = DefaultSelection
453
+
454
+ except LevelDBEncrypted as e:
455
+ self._is_open = self._has_lock = False
456
+ raise LevelDBException(
457
+ "It looks like this world is from the marketplace.\nThese worlds are encrypted and cannot be edited."
458
+ ) from e
459
+ except LevelDBException as e:
460
+ msg = str(e)
461
+ self._is_open = self._has_lock = False
462
+ # I don't know if there is a better way of handling this.
463
+ if msg.startswith("IO error:") and msg.endswith(": Permission denied"):
464
+ traceback.print_exc()
465
+ raise LevelDBException(
466
+ f"Failed to load the database. The world may be open somewhere else.\n{msg}"
467
+ ) from e
468
+ else:
469
+ raise e
470
+
471
+ def _open(self):
472
+ """Open the database for reading and writing"""
473
+ self._reload_world()
474
+
475
+ def _create(
476
+ self,
477
+ overwrite: bool,
478
+ bounds: Union[
479
+ SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
480
+ ] = None,
481
+ **kwargs,
482
+ ):
483
+ if os.path.isdir(self.path):
484
+ if overwrite:
485
+ shutil.rmtree(self.path)
486
+ else:
487
+ raise ObjectWriteError(
488
+ f"A world already exists at the path {self.path}"
489
+ )
490
+
491
+ version = self.translation_manager.get_version(
492
+ self.platform, self.version
493
+ ).version_number
494
+ self._version = version + (0,) * (5 - len(version))
495
+
496
+ self.root_tag = root = CompoundTag()
497
+ root["StorageVersion"] = IntTag(8)
498
+ root["lastOpenedWithVersion"] = ListTag([IntTag(i) for i in self._version])
499
+ root["Generator"] = IntTag(1)
500
+ root["LastPlayed"] = LongTag(int(time.time()))
501
+ root["LevelName"] = StringTag("World Created By Amulet")
502
+
503
+ os.makedirs(self.path, exist_ok=True)
504
+ self.root_tag.save()
505
+
506
+ db = LevelDB(os.path.join(self.path, "db"), True)
507
+ db.close()
508
+
509
+ self._reload_world()
510
+
511
+ @property
512
+ def has_lock(self) -> bool:
513
+ if self._has_lock:
514
+ return True # TODO: implement a check to ensure access to the database
515
+ return False
516
+
517
+ def _save(self):
518
+ os.makedirs(self.path, exist_ok=True)
519
+ self.root_tag.save()
520
+ with open(os.path.join(self.path, "levelname.txt"), "w", encoding="utf-8") as f:
521
+ f.write(self.level_name)
522
+
523
+ def _close(self):
524
+ self._db.close()
525
+ self._db = None
526
+ self._dimension_manager = None
527
+ self._actor_counter = None
528
+
529
+ def unload(self):
530
+ pass
531
+
532
+ def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
533
+ self._verify_has_lock()
534
+ if dimension in self._dimension_to_internal:
535
+ yield from self._dimension_manager.all_chunk_coords(
536
+ self._dimension_to_internal[dimension]
537
+ )
538
+
539
+ def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
540
+ if dimension in self._dimension_to_internal:
541
+ return self._dimension_manager.has_chunk(
542
+ cx, cz, self._dimension_to_internal[dimension]
543
+ )
544
+ return False
545
+
546
+ def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
547
+ if dimension in self._dimension_to_internal:
548
+ self._dimension_manager.delete_chunk(
549
+ cx, cz, self._dimension_to_internal[dimension]
550
+ )
551
+
552
+ def _put_raw_chunk_data(
553
+ self, cx: int, cz: int, data: ChunkData, dimension: Dimension
554
+ ):
555
+ self._dimension_manager.put_chunk_data(
556
+ cx, cz, data, self._dimension_to_internal[dimension]
557
+ )
558
+
559
+ def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> ChunkData:
560
+ """
561
+ Return the raw data as loaded from disk.
562
+
563
+ :param cx: The x coordinate of the chunk.
564
+ :param cz: The z coordinate of the chunk.
565
+ :param dimension: The dimension to load the data from.
566
+ :return: The raw chunk data.
567
+ """
568
+ if dimension not in self._dimension_to_internal:
569
+ raise ChunkDoesNotExist
570
+ return self._dimension_manager.get_chunk_data(
571
+ cx, cz, self._dimension_to_internal[dimension]
572
+ )
573
+
574
+ def all_player_ids(self) -> Iterable[str]:
575
+ """
576
+ Returns a generator of all player ids that are present in the level
577
+ """
578
+ yield from (
579
+ pid[7:].decode("utf-8")
580
+ for pid, _ in self._db.iterate(b"player_", b"player_\xFF")
581
+ )
582
+ if self.has_player(LOCAL_PLAYER):
583
+ yield LOCAL_PLAYER
584
+
585
+ def has_player(self, player_id: str) -> bool:
586
+ if player_id != LOCAL_PLAYER:
587
+ player_id = f"player_{player_id}"
588
+ return player_id.encode("utf-8") in self._db
589
+
590
+ def _load_player(self, player_id: str) -> Player:
591
+ """
592
+ Gets the :class:`Player` object that belongs to the specified player id
593
+
594
+ If no parameter is supplied, the data of the local player will be returned
595
+
596
+ :param player_id: The desired player id
597
+ :return: A Player instance
598
+ """
599
+ player_nbt = self._get_raw_player_data(player_id).compound
600
+ dimension = player_nbt["DimensionId"]
601
+ if isinstance(dimension, IntTag) and IntTag(0) <= dimension <= IntTag(2):
602
+ dimension_str = {
603
+ 0: OVERWORLD,
604
+ 1: THE_NETHER,
605
+ 2: THE_END,
606
+ }[dimension.py_int]
607
+ else:
608
+ dimension_str = OVERWORLD
609
+
610
+ # get the players position
611
+ pos_data = player_nbt.get("Pos")
612
+ if (
613
+ isinstance(pos_data, ListTag)
614
+ and len(pos_data) == 3
615
+ and pos_data.list_data_type == FloatTag.tag_id
616
+ ):
617
+ position = tuple(map(float, pos_data))
618
+ position = tuple(
619
+ p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
620
+ )
621
+ else:
622
+ position = (0.0, 0.0, 0.0)
623
+
624
+ # get the players rotation
625
+ rot_data = player_nbt.get("Rotation")
626
+ if (
627
+ isinstance(rot_data, ListTag)
628
+ and len(rot_data) == 2
629
+ and rot_data.list_data_type == FloatTag.tag_id
630
+ ):
631
+ rotation = tuple(map(float, rot_data))
632
+ rotation = tuple(
633
+ p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
634
+ )
635
+ else:
636
+ rotation = (0.0, 0.0)
637
+
638
+ return Player(
639
+ player_id,
640
+ dimension_str,
641
+ position,
642
+ rotation,
643
+ )
644
+
645
+ def _get_raw_player_data(self, player_id: str) -> NamedTag:
646
+ if player_id == LOCAL_PLAYER:
647
+ key = player_id.encode("utf-8")
648
+ else:
649
+ key = f"player_{player_id}".encode("utf-8")
650
+ try:
651
+ data = self._db.get(key)
652
+ except KeyError:
653
+ raise PlayerDoesNotExist(f"Player {player_id} doesn't exist")
654
+ return load_nbt(
655
+ data,
656
+ compressed=False,
657
+ little_endian=True,
658
+ string_decoder=utf8_escape_decoder,
659
+ )