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,769 +1,769 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import struct
5
- from typing import (
6
- Tuple,
7
- Dict,
8
- Generator,
9
- Optional,
10
- List,
11
- Union,
12
- Iterable,
13
- BinaryIO,
14
- Any,
15
- )
16
- import time
17
- import glob
18
- import shutil
19
- import json
20
- import logging
21
-
22
- import portalocker
23
-
24
- from amulet_nbt import (
25
- IntTag,
26
- LongTag,
27
- DoubleTag,
28
- StringTag,
29
- ListTag,
30
- CompoundTag,
31
- NamedTag,
32
- load as load_nbt,
33
- )
34
- from amulet.api.player import Player, LOCAL_PLAYER
35
- from amulet.api.chunk import Chunk
36
- from amulet.api.selection import SelectionGroup, SelectionBox
37
- from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
38
- from amulet.utils.format_utils import check_all_exist
39
- from amulet.api.errors import (
40
- DimensionDoesNotExist,
41
- ObjectWriteError,
42
- ChunkLoadError,
43
- ChunkDoesNotExist,
44
- PlayerDoesNotExist,
45
- )
46
- from amulet.api.data_types import (
47
- ChunkCoordinates,
48
- VersionNumberInt,
49
- PlatformType,
50
- DimensionCoordinates,
51
- AnyNDArray,
52
- Dimension,
53
- )
54
- from .dimension import AnvilDimensionManager, ChunkDataType
55
- from amulet.api import level as api_level
56
- from amulet.level.interfaces.chunk.anvil.base_anvil_interface import BaseAnvilInterface
57
- from .data_pack import DataPack, DataPackManager
58
-
59
- log = logging.getLogger(__name__)
60
-
61
- InternalDimension = str
62
- OVERWORLD = "minecraft:overworld"
63
- THE_NETHER = "minecraft:the_nether"
64
- THE_END = "minecraft:the_end"
65
-
66
-
67
- class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
68
- """
69
- This FormatWrapper class exists to interface with the Java world format.
70
- """
71
-
72
- def __init__(self, path: str):
73
- """
74
- Construct a new instance of :class:`AnvilFormat`.
75
-
76
- This should not be used directly. You should instead use :func:`amulet.load_format`.
77
-
78
- :param path: The file path to the serialised data.
79
- """
80
- super().__init__(path)
81
- self._platform = "java"
82
- self._root_tag: NamedTag = NamedTag()
83
- self._levels: Dict[InternalDimension, AnvilDimensionManager] = {}
84
- self._dimension_name_map: Dict[Dimension, InternalDimension] = {}
85
- self._mcc_support: Optional[bool] = None
86
- self._lock_time: Optional[bytes] = None
87
- self._lock: Optional[BinaryIO] = None
88
- self._data_pack: Optional[DataPackManager] = None
89
- self._shallow_load()
90
-
91
- def __del__(self):
92
- self.close()
93
-
94
- def _shallow_load(self):
95
- try:
96
- self._load_level_dat()
97
- except:
98
- pass
99
-
100
- def _load_level_dat(self):
101
- """Load the level.dat file and check the image file"""
102
- if os.path.isfile(os.path.join(self.path, "icon.png")):
103
- self._world_image_path = os.path.join(self.path, "icon.png")
104
- else:
105
- self._world_image_path = self._missing_world_icon
106
- self.root_tag = load_nbt(os.path.join(self.path, "level.dat"))
107
-
108
- @staticmethod
109
- def is_valid(path: str) -> bool:
110
- if not check_all_exist(path, "level.dat"):
111
- return False
112
-
113
- try:
114
- level_dat_root = load_nbt(os.path.join(path, "level.dat")).compound
115
- except:
116
- return False
117
-
118
- return "Data" in level_dat_root and "FML" not in level_dat_root
119
-
120
- @property
121
- def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
122
- return {"java": (True, True)}
123
-
124
- @property
125
- def version(self) -> VersionNumberInt:
126
- """The data version number that the world was last opened in. eg 2578"""
127
- if self._version is None:
128
- self._version = self._get_version()
129
- return self._version
130
-
131
- def _get_version(self) -> VersionNumberInt:
132
- return (
133
- self.root_tag.compound.get_compound("Data", CompoundTag())
134
- .get_int("DataVersion", IntTag(-1))
135
- .py_int
136
- )
137
-
138
- @property
139
- def root_tag(self) -> NamedTag:
140
- """The level.dat data for the level."""
141
- return self._root_tag
142
-
143
- @root_tag.setter
144
- def root_tag(self, root_tag: Union[NamedTag, CompoundTag]):
145
- if isinstance(root_tag, CompoundTag):
146
- self._root_tag = NamedTag(root_tag)
147
- elif isinstance(root_tag, NamedTag):
148
- self._root_tag = root_tag
149
- else:
150
- raise ValueError("root_tag must be a CompoundTag or NamedTag")
151
-
152
- @property
153
- def level_name(self) -> str:
154
- return (
155
- self.root_tag.compound.get_compound("Data").get_string("LevelName").py_str
156
- )
157
-
158
- @level_name.setter
159
- def level_name(self, value: str):
160
- self.root_tag.compound.setdefault_compound("Data")["LevelName"] = StringTag(
161
- value
162
- )
163
-
164
- @property
165
- def last_played(self) -> int:
166
- return (
167
- self.root_tag.compound.get_compound("Data")
168
- .get_long("LastPlayed", LongTag())
169
- .py_int
170
- // 1000
171
- )
172
-
173
- @property
174
- def game_version_string(self) -> str:
175
- try:
176
- return f'Java {self.root_tag.compound.get_compound("Data").get_compound("Version").get_string("Name").py_str}'
177
- except Exception:
178
- return f"Java Unknown Version"
179
-
180
- @property
181
- def data_pack(self) -> DataPackManager:
182
- if self._data_pack is None:
183
- packs = []
184
- enabled_packs = (
185
- self.root_tag.compound.get_compound("Data")
186
- .get_compound("DataPacks", CompoundTag())
187
- .get_list("Enabled", ListTag())
188
- )
189
- for pack in enabled_packs:
190
- if isinstance(pack, StringTag):
191
- pack_name: str = pack.py_str
192
- if pack_name == "vanilla":
193
- pass
194
- elif pack_name.startswith("file/"):
195
- path = os.path.join(self.path, "datapacks", pack_name[5:])
196
- if DataPack.is_path_valid(path):
197
- packs.append(DataPack(path))
198
- self._data_pack = DataPackManager(packs)
199
- return self._data_pack
200
-
201
- @property
202
- def dimensions(self) -> List[Dimension]:
203
- return list(self._dimension_name_map.keys())
204
-
205
- def _register_dimension(
206
- self,
207
- relative_dimension_path: InternalDimension,
208
- dimension_name: Optional[Dimension] = None,
209
- ):
210
- """
211
- Register a new dimension.
212
-
213
- :param relative_dimension_path: The relative path to the dimension directory from the world root. "" for the world root.
214
- :param dimension_name: The name of the dimension shown to the user
215
- """
216
- if dimension_name is None:
217
- dimension_name: Dimension = relative_dimension_path
218
-
219
- if relative_dimension_path:
220
- path = os.path.join(self.path, relative_dimension_path)
221
- else:
222
- path = self.path
223
-
224
- if (
225
- relative_dimension_path not in self._levels
226
- and dimension_name not in self._dimension_name_map
227
- ):
228
- self._levels[relative_dimension_path] = AnvilDimensionManager(
229
- path,
230
- mcc=self._mcc_support,
231
- layers=("region",) + ("entities",) * (self.version >= 2681),
232
- )
233
- self._dimension_name_map[dimension_name] = relative_dimension_path
234
- self._bounds[dimension_name] = self._get_dimenion_bounds(dimension_name)
235
-
236
- def _get_dimenion_bounds(self, dimension_type_str: Dimension) -> SelectionGroup:
237
- if self.version >= 2709: # This number might be smaller
238
- # If in a version that supports custom height data packs
239
- dimension_settings = (
240
- self.root_tag.compound.get_compound("Data", CompoundTag())
241
- .get_compound("WorldGenSettings", CompoundTag())
242
- .get_compound("dimensions", CompoundTag())
243
- .get_compound(dimension_type_str, CompoundTag())
244
- )
245
-
246
- # "type" can be a reference (string) or inline (compound) dimension-type data.
247
- dimension_type = dimension_settings.get("type")
248
-
249
- if isinstance(dimension_type, StringTag):
250
- # Reference type. Load the dimension data
251
- dimension_type_str = dimension_type.py_str
252
- if ":" in dimension_type_str:
253
- namespace, base_name = dimension_type_str.split(":", 1)
254
- else:
255
- namespace = "minecraft"
256
- base_name = dimension_type_str
257
- name_tuple = namespace, base_name
258
-
259
- # First try and load the reference from the data pack and then from defaults
260
- dimension_path = f"data/{namespace}/dimension_type/{base_name}.json"
261
- if self.data_pack.has_file(dimension_path):
262
- with self.data_pack.open(dimension_path) as d:
263
- try:
264
- dimension_settings_json = json.load(d)
265
- except json.JSONDecodeError:
266
- pass
267
- else:
268
- if "min_y" in dimension_settings_json and isinstance(
269
- dimension_settings_json["min_y"], int
270
- ):
271
- min_y = dimension_settings_json["min_y"]
272
- if min_y % 16:
273
- min_y = 16 * (min_y // 16)
274
- else:
275
- min_y = 0
276
- if "height" in dimension_settings_json and isinstance(
277
- dimension_settings_json["height"], int
278
- ):
279
- height = dimension_settings_json["height"]
280
- if height % 16:
281
- height = -16 * (-height // 16)
282
- else:
283
- height = 256
284
-
285
- return SelectionGroup(
286
- SelectionBox(
287
- (-30_000_000, min_y, -30_000_000),
288
- (30_000_000, min_y + height, 30_000_000),
289
- )
290
- )
291
-
292
- elif name_tuple in {
293
- ("minecraft", "overworld"),
294
- ("minecraft", "overworld_caves"),
295
- }:
296
- if self.version >= 2825:
297
- # If newer than the height change version
298
- return SelectionGroup(
299
- SelectionBox(
300
- (-30_000_000, -64, -30_000_000),
301
- (30_000_000, 320, 30_000_000),
302
- )
303
- )
304
- else:
305
- return DefaultSelection
306
- elif name_tuple in {
307
- ("minecraft", "the_nether"),
308
- ("minecraft", "the_end"),
309
- }:
310
- return DefaultSelection
311
- else:
312
- log.error(f"Could not find dimension_type {':'.join(name_tuple)}")
313
-
314
- elif isinstance(dimension_type, CompoundTag):
315
- # Inline type
316
- dimension_type_compound = dimension_type
317
- min_y = (
318
- dimension_type_compound.get_int("min_y", IntTag()).py_int // 16
319
- ) * 16
320
- height = (
321
- -dimension_type_compound.get_int("height", IntTag(256)).py_int // 16
322
- ) * -16
323
- return SelectionGroup(
324
- SelectionBox(
325
- (-30_000_000, min_y, -30_000_000),
326
- (30_000_000, min_y + height, 30_000_000),
327
- )
328
- )
329
- else:
330
- log.error(
331
- f'level_dat["Data"]["WorldGenSettings"]["dimensions"]["{dimension_type_str}"]["type"] was not a StringTag or CompoundTag.'
332
- )
333
-
334
- # Return the default if nothing else returned
335
- return DefaultSelection
336
-
337
- def _get_interface(self, raw_chunk_data: Optional[Any] = None) -> "Interface":
338
- from amulet.level.loader import Interfaces
339
-
340
- key = self._get_interface_key(raw_chunk_data)
341
- return Interfaces.get(key)
342
-
343
- def _get_interface_key(
344
- self, raw_chunk_data: Optional[ChunkDataType] = None
345
- ) -> Tuple[str, int]:
346
- if raw_chunk_data is None:
347
- return self.max_world_version
348
- else:
349
- return (
350
- self.platform,
351
- raw_chunk_data.get("region", NamedTag())
352
- .compound.get_int("DataVersion", IntTag(-1))
353
- .py_int,
354
- )
355
-
356
- def _decode(
357
- self,
358
- interface: BaseAnvilInterface,
359
- dimension: Dimension,
360
- cx: int,
361
- cz: int,
362
- raw_chunk_data: ChunkDataType,
363
- ) -> Tuple[Chunk, AnyNDArray]:
364
- bounds = self.bounds(dimension).bounds
365
- return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
366
-
367
- def _encode(
368
- self,
369
- interface: BaseAnvilInterface,
370
- chunk: Chunk,
371
- dimension: Dimension,
372
- chunk_palette: AnyNDArray,
373
- ) -> ChunkDataType:
374
- bounds = self.bounds(dimension).bounds
375
- return interface.encode(
376
- chunk, chunk_palette, self.max_world_version, (bounds[0][1], bounds[1][1])
377
- )
378
-
379
- def _reload_world(self):
380
- # reload the level.dat in case it has changed
381
- self._load_level_dat()
382
-
383
- # create the session.lock file (this has mostly been lifted from MCEdit)
384
- try:
385
- # open the file for writing and reading and lock it
386
- self._lock = open(os.path.join(self.path, "session.lock"), "wb+")
387
- portalocker.lock(self._lock, portalocker.LockFlags.EXCLUSIVE)
388
-
389
- # write the current time to the file
390
- self._lock_time = struct.pack(">Q", int(time.time() * 1000))
391
- self._lock.write(self._lock_time)
392
-
393
- # flush the changes to disk
394
- self._lock.flush()
395
- os.fsync(self._lock.fileno())
396
-
397
- except Exception as e:
398
- self._lock_time = None
399
- if self._lock is not None:
400
- self._lock.close()
401
- self._lock = None
402
-
403
- self._is_open = False
404
- self._has_lock = False
405
- raise Exception(
406
- f"Could not access session.lock. The world may be open somewhere else.\n{e}"
407
- ) from e
408
-
409
- self._is_open = True
410
- self._has_lock = True
411
-
412
- # the real number might actually be lower
413
- self._mcc_support = self.version > 2203
414
-
415
- self._levels.clear()
416
- self._bounds.clear()
417
-
418
- # load all the levels
419
- self._register_dimension("", OVERWORLD)
420
- self._register_dimension("DIM-1", THE_NETHER)
421
- self._register_dimension("DIM1", THE_END)
422
-
423
- for level_path in glob.glob(os.path.join(glob.escape(self.path), "DIM*")):
424
- if os.path.isdir(level_path):
425
- dir_name = os.path.basename(level_path)
426
- if AnvilDimensionManager.level_regex.fullmatch(dir_name) is None:
427
- continue
428
- self._register_dimension(dir_name)
429
-
430
- for region_path in glob.glob(
431
- os.path.join(
432
- glob.escape(self.path), "dimensions", "*", "*", "**", "region"
433
- ),
434
- recursive=True,
435
- ):
436
- if not os.path.isdir(region_path):
437
- continue
438
- dimension_path = os.path.dirname(region_path)
439
- rel_dim_path = os.path.relpath(dimension_path, self.path)
440
- _, dimension, *base_name = rel_dim_path.split(os.sep)
441
-
442
- dimension_name = f"{dimension}:{'/'.join(base_name)}"
443
- self._register_dimension(rel_dim_path, dimension_name)
444
-
445
- def _open(self):
446
- """Open the database for reading and writing"""
447
- self._reload_world()
448
-
449
- def _create(
450
- self,
451
- overwrite: bool,
452
- bounds: Union[
453
- SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
454
- ] = None,
455
- **kwargs,
456
- ):
457
- if os.path.isdir(self.path):
458
- if overwrite:
459
- shutil.rmtree(self.path)
460
- else:
461
- raise ObjectWriteError(
462
- f"A world already exists at the path {self.path}"
463
- )
464
- self._version = self.translation_manager.get_version(
465
- self.platform, self.version
466
- ).data_version
467
-
468
- self.root_tag = root = CompoundTag()
469
- root["Data"] = data = CompoundTag()
470
- data["version"] = IntTag(19133)
471
- data["DataVersion"] = IntTag(self._version)
472
- data["LastPlayed"] = LongTag(int(time.time() * 1000))
473
- data["LevelName"] = StringTag("World Created By Amulet")
474
-
475
- os.makedirs(self.path, exist_ok=True)
476
- self.root_tag.save_to(os.path.join(self.path, "level.dat"))
477
- self._reload_world()
478
-
479
- @property
480
- def has_lock(self) -> bool:
481
- if self._has_lock:
482
- self._lock.seek(0)
483
- return self._lock.read(8) == self._lock_time
484
- return False
485
-
486
- def pre_save_operation(
487
- self, level: api_level.BaseLevel
488
- ) -> Generator[float, None, bool]:
489
- changed_chunks = list(level.chunks.changed_chunks())
490
- height = self._calculate_height(level, changed_chunks)
491
- try:
492
- while True:
493
- yield next(height) / 2
494
- except StopIteration as e:
495
- height_changed = e.value
496
-
497
- # light = self._calculate_light(level, changed_chunks)
498
- # try:
499
- # while True:
500
- # yield next(light) / 2
501
- # except StopIteration as e:
502
- # light_changed = e.value
503
-
504
- return height_changed # or light_changed
505
-
506
- @staticmethod
507
- def _calculate_height(
508
- level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
509
- ) -> Generator[float, None, bool]:
510
- """Calculate the height values for chunks."""
511
- chunk_count = len(chunks)
512
- # it looks like the game recalculates the height value if not defined.
513
- # Just delete the stored height values so that they do not get written back.
514
- # tested as of 1.12.2. This may not be true for older versions.
515
- changed = False
516
- for i, (dimension, cx, cz) in enumerate(chunks):
517
- try:
518
- chunk = level.get_chunk(cx, cz, dimension)
519
- except ChunkLoadError:
520
- pass
521
- else:
522
- changed_ = False
523
- changed_ |= chunk.misc.pop("height_mapC", None) is not None
524
- changed_ |= chunk.misc.pop("height_map256IA", None) is not None
525
- if changed_:
526
- changed = True
527
- chunk.changed = True
528
- yield i / chunk_count
529
- return changed
530
-
531
- @staticmethod
532
- def _calculate_light(
533
- level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
534
- ) -> Generator[float, None, bool]:
535
- """Calculate the height values for chunks."""
536
- # this is needed for before 1.14
537
- chunk_count = len(chunks)
538
- changed = False
539
- if level.level_wrapper.version < 1934:
540
- # the version may be less than 1934 but is at least 1924
541
- # calculate the light values
542
- pass
543
- # TODO
544
- else:
545
- # the game will recalculate the light levels
546
- for i, (dimension, cx, cz) in enumerate(chunks):
547
- try:
548
- chunk = level.get_chunk(cx, cz, dimension)
549
- except ChunkLoadError:
550
- pass
551
- else:
552
- changed_ = False
553
- changed_ |= chunk.misc.pop("block_light", None) is not None
554
- changed_ |= chunk.misc.pop("sky_light", None) is not None
555
- if changed_:
556
- changed = True
557
- chunk.changed = True
558
- yield i / chunk_count
559
- return changed
560
-
561
- def _save(self):
562
- """Save the data back to the disk database"""
563
- os.makedirs(self.path, exist_ok=True)
564
- self.root_tag.save_to(os.path.join(self.path, "level.dat"))
565
- # TODO: save other world data
566
-
567
- def _close(self):
568
- """Close the disk database"""
569
- if self._lock is not None:
570
- portalocker.unlock(self._lock)
571
- self._lock.close()
572
-
573
- def unload(self):
574
- for level in self._levels.values():
575
- level.unload()
576
-
577
- def _has_dimension(self, dimension: Dimension):
578
- return (
579
- dimension in self._dimension_name_map
580
- and self._dimension_name_map[dimension] in self._levels
581
- )
582
-
583
- def _get_dimension(self, dimension: Dimension):
584
- self._verify_has_lock()
585
- if self._has_dimension(dimension):
586
- return self._levels[self._dimension_name_map[dimension]]
587
- else:
588
- raise DimensionDoesNotExist(dimension)
589
-
590
- def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
591
- if self._has_dimension(dimension):
592
- yield from self._get_dimension(dimension).all_chunk_coords()
593
-
594
- def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
595
- return self._has_dimension(dimension) and self._get_dimension(
596
- dimension
597
- ).has_chunk(cx, cz)
598
-
599
- def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
600
- """Delete a chunk from a given dimension"""
601
- if self._has_dimension(dimension):
602
- self._get_dimension(dimension).delete_chunk(cx, cz)
603
-
604
- # TODO: add a new version of this method that handles all the raw data
605
- def put_raw_chunk_data(
606
- self, cx: int, cz: int, data: NamedTag, dimension: Dimension
607
- ):
608
- """
609
- Commit the raw chunk data to the FormatWrapper cache.
610
-
611
- Call :meth:`save` to push all the cache data to the level.
612
-
613
- :param cx: The x coordinate of the chunk.
614
- :param cz: The z coordinate of the chunk.
615
- :param data: The raw data to commit to the level.
616
- :param dimension: The dimension to load the data from.
617
- """
618
- self._verify_has_lock()
619
- self._put_raw_chunk_data(cx, cz, {"region": data}, dimension)
620
-
621
- def _put_raw_chunk_data(
622
- self, cx: int, cz: int, data: ChunkDataType, dimension: Dimension
623
- ):
624
- self._get_dimension(dimension).put_chunk_data_layers(cx, cz, data)
625
-
626
- # TODO: add a new version of this method that handles all the raw data
627
- def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> NamedTag:
628
- """
629
- Return the raw data as loaded from disk.
630
-
631
- :param cx: The x coordinate of the chunk.
632
- :param cz: The z coordinate of the chunk.
633
- :param dimension: The dimension to load the data from.
634
- :return: The raw chunk data.
635
- """
636
- # TODO: make this return layer data
637
- return self._safe_load(
638
- self._legacy_get_raw_chunk_data,
639
- (cx, cz, dimension),
640
- "Error loading chunk {} {} {}",
641
- ChunkLoadError,
642
- ChunkDoesNotExist,
643
- )
644
-
645
- def _legacy_get_raw_chunk_data(
646
- self, cx: int, cz: int, dimension: Dimension
647
- ) -> NamedTag:
648
- layers = self._get_raw_chunk_data(cx, cz, dimension)
649
- if "region" in layers:
650
- return layers["region"]
651
- else:
652
- raise ChunkDoesNotExist
653
-
654
- def _get_raw_chunk_data(
655
- self, cx: int, cz: int, dimension: Dimension
656
- ) -> ChunkDataType:
657
- """
658
- Return the raw data as loaded from disk.
659
-
660
- :param cx: The x coordinate of the chunk.
661
- :param cz: The z coordinate of the chunk.
662
- :param dimension: The dimension to load the data from.
663
- :return: The raw chunk data.
664
- """
665
- return self._get_dimension(dimension).get_chunk_data_layers(cx, cz)
666
-
667
- def all_player_ids(self) -> Iterable[str]:
668
- """
669
- Returns a generator of all player ids that are present in the level
670
- """
671
- for f in glob.iglob(
672
- os.path.join(glob.escape(self.path), "playerdata", "*.dat")
673
- ):
674
- yield os.path.splitext(os.path.basename(f))[0]
675
- if self.has_player(LOCAL_PLAYER):
676
- yield LOCAL_PLAYER
677
-
678
- def has_player(self, player_id: str) -> bool:
679
- if player_id == LOCAL_PLAYER:
680
- return "Player" in self.root_tag.compound.get_compound("Data")
681
- else:
682
- return os.path.isfile(
683
- os.path.join(self.path, "playerdata", f"{player_id}.dat")
684
- )
685
-
686
- def _load_player(self, player_id: str) -> Player:
687
- """
688
- Gets the :class:`Player` object that belongs to the specified player id
689
-
690
- If no parameter is supplied, the data of the local player will be returned
691
-
692
- :param player_id: The desired player id
693
- :return: A Player instance
694
- """
695
- player_nbt = self._get_raw_player_data(player_id)
696
- dimension = player_nbt["Dimension"]
697
- # TODO: rework this when there is better dimension support.
698
- if isinstance(dimension, IntTag):
699
- if -1 <= dimension.py_int <= 1:
700
- dimension_str = {-1: THE_NETHER, 0: OVERWORLD, 1: THE_END}[
701
- dimension.py_int
702
- ]
703
- else:
704
- dimension_str = f"DIM{dimension}"
705
- elif isinstance(dimension, StringTag):
706
- dimension_str = dimension.py_str
707
- else:
708
- dimension_str = OVERWORLD
709
- if dimension_str not in self._dimension_name_map:
710
- dimension_str = OVERWORLD
711
-
712
- # get the players position
713
- pos_data = player_nbt.get("Pos")
714
- if (
715
- isinstance(pos_data, ListTag)
716
- and len(pos_data) == 3
717
- and pos_data.list_data_type == DoubleTag.tag_id
718
- ):
719
- position = tuple(map(float, pos_data))
720
- position = tuple(
721
- p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
722
- )
723
- else:
724
- position = (0.0, 0.0, 0.0)
725
-
726
- # get the players rotation
727
- rot_data = player_nbt.get("Rotation")
728
- if (
729
- isinstance(rot_data, ListTag)
730
- and len(rot_data) == 2
731
- and rot_data.list_data_type == DoubleTag.tag_id
732
- ):
733
- rotation = tuple(map(float, rot_data))
734
- rotation = tuple(
735
- p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
736
- )
737
- else:
738
- rotation = (0.0, 0.0)
739
-
740
- return Player(
741
- player_id,
742
- dimension_str,
743
- position,
744
- rotation,
745
- )
746
-
747
- def _get_raw_player_data(self, player_id: str) -> CompoundTag:
748
- if player_id == LOCAL_PLAYER:
749
- if "Player" in self.root_tag.compound.get_compound("Data"):
750
- return self.root_tag.compound.get_compound("Data").get_compound(
751
- "Player"
752
- )
753
- else:
754
- raise PlayerDoesNotExist("Local player doesn't exist")
755
- else:
756
- path = os.path.join(self.path, "playerdata", f"{player_id}.dat")
757
- if os.path.exists(path):
758
- return load_nbt(path).compound
759
- raise PlayerDoesNotExist(f"Player {player_id} does not exist")
760
-
761
-
762
- if __name__ == "__main__":
763
- import sys
764
-
765
- world_path = sys.argv[1]
766
- world = AnvilDimensionManager(world_path)
767
- chunk_ = world.get_chunk_data(0, 0)
768
- print(chunk_)
769
- world.put_chunk_data(0, 0, chunk_)
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import struct
5
+ from typing import (
6
+ Tuple,
7
+ Dict,
8
+ Generator,
9
+ Optional,
10
+ List,
11
+ Union,
12
+ Iterable,
13
+ BinaryIO,
14
+ Any,
15
+ )
16
+ import time
17
+ import glob
18
+ import shutil
19
+ import json
20
+ import logging
21
+
22
+ import portalocker
23
+
24
+ from amulet_nbt import (
25
+ IntTag,
26
+ LongTag,
27
+ DoubleTag,
28
+ StringTag,
29
+ ListTag,
30
+ CompoundTag,
31
+ NamedTag,
32
+ load as load_nbt,
33
+ )
34
+ from amulet.api.player import Player, LOCAL_PLAYER
35
+ from amulet.api.chunk import Chunk
36
+ from amulet.api.selection import SelectionGroup, SelectionBox
37
+ from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
38
+ from amulet.utils.format_utils import check_all_exist
39
+ from amulet.api.errors import (
40
+ DimensionDoesNotExist,
41
+ ObjectWriteError,
42
+ ChunkLoadError,
43
+ ChunkDoesNotExist,
44
+ PlayerDoesNotExist,
45
+ )
46
+ from amulet.api.data_types import (
47
+ ChunkCoordinates,
48
+ VersionNumberInt,
49
+ PlatformType,
50
+ DimensionCoordinates,
51
+ AnyNDArray,
52
+ Dimension,
53
+ )
54
+ from .dimension import AnvilDimensionManager, ChunkDataType
55
+ from amulet.api import level as api_level
56
+ from amulet.level.interfaces.chunk.anvil.base_anvil_interface import BaseAnvilInterface
57
+ from .data_pack import DataPack, DataPackManager
58
+
59
+ log = logging.getLogger(__name__)
60
+
61
+ InternalDimension = str
62
+ OVERWORLD = "minecraft:overworld"
63
+ THE_NETHER = "minecraft:the_nether"
64
+ THE_END = "minecraft:the_end"
65
+
66
+
67
+ class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
68
+ """
69
+ This FormatWrapper class exists to interface with the Java world format.
70
+ """
71
+
72
+ def __init__(self, path: str):
73
+ """
74
+ Construct a new instance of :class:`AnvilFormat`.
75
+
76
+ This should not be used directly. You should instead use :func:`amulet.load_format`.
77
+
78
+ :param path: The file path to the serialised data.
79
+ """
80
+ super().__init__(path)
81
+ self._platform = "java"
82
+ self._root_tag: NamedTag = NamedTag()
83
+ self._levels: Dict[InternalDimension, AnvilDimensionManager] = {}
84
+ self._dimension_name_map: Dict[Dimension, InternalDimension] = {}
85
+ self._mcc_support: Optional[bool] = None
86
+ self._lock_time: Optional[bytes] = None
87
+ self._lock: Optional[BinaryIO] = None
88
+ self._data_pack: Optional[DataPackManager] = None
89
+ self._shallow_load()
90
+
91
+ def __del__(self):
92
+ self.close()
93
+
94
+ def _shallow_load(self):
95
+ try:
96
+ self._load_level_dat()
97
+ except:
98
+ pass
99
+
100
+ def _load_level_dat(self):
101
+ """Load the level.dat file and check the image file"""
102
+ if os.path.isfile(os.path.join(self.path, "icon.png")):
103
+ self._world_image_path = os.path.join(self.path, "icon.png")
104
+ else:
105
+ self._world_image_path = self._missing_world_icon
106
+ self.root_tag = load_nbt(os.path.join(self.path, "level.dat"))
107
+
108
+ @staticmethod
109
+ def is_valid(path: str) -> bool:
110
+ if not check_all_exist(path, "level.dat"):
111
+ return False
112
+
113
+ try:
114
+ level_dat_root = load_nbt(os.path.join(path, "level.dat")).compound
115
+ except:
116
+ return False
117
+
118
+ return "Data" in level_dat_root and "FML" not in level_dat_root
119
+
120
+ @property
121
+ def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
122
+ return {"java": (True, True)}
123
+
124
+ @property
125
+ def version(self) -> VersionNumberInt:
126
+ """The data version number that the world was last opened in. eg 2578"""
127
+ if self._version is None:
128
+ self._version = self._get_version()
129
+ return self._version
130
+
131
+ def _get_version(self) -> VersionNumberInt:
132
+ return (
133
+ self.root_tag.compound.get_compound("Data", CompoundTag())
134
+ .get_int("DataVersion", IntTag(-1))
135
+ .py_int
136
+ )
137
+
138
+ @property
139
+ def root_tag(self) -> NamedTag:
140
+ """The level.dat data for the level."""
141
+ return self._root_tag
142
+
143
+ @root_tag.setter
144
+ def root_tag(self, root_tag: Union[NamedTag, CompoundTag]):
145
+ if isinstance(root_tag, CompoundTag):
146
+ self._root_tag = NamedTag(root_tag)
147
+ elif isinstance(root_tag, NamedTag):
148
+ self._root_tag = root_tag
149
+ else:
150
+ raise ValueError("root_tag must be a CompoundTag or NamedTag")
151
+
152
+ @property
153
+ def level_name(self) -> str:
154
+ return (
155
+ self.root_tag.compound.get_compound("Data").get_string("LevelName").py_str
156
+ )
157
+
158
+ @level_name.setter
159
+ def level_name(self, value: str):
160
+ self.root_tag.compound.setdefault_compound("Data")["LevelName"] = StringTag(
161
+ value
162
+ )
163
+
164
+ @property
165
+ def last_played(self) -> int:
166
+ return (
167
+ self.root_tag.compound.get_compound("Data")
168
+ .get_long("LastPlayed", LongTag())
169
+ .py_int
170
+ // 1000
171
+ )
172
+
173
+ @property
174
+ def game_version_string(self) -> str:
175
+ try:
176
+ return f'Java {self.root_tag.compound.get_compound("Data").get_compound("Version").get_string("Name").py_str}'
177
+ except Exception:
178
+ return f"Java Unknown Version"
179
+
180
+ @property
181
+ def data_pack(self) -> DataPackManager:
182
+ if self._data_pack is None:
183
+ packs = []
184
+ enabled_packs = (
185
+ self.root_tag.compound.get_compound("Data")
186
+ .get_compound("DataPacks", CompoundTag())
187
+ .get_list("Enabled", ListTag())
188
+ )
189
+ for pack in enabled_packs:
190
+ if isinstance(pack, StringTag):
191
+ pack_name: str = pack.py_str
192
+ if pack_name == "vanilla":
193
+ pass
194
+ elif pack_name.startswith("file/"):
195
+ path = os.path.join(self.path, "datapacks", pack_name[5:])
196
+ if DataPack.is_path_valid(path):
197
+ packs.append(DataPack(path))
198
+ self._data_pack = DataPackManager(packs)
199
+ return self._data_pack
200
+
201
+ @property
202
+ def dimensions(self) -> List[Dimension]:
203
+ return list(self._dimension_name_map.keys())
204
+
205
+ def _register_dimension(
206
+ self,
207
+ relative_dimension_path: InternalDimension,
208
+ dimension_name: Optional[Dimension] = None,
209
+ ):
210
+ """
211
+ Register a new dimension.
212
+
213
+ :param relative_dimension_path: The relative path to the dimension directory from the world root. "" for the world root.
214
+ :param dimension_name: The name of the dimension shown to the user
215
+ """
216
+ if dimension_name is None:
217
+ dimension_name: Dimension = relative_dimension_path
218
+
219
+ if relative_dimension_path:
220
+ path = os.path.join(self.path, relative_dimension_path)
221
+ else:
222
+ path = self.path
223
+
224
+ if (
225
+ relative_dimension_path not in self._levels
226
+ and dimension_name not in self._dimension_name_map
227
+ ):
228
+ self._levels[relative_dimension_path] = AnvilDimensionManager(
229
+ path,
230
+ mcc=self._mcc_support,
231
+ layers=("region",) + ("entities",) * (self.version >= 2681),
232
+ )
233
+ self._dimension_name_map[dimension_name] = relative_dimension_path
234
+ self._bounds[dimension_name] = self._get_dimenion_bounds(dimension_name)
235
+
236
+ def _get_dimenion_bounds(self, dimension_type_str: Dimension) -> SelectionGroup:
237
+ if self.version >= 2709: # This number might be smaller
238
+ # If in a version that supports custom height data packs
239
+ dimension_settings = (
240
+ self.root_tag.compound.get_compound("Data", CompoundTag())
241
+ .get_compound("WorldGenSettings", CompoundTag())
242
+ .get_compound("dimensions", CompoundTag())
243
+ .get_compound(dimension_type_str, CompoundTag())
244
+ )
245
+
246
+ # "type" can be a reference (string) or inline (compound) dimension-type data.
247
+ dimension_type = dimension_settings.get("type")
248
+
249
+ if isinstance(dimension_type, StringTag):
250
+ # Reference type. Load the dimension data
251
+ dimension_type_str = dimension_type.py_str
252
+ if ":" in dimension_type_str:
253
+ namespace, base_name = dimension_type_str.split(":", 1)
254
+ else:
255
+ namespace = "minecraft"
256
+ base_name = dimension_type_str
257
+ name_tuple = namespace, base_name
258
+
259
+ # First try and load the reference from the data pack and then from defaults
260
+ dimension_path = f"data/{namespace}/dimension_type/{base_name}.json"
261
+ if self.data_pack.has_file(dimension_path):
262
+ with self.data_pack.open(dimension_path) as d:
263
+ try:
264
+ dimension_settings_json = json.load(d)
265
+ except json.JSONDecodeError:
266
+ pass
267
+ else:
268
+ if "min_y" in dimension_settings_json and isinstance(
269
+ dimension_settings_json["min_y"], int
270
+ ):
271
+ min_y = dimension_settings_json["min_y"]
272
+ if min_y % 16:
273
+ min_y = 16 * (min_y // 16)
274
+ else:
275
+ min_y = 0
276
+ if "height" in dimension_settings_json and isinstance(
277
+ dimension_settings_json["height"], int
278
+ ):
279
+ height = dimension_settings_json["height"]
280
+ if height % 16:
281
+ height = -16 * (-height // 16)
282
+ else:
283
+ height = 256
284
+
285
+ return SelectionGroup(
286
+ SelectionBox(
287
+ (-30_000_000, min_y, -30_000_000),
288
+ (30_000_000, min_y + height, 30_000_000),
289
+ )
290
+ )
291
+
292
+ elif name_tuple in {
293
+ ("minecraft", "overworld"),
294
+ ("minecraft", "overworld_caves"),
295
+ }:
296
+ if self.version >= 2825:
297
+ # If newer than the height change version
298
+ return SelectionGroup(
299
+ SelectionBox(
300
+ (-30_000_000, -64, -30_000_000),
301
+ (30_000_000, 320, 30_000_000),
302
+ )
303
+ )
304
+ else:
305
+ return DefaultSelection
306
+ elif name_tuple in {
307
+ ("minecraft", "the_nether"),
308
+ ("minecraft", "the_end"),
309
+ }:
310
+ return DefaultSelection
311
+ else:
312
+ log.error(f"Could not find dimension_type {':'.join(name_tuple)}")
313
+
314
+ elif isinstance(dimension_type, CompoundTag):
315
+ # Inline type
316
+ dimension_type_compound = dimension_type
317
+ min_y = (
318
+ dimension_type_compound.get_int("min_y", IntTag()).py_int // 16
319
+ ) * 16
320
+ height = (
321
+ -dimension_type_compound.get_int("height", IntTag(256)).py_int // 16
322
+ ) * -16
323
+ return SelectionGroup(
324
+ SelectionBox(
325
+ (-30_000_000, min_y, -30_000_000),
326
+ (30_000_000, min_y + height, 30_000_000),
327
+ )
328
+ )
329
+ else:
330
+ log.error(
331
+ f'level_dat["Data"]["WorldGenSettings"]["dimensions"]["{dimension_type_str}"]["type"] was not a StringTag or CompoundTag.'
332
+ )
333
+
334
+ # Return the default if nothing else returned
335
+ return DefaultSelection
336
+
337
+ def _get_interface(self, raw_chunk_data: Optional[Any] = None) -> "Interface":
338
+ from amulet.level.loader import Interfaces
339
+
340
+ key = self._get_interface_key(raw_chunk_data)
341
+ return Interfaces.get(key)
342
+
343
+ def _get_interface_key(
344
+ self, raw_chunk_data: Optional[ChunkDataType] = None
345
+ ) -> Tuple[str, int]:
346
+ if raw_chunk_data is None:
347
+ return self.max_world_version
348
+ else:
349
+ return (
350
+ self.platform,
351
+ raw_chunk_data.get("region", NamedTag())
352
+ .compound.get_int("DataVersion", IntTag(-1))
353
+ .py_int,
354
+ )
355
+
356
+ def _decode(
357
+ self,
358
+ interface: BaseAnvilInterface,
359
+ dimension: Dimension,
360
+ cx: int,
361
+ cz: int,
362
+ raw_chunk_data: ChunkDataType,
363
+ ) -> Tuple[Chunk, AnyNDArray]:
364
+ bounds = self.bounds(dimension).bounds
365
+ return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
366
+
367
+ def _encode(
368
+ self,
369
+ interface: BaseAnvilInterface,
370
+ chunk: Chunk,
371
+ dimension: Dimension,
372
+ chunk_palette: AnyNDArray,
373
+ ) -> ChunkDataType:
374
+ bounds = self.bounds(dimension).bounds
375
+ return interface.encode(
376
+ chunk, chunk_palette, self.max_world_version, (bounds[0][1], bounds[1][1])
377
+ )
378
+
379
+ def _reload_world(self):
380
+ # reload the level.dat in case it has changed
381
+ self._load_level_dat()
382
+
383
+ # create the session.lock file (this has mostly been lifted from MCEdit)
384
+ try:
385
+ # open the file for writing and reading and lock it
386
+ self._lock = open(os.path.join(self.path, "session.lock"), "wb+")
387
+ portalocker.lock(self._lock, portalocker.LockFlags.EXCLUSIVE)
388
+
389
+ # write the current time to the file
390
+ self._lock_time = struct.pack(">Q", int(time.time() * 1000))
391
+ self._lock.write(self._lock_time)
392
+
393
+ # flush the changes to disk
394
+ self._lock.flush()
395
+ os.fsync(self._lock.fileno())
396
+
397
+ except Exception as e:
398
+ self._lock_time = None
399
+ if self._lock is not None:
400
+ self._lock.close()
401
+ self._lock = None
402
+
403
+ self._is_open = False
404
+ self._has_lock = False
405
+ raise Exception(
406
+ f"Could not access session.lock. The world may be open somewhere else.\n{e}"
407
+ ) from e
408
+
409
+ self._is_open = True
410
+ self._has_lock = True
411
+
412
+ # the real number might actually be lower
413
+ self._mcc_support = self.version > 2203
414
+
415
+ self._levels.clear()
416
+ self._bounds.clear()
417
+
418
+ # load all the levels
419
+ self._register_dimension("", OVERWORLD)
420
+ self._register_dimension("DIM-1", THE_NETHER)
421
+ self._register_dimension("DIM1", THE_END)
422
+
423
+ for level_path in glob.glob(os.path.join(glob.escape(self.path), "DIM*")):
424
+ if os.path.isdir(level_path):
425
+ dir_name = os.path.basename(level_path)
426
+ if AnvilDimensionManager.level_regex.fullmatch(dir_name) is None:
427
+ continue
428
+ self._register_dimension(dir_name)
429
+
430
+ for region_path in glob.glob(
431
+ os.path.join(
432
+ glob.escape(self.path), "dimensions", "*", "*", "**", "region"
433
+ ),
434
+ recursive=True,
435
+ ):
436
+ if not os.path.isdir(region_path):
437
+ continue
438
+ dimension_path = os.path.dirname(region_path)
439
+ rel_dim_path = os.path.relpath(dimension_path, self.path)
440
+ _, dimension, *base_name = rel_dim_path.split(os.sep)
441
+
442
+ dimension_name = f"{dimension}:{'/'.join(base_name)}"
443
+ self._register_dimension(rel_dim_path, dimension_name)
444
+
445
+ def _open(self):
446
+ """Open the database for reading and writing"""
447
+ self._reload_world()
448
+
449
+ def _create(
450
+ self,
451
+ overwrite: bool,
452
+ bounds: Union[
453
+ SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
454
+ ] = None,
455
+ **kwargs,
456
+ ):
457
+ if os.path.isdir(self.path):
458
+ if overwrite:
459
+ shutil.rmtree(self.path)
460
+ else:
461
+ raise ObjectWriteError(
462
+ f"A world already exists at the path {self.path}"
463
+ )
464
+ self._version = self.translation_manager.get_version(
465
+ self.platform, self.version
466
+ ).data_version
467
+
468
+ self.root_tag = root = CompoundTag()
469
+ root["Data"] = data = CompoundTag()
470
+ data["version"] = IntTag(19133)
471
+ data["DataVersion"] = IntTag(self._version)
472
+ data["LastPlayed"] = LongTag(int(time.time() * 1000))
473
+ data["LevelName"] = StringTag("World Created By Amulet")
474
+
475
+ os.makedirs(self.path, exist_ok=True)
476
+ self.root_tag.save_to(os.path.join(self.path, "level.dat"))
477
+ self._reload_world()
478
+
479
+ @property
480
+ def has_lock(self) -> bool:
481
+ if self._has_lock:
482
+ self._lock.seek(0)
483
+ return self._lock.read(8) == self._lock_time
484
+ return False
485
+
486
+ def pre_save_operation(
487
+ self, level: api_level.BaseLevel
488
+ ) -> Generator[float, None, bool]:
489
+ changed_chunks = list(level.chunks.changed_chunks())
490
+ height = self._calculate_height(level, changed_chunks)
491
+ try:
492
+ while True:
493
+ yield next(height) / 2
494
+ except StopIteration as e:
495
+ height_changed = e.value
496
+
497
+ # light = self._calculate_light(level, changed_chunks)
498
+ # try:
499
+ # while True:
500
+ # yield next(light) / 2
501
+ # except StopIteration as e:
502
+ # light_changed = e.value
503
+
504
+ return height_changed # or light_changed
505
+
506
+ @staticmethod
507
+ def _calculate_height(
508
+ level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
509
+ ) -> Generator[float, None, bool]:
510
+ """Calculate the height values for chunks."""
511
+ chunk_count = len(chunks)
512
+ # it looks like the game recalculates the height value if not defined.
513
+ # Just delete the stored height values so that they do not get written back.
514
+ # tested as of 1.12.2. This may not be true for older versions.
515
+ changed = False
516
+ for i, (dimension, cx, cz) in enumerate(chunks):
517
+ try:
518
+ chunk = level.get_chunk(cx, cz, dimension)
519
+ except ChunkLoadError:
520
+ pass
521
+ else:
522
+ changed_ = False
523
+ changed_ |= chunk.misc.pop("height_mapC", None) is not None
524
+ changed_ |= chunk.misc.pop("height_map256IA", None) is not None
525
+ if changed_:
526
+ changed = True
527
+ chunk.changed = True
528
+ yield i / chunk_count
529
+ return changed
530
+
531
+ @staticmethod
532
+ def _calculate_light(
533
+ level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
534
+ ) -> Generator[float, None, bool]:
535
+ """Calculate the height values for chunks."""
536
+ # this is needed for before 1.14
537
+ chunk_count = len(chunks)
538
+ changed = False
539
+ if level.level_wrapper.version < 1934:
540
+ # the version may be less than 1934 but is at least 1924
541
+ # calculate the light values
542
+ pass
543
+ # TODO
544
+ else:
545
+ # the game will recalculate the light levels
546
+ for i, (dimension, cx, cz) in enumerate(chunks):
547
+ try:
548
+ chunk = level.get_chunk(cx, cz, dimension)
549
+ except ChunkLoadError:
550
+ pass
551
+ else:
552
+ changed_ = False
553
+ changed_ |= chunk.misc.pop("block_light", None) is not None
554
+ changed_ |= chunk.misc.pop("sky_light", None) is not None
555
+ if changed_:
556
+ changed = True
557
+ chunk.changed = True
558
+ yield i / chunk_count
559
+ return changed
560
+
561
+ def _save(self):
562
+ """Save the data back to the disk database"""
563
+ os.makedirs(self.path, exist_ok=True)
564
+ self.root_tag.save_to(os.path.join(self.path, "level.dat"))
565
+ # TODO: save other world data
566
+
567
+ def _close(self):
568
+ """Close the disk database"""
569
+ if self._lock is not None:
570
+ portalocker.unlock(self._lock)
571
+ self._lock.close()
572
+
573
+ def unload(self):
574
+ for level in self._levels.values():
575
+ level.unload()
576
+
577
+ def _has_dimension(self, dimension: Dimension):
578
+ return (
579
+ dimension in self._dimension_name_map
580
+ and self._dimension_name_map[dimension] in self._levels
581
+ )
582
+
583
+ def _get_dimension(self, dimension: Dimension):
584
+ self._verify_has_lock()
585
+ if self._has_dimension(dimension):
586
+ return self._levels[self._dimension_name_map[dimension]]
587
+ else:
588
+ raise DimensionDoesNotExist(dimension)
589
+
590
+ def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
591
+ if self._has_dimension(dimension):
592
+ yield from self._get_dimension(dimension).all_chunk_coords()
593
+
594
+ def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
595
+ return self._has_dimension(dimension) and self._get_dimension(
596
+ dimension
597
+ ).has_chunk(cx, cz)
598
+
599
+ def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
600
+ """Delete a chunk from a given dimension"""
601
+ if self._has_dimension(dimension):
602
+ self._get_dimension(dimension).delete_chunk(cx, cz)
603
+
604
+ # TODO: add a new version of this method that handles all the raw data
605
+ def put_raw_chunk_data(
606
+ self, cx: int, cz: int, data: NamedTag, dimension: Dimension
607
+ ):
608
+ """
609
+ Commit the raw chunk data to the FormatWrapper cache.
610
+
611
+ Call :meth:`save` to push all the cache data to the level.
612
+
613
+ :param cx: The x coordinate of the chunk.
614
+ :param cz: The z coordinate of the chunk.
615
+ :param data: The raw data to commit to the level.
616
+ :param dimension: The dimension to load the data from.
617
+ """
618
+ self._verify_has_lock()
619
+ self._put_raw_chunk_data(cx, cz, {"region": data}, dimension)
620
+
621
+ def _put_raw_chunk_data(
622
+ self, cx: int, cz: int, data: ChunkDataType, dimension: Dimension
623
+ ):
624
+ self._get_dimension(dimension).put_chunk_data_layers(cx, cz, data)
625
+
626
+ # TODO: add a new version of this method that handles all the raw data
627
+ def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> NamedTag:
628
+ """
629
+ Return the raw data as loaded from disk.
630
+
631
+ :param cx: The x coordinate of the chunk.
632
+ :param cz: The z coordinate of the chunk.
633
+ :param dimension: The dimension to load the data from.
634
+ :return: The raw chunk data.
635
+ """
636
+ # TODO: make this return layer data
637
+ return self._safe_load(
638
+ self._legacy_get_raw_chunk_data,
639
+ (cx, cz, dimension),
640
+ "Error loading chunk {} {} {}",
641
+ ChunkLoadError,
642
+ ChunkDoesNotExist,
643
+ )
644
+
645
+ def _legacy_get_raw_chunk_data(
646
+ self, cx: int, cz: int, dimension: Dimension
647
+ ) -> NamedTag:
648
+ layers = self._get_raw_chunk_data(cx, cz, dimension)
649
+ if "region" in layers:
650
+ return layers["region"]
651
+ else:
652
+ raise ChunkDoesNotExist
653
+
654
+ def _get_raw_chunk_data(
655
+ self, cx: int, cz: int, dimension: Dimension
656
+ ) -> ChunkDataType:
657
+ """
658
+ Return the raw data as loaded from disk.
659
+
660
+ :param cx: The x coordinate of the chunk.
661
+ :param cz: The z coordinate of the chunk.
662
+ :param dimension: The dimension to load the data from.
663
+ :return: The raw chunk data.
664
+ """
665
+ return self._get_dimension(dimension).get_chunk_data_layers(cx, cz)
666
+
667
+ def all_player_ids(self) -> Iterable[str]:
668
+ """
669
+ Returns a generator of all player ids that are present in the level
670
+ """
671
+ for f in glob.iglob(
672
+ os.path.join(glob.escape(self.path), "playerdata", "*.dat")
673
+ ):
674
+ yield os.path.splitext(os.path.basename(f))[0]
675
+ if self.has_player(LOCAL_PLAYER):
676
+ yield LOCAL_PLAYER
677
+
678
+ def has_player(self, player_id: str) -> bool:
679
+ if player_id == LOCAL_PLAYER:
680
+ return "Player" in self.root_tag.compound.get_compound("Data")
681
+ else:
682
+ return os.path.isfile(
683
+ os.path.join(self.path, "playerdata", f"{player_id}.dat")
684
+ )
685
+
686
+ def _load_player(self, player_id: str) -> Player:
687
+ """
688
+ Gets the :class:`Player` object that belongs to the specified player id
689
+
690
+ If no parameter is supplied, the data of the local player will be returned
691
+
692
+ :param player_id: The desired player id
693
+ :return: A Player instance
694
+ """
695
+ player_nbt = self._get_raw_player_data(player_id)
696
+ dimension = player_nbt["Dimension"]
697
+ # TODO: rework this when there is better dimension support.
698
+ if isinstance(dimension, IntTag):
699
+ if -1 <= dimension.py_int <= 1:
700
+ dimension_str = {-1: THE_NETHER, 0: OVERWORLD, 1: THE_END}[
701
+ dimension.py_int
702
+ ]
703
+ else:
704
+ dimension_str = f"DIM{dimension}"
705
+ elif isinstance(dimension, StringTag):
706
+ dimension_str = dimension.py_str
707
+ else:
708
+ dimension_str = OVERWORLD
709
+ if dimension_str not in self._dimension_name_map:
710
+ dimension_str = OVERWORLD
711
+
712
+ # get the players position
713
+ pos_data = player_nbt.get("Pos")
714
+ if (
715
+ isinstance(pos_data, ListTag)
716
+ and len(pos_data) == 3
717
+ and pos_data.list_data_type == DoubleTag.tag_id
718
+ ):
719
+ position = tuple(map(float, pos_data))
720
+ position = tuple(
721
+ p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
722
+ )
723
+ else:
724
+ position = (0.0, 0.0, 0.0)
725
+
726
+ # get the players rotation
727
+ rot_data = player_nbt.get("Rotation")
728
+ if (
729
+ isinstance(rot_data, ListTag)
730
+ and len(rot_data) == 2
731
+ and rot_data.list_data_type == DoubleTag.tag_id
732
+ ):
733
+ rotation = tuple(map(float, rot_data))
734
+ rotation = tuple(
735
+ p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
736
+ )
737
+ else:
738
+ rotation = (0.0, 0.0)
739
+
740
+ return Player(
741
+ player_id,
742
+ dimension_str,
743
+ position,
744
+ rotation,
745
+ )
746
+
747
+ def _get_raw_player_data(self, player_id: str) -> CompoundTag:
748
+ if player_id == LOCAL_PLAYER:
749
+ if "Player" in self.root_tag.compound.get_compound("Data"):
750
+ return self.root_tag.compound.get_compound("Data").get_compound(
751
+ "Player"
752
+ )
753
+ else:
754
+ raise PlayerDoesNotExist("Local player doesn't exist")
755
+ else:
756
+ path = os.path.join(self.path, "playerdata", f"{player_id}.dat")
757
+ if os.path.exists(path):
758
+ return load_nbt(path).compound
759
+ raise PlayerDoesNotExist(f"Player {player_id} does not exist")
760
+
761
+
762
+ if __name__ == "__main__":
763
+ import sys
764
+
765
+ world_path = sys.argv[1]
766
+ world = AnvilDimensionManager(world_path)
767
+ chunk_ = world.get_chunk_data(0, 0)
768
+ print(chunk_)
769
+ world.put_chunk_data(0, 0, chunk_)