amulet-core 2.0a5__cp311-cp311-macosx_10_9_universal2.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 (210) hide show
  1. amulet/__init__.cpython-311-darwin.so +0 -0
  2. amulet/__init__.pyi +30 -0
  3. amulet/__pyinstaller/__init__.py +2 -0
  4. amulet/__pyinstaller/hook-amulet.py +4 -0
  5. amulet/_init.py +28 -0
  6. amulet/_version.py +21 -0
  7. amulet/biome.cpp +36 -0
  8. amulet/biome.hpp +43 -0
  9. amulet/biome.pyi +77 -0
  10. amulet/block.cpp +435 -0
  11. amulet/block.hpp +119 -0
  12. amulet/block.pyi +273 -0
  13. amulet/block_entity.cpp +12 -0
  14. amulet/block_entity.hpp +56 -0
  15. amulet/block_entity.pyi +80 -0
  16. amulet/chunk.cpp +16 -0
  17. amulet/chunk.hpp +99 -0
  18. amulet/chunk.pyi +30 -0
  19. amulet/chunk_/components/biome.py +155 -0
  20. amulet/chunk_/components/block_entity.py +117 -0
  21. amulet/chunk_/components/entity.py +64 -0
  22. amulet/chunk_/components/height_2d.py +16 -0
  23. amulet/chunk_components.pyi +95 -0
  24. amulet/collections.pyi +37 -0
  25. amulet/data_types.py +29 -0
  26. amulet/entity.py +180 -0
  27. amulet/errors.py +63 -0
  28. amulet/game/__init__.py +7 -0
  29. amulet/game/_game.py +152 -0
  30. amulet/game/_universal/__init__.py +1 -0
  31. amulet/game/_universal/_biome.py +17 -0
  32. amulet/game/_universal/_block.py +47 -0
  33. amulet/game/_universal/_version.py +68 -0
  34. amulet/game/abc/__init__.py +22 -0
  35. amulet/game/abc/_block_specification.py +150 -0
  36. amulet/game/abc/biome.py +213 -0
  37. amulet/game/abc/block.py +331 -0
  38. amulet/game/abc/game_version_container.py +25 -0
  39. amulet/game/abc/json_interface.py +27 -0
  40. amulet/game/abc/version.py +44 -0
  41. amulet/game/bedrock/__init__.py +1 -0
  42. amulet/game/bedrock/_biome.py +35 -0
  43. amulet/game/bedrock/_block.py +42 -0
  44. amulet/game/bedrock/_version.py +165 -0
  45. amulet/game/java/__init__.py +2 -0
  46. amulet/game/java/_biome.py +35 -0
  47. amulet/game/java/_block.py +60 -0
  48. amulet/game/java/_version.py +176 -0
  49. amulet/game/translate/__init__.py +12 -0
  50. amulet/game/translate/_functions/__init__.py +15 -0
  51. amulet/game/translate/_functions/_code_functions/__init__.py +0 -0
  52. amulet/game/translate/_functions/_code_functions/_text.py +553 -0
  53. amulet/game/translate/_functions/_code_functions/banner_pattern.py +67 -0
  54. amulet/game/translate/_functions/_code_functions/bedrock_chest_connection.py +152 -0
  55. amulet/game/translate/_functions/_code_functions/bedrock_moving_block_pos.py +88 -0
  56. amulet/game/translate/_functions/_code_functions/bedrock_sign.py +152 -0
  57. amulet/game/translate/_functions/_code_functions/bedrock_skull_rotation.py +16 -0
  58. amulet/game/translate/_functions/_code_functions/custom_name.py +146 -0
  59. amulet/game/translate/_functions/_frozen.py +66 -0
  60. amulet/game/translate/_functions/_state.py +54 -0
  61. amulet/game/translate/_functions/_typing.py +98 -0
  62. amulet/game/translate/_functions/abc.py +116 -0
  63. amulet/game/translate/_functions/carry_nbt.py +160 -0
  64. amulet/game/translate/_functions/carry_properties.py +80 -0
  65. amulet/game/translate/_functions/code.py +143 -0
  66. amulet/game/translate/_functions/map_block_name.py +66 -0
  67. amulet/game/translate/_functions/map_nbt.py +111 -0
  68. amulet/game/translate/_functions/map_properties.py +93 -0
  69. amulet/game/translate/_functions/multiblock.py +112 -0
  70. amulet/game/translate/_functions/new_block.py +42 -0
  71. amulet/game/translate/_functions/new_entity.py +43 -0
  72. amulet/game/translate/_functions/new_nbt.py +206 -0
  73. amulet/game/translate/_functions/new_properties.py +64 -0
  74. amulet/game/translate/_functions/sequence.py +51 -0
  75. amulet/game/translate/_functions/walk_input_nbt.py +331 -0
  76. amulet/game/translate/_translator.py +433 -0
  77. amulet/item.py +75 -0
  78. amulet/level/__init__.pyi +27 -0
  79. amulet/level/_load.py +100 -0
  80. amulet/level/abc/__init__.py +12 -0
  81. amulet/level/abc/_chunk_handle.py +335 -0
  82. amulet/level/abc/_dimension.py +86 -0
  83. amulet/level/abc/_history/__init__.py +1 -0
  84. amulet/level/abc/_history/_cache.py +224 -0
  85. amulet/level/abc/_history/_history_manager.py +291 -0
  86. amulet/level/abc/_level/__init__.py +5 -0
  87. amulet/level/abc/_level/_compactable_level.py +10 -0
  88. amulet/level/abc/_level/_creatable_level.py +29 -0
  89. amulet/level/abc/_level/_disk_level.py +17 -0
  90. amulet/level/abc/_level/_level.py +453 -0
  91. amulet/level/abc/_level/_loadable_level.py +42 -0
  92. amulet/level/abc/_player_storage.py +7 -0
  93. amulet/level/abc/_raw_level.py +187 -0
  94. amulet/level/abc/_registry.py +40 -0
  95. amulet/level/bedrock/__init__.py +2 -0
  96. amulet/level/bedrock/_chunk_handle.py +19 -0
  97. amulet/level/bedrock/_dimension.py +22 -0
  98. amulet/level/bedrock/_level.py +187 -0
  99. amulet/level/bedrock/_raw/__init__.py +5 -0
  100. amulet/level/bedrock/_raw/_actor_counter.py +53 -0
  101. amulet/level/bedrock/_raw/_chunk.py +54 -0
  102. amulet/level/bedrock/_raw/_chunk_decode.py +668 -0
  103. amulet/level/bedrock/_raw/_chunk_encode.py +602 -0
  104. amulet/level/bedrock/_raw/_constant.py +9 -0
  105. amulet/level/bedrock/_raw/_dimension.py +343 -0
  106. amulet/level/bedrock/_raw/_level.py +463 -0
  107. amulet/level/bedrock/_raw/_level_dat.py +90 -0
  108. amulet/level/bedrock/_raw/_typing.py +6 -0
  109. amulet/level/bedrock/_raw/leveldb_chunk_versions.py +83 -0
  110. amulet/level/bedrock/chunk/__init__.py +1 -0
  111. amulet/level/bedrock/chunk/_chunk.py +126 -0
  112. amulet/level/bedrock/chunk/components/__init__.py +0 -0
  113. amulet/level/bedrock/chunk/components/chunk_version.py +12 -0
  114. amulet/level/bedrock/chunk/components/finalised_state.py +13 -0
  115. amulet/level/bedrock/chunk/components/raw_chunk.py +15 -0
  116. amulet/level/construction/__init__.py +0 -0
  117. amulet/level/java/__init__.pyi +21 -0
  118. amulet/level/java/_chunk_handle.py +17 -0
  119. amulet/level/java/_chunk_handle.pyi +15 -0
  120. amulet/level/java/_dimension.py +20 -0
  121. amulet/level/java/_dimension.pyi +13 -0
  122. amulet/level/java/_level.py +184 -0
  123. amulet/level/java/_level.pyi +120 -0
  124. amulet/level/java/_raw/__init__.pyi +19 -0
  125. amulet/level/java/_raw/_chunk.pyi +23 -0
  126. amulet/level/java/_raw/_chunk_decode.py +561 -0
  127. amulet/level/java/_raw/_chunk_encode.py +463 -0
  128. amulet/level/java/_raw/_constant.py +9 -0
  129. amulet/level/java/_raw/_constant.pyi +20 -0
  130. amulet/level/java/_raw/_data_pack/__init__.py +2 -0
  131. amulet/level/java/_raw/_data_pack/__init__.pyi +8 -0
  132. amulet/level/java/_raw/_data_pack/data_pack.py +241 -0
  133. amulet/level/java/_raw/_data_pack/data_pack.pyi +197 -0
  134. amulet/level/java/_raw/_data_pack/data_pack_manager.py +77 -0
  135. amulet/level/java/_raw/_data_pack/data_pack_manager.pyi +75 -0
  136. amulet/level/java/_raw/_dimension.py +86 -0
  137. amulet/level/java/_raw/_dimension.pyi +72 -0
  138. amulet/level/java/_raw/_level.py +507 -0
  139. amulet/level/java/_raw/_level.pyi +238 -0
  140. amulet/level/java/_raw/_typing.py +3 -0
  141. amulet/level/java/_raw/_typing.pyi +5 -0
  142. amulet/level/java/anvil/__init__.py +2 -0
  143. amulet/level/java/anvil/__init__.pyi +11 -0
  144. amulet/level/java/anvil/_dimension.py +170 -0
  145. amulet/level/java/anvil/_dimension.pyi +109 -0
  146. amulet/level/java/anvil/_region.py +421 -0
  147. amulet/level/java/anvil/_region.pyi +197 -0
  148. amulet/level/java/anvil/_sector_manager.py +223 -0
  149. amulet/level/java/anvil/_sector_manager.pyi +142 -0
  150. amulet/level/java/chunk.pyi +81 -0
  151. amulet/level/java/chunk_/_chunk.py +260 -0
  152. amulet/level/java/chunk_/components/inhabited_time.py +12 -0
  153. amulet/level/java/chunk_/components/last_update.py +12 -0
  154. amulet/level/java/chunk_/components/legacy_version.py +12 -0
  155. amulet/level/java/chunk_/components/light_populated.py +12 -0
  156. amulet/level/java/chunk_/components/named_height_2d.py +37 -0
  157. amulet/level/java/chunk_/components/status.py +11 -0
  158. amulet/level/java/chunk_/components/terrain_populated.py +12 -0
  159. amulet/level/java/chunk_components.pyi +22 -0
  160. amulet/level/java/long_array.pyi +38 -0
  161. amulet/level/java_forge/__init__.py +0 -0
  162. amulet/level/mcstructure/__init__.py +0 -0
  163. amulet/level/nbt/__init__.py +0 -0
  164. amulet/level/schematic/__init__.py +0 -0
  165. amulet/level/sponge_schematic/__init__.py +0 -0
  166. amulet/level/temporary_level/__init__.py +1 -0
  167. amulet/level/temporary_level/_level.py +16 -0
  168. amulet/palette/__init__.pyi +8 -0
  169. amulet/palette/biome_palette.pyi +45 -0
  170. amulet/palette/block_palette.pyi +45 -0
  171. amulet/player.py +64 -0
  172. amulet/py.typed +0 -0
  173. amulet/selection/__init__.py +2 -0
  174. amulet/selection/abstract_selection.py +342 -0
  175. amulet/selection/box.py +852 -0
  176. amulet/selection/group.py +481 -0
  177. amulet/utils/__init__.pyi +28 -0
  178. amulet/utils/call_spec/__init__.py +24 -0
  179. amulet/utils/call_spec/__init__.pyi +53 -0
  180. amulet/utils/call_spec/_call_spec.py +262 -0
  181. amulet/utils/call_spec/_call_spec.pyi +272 -0
  182. amulet/utils/format_utils.py +41 -0
  183. amulet/utils/generator.py +18 -0
  184. amulet/utils/matrix.py +243 -0
  185. amulet/utils/matrix.pyi +177 -0
  186. amulet/utils/numpy.pyi +11 -0
  187. amulet/utils/numpy_helpers.py +19 -0
  188. amulet/utils/shareable_lock.py +335 -0
  189. amulet/utils/shareable_lock.pyi +190 -0
  190. amulet/utils/signal/__init__.py +10 -0
  191. amulet/utils/signal/__init__.pyi +25 -0
  192. amulet/utils/signal/_signal.py +228 -0
  193. amulet/utils/signal/_signal.pyi +84 -0
  194. amulet/utils/task_manager.py +235 -0
  195. amulet/utils/task_manager.pyi +168 -0
  196. amulet/utils/typed_property.py +111 -0
  197. amulet/utils/typing.py +4 -0
  198. amulet/utils/typing.pyi +6 -0
  199. amulet/utils/weakref.py +70 -0
  200. amulet/utils/weakref.pyi +50 -0
  201. amulet/utils/world_utils.py +102 -0
  202. amulet/utils/world_utils.pyi +109 -0
  203. amulet/version.cpp +136 -0
  204. amulet/version.hpp +142 -0
  205. amulet/version.pyi +94 -0
  206. amulet_core-2.0a5.dist-info/METADATA +103 -0
  207. amulet_core-2.0a5.dist-info/RECORD +210 -0
  208. amulet_core-2.0a5.dist-info/WHEEL +5 -0
  209. amulet_core-2.0a5.dist-info/entry_points.txt +2 -0
  210. amulet_core-2.0a5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,421 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import struct
5
+ import zlib
6
+ import gzip
7
+ from typing import BinaryIO
8
+ from collections.abc import Iterator
9
+ import numpy
10
+ import time
11
+ import re
12
+ import threading
13
+ import logging
14
+ from enum import IntEnum
15
+
16
+ import lz4.block as lz4_block # type: ignore
17
+ from amulet_nbt import NamedTag, read_nbt
18
+
19
+ from amulet.errors import ChunkDoesNotExist, ChunkLoadError
20
+ from amulet.data_types import ChunkCoordinates
21
+ from ._sector_manager import SectorManager, Sector
22
+
23
+ SectorSize = 0x1000
24
+ MaxRegionSize = 255 * SectorSize # the maximum size data in the region file can be
25
+ HeaderSector = Sector(0, 0x2000)
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+
30
+ class RegionFileVersion(IntEnum):
31
+ VERSION_GZIP = 1
32
+ VERSION_DEFLATE = 2
33
+ VERSION_NONE = 3
34
+ VERSION_LZ4 = 4
35
+
36
+
37
+ LZ4_HEADER = struct.Struct("<8sBiii")
38
+ LZ4_MAGIC = b"LZ4Block"
39
+ COMPRESSION_METHOD_RAW = 0x10
40
+ COMPRESSION_METHOD_LZ4 = 0x20
41
+
42
+
43
+ def _decompress_lz4(data: bytes) -> bytes:
44
+ """The LZ4 compression format is a sequence of LZ4 blocks with some header data."""
45
+ # https://github.com/lz4/lz4-java/blob/7c931bef32d179ec3d3286ee71638b23ebde3459/src/java/net/jpountz/lz4/LZ4BlockInputStream.java#L200
46
+ decompressed: list[bytes] = []
47
+ index = 0
48
+ while index < len(data):
49
+ magic, token, compressed_length, original_length, checksum = LZ4_HEADER.unpack(
50
+ data[index : index + LZ4_HEADER.size]
51
+ )
52
+ index += LZ4_HEADER.size
53
+ compression_method = token & 0xF0
54
+ if (
55
+ magic != LZ4_MAGIC
56
+ or original_length < 0
57
+ or compressed_length < 0
58
+ or (original_length == 0 and compressed_length != 0)
59
+ or (original_length != 0 and compressed_length == 0)
60
+ or (
61
+ compression_method == COMPRESSION_METHOD_RAW
62
+ and original_length != compressed_length
63
+ )
64
+ ):
65
+ raise ValueError("LZ4 compressed block is corrupted.")
66
+ if compression_method == COMPRESSION_METHOD_RAW:
67
+ decompressed.append(data[index : index + original_length])
68
+ index += original_length
69
+ elif compression_method == COMPRESSION_METHOD_LZ4:
70
+ decompressed.append(
71
+ lz4_block.decompress(
72
+ data[index : index + compressed_length], original_length
73
+ )
74
+ )
75
+ index += compressed_length
76
+ else:
77
+ raise ValueError("LZ4 compressed block is corrupted.")
78
+ return b"".join(decompressed)
79
+
80
+
81
+ def _compress(tag: NamedTag) -> bytes:
82
+ """Convert an NBTFile into a compressed bytes object"""
83
+ data = tag.save_to(compressed=False)
84
+ return b"\x02" + zlib.compress(data)
85
+
86
+
87
+ def _decompress(data: bytes) -> NamedTag:
88
+ """Convert a bytes object into an NBTFile"""
89
+ compress_type, data = data[0], data[1:]
90
+ if compress_type == RegionFileVersion.VERSION_GZIP:
91
+ return read_nbt(gzip.decompress(data), compressed=False)
92
+ elif compress_type == RegionFileVersion.VERSION_DEFLATE:
93
+ return read_nbt(zlib.decompress(data), compressed=False)
94
+ elif compress_type == RegionFileVersion.VERSION_NONE:
95
+ return read_nbt(data, compressed=False)
96
+ elif compress_type == RegionFileVersion.VERSION_LZ4:
97
+ return read_nbt(_decompress_lz4(data), compressed=False)
98
+ raise ChunkLoadError(f"Invalid compression type {compress_type}")
99
+
100
+
101
+ def _sanitise_file(handler: BinaryIO) -> None:
102
+ handler.seek(0, os.SEEK_END)
103
+ file_size = handler.tell()
104
+ if file_size & 0xFFF:
105
+ # ensure the file is a multiple of 4096 bytes
106
+ file_size = (file_size | 0xFFF) + 1
107
+ handler.truncate(file_size)
108
+
109
+ # if the length of the region file is less than 8KiB extend it to 8KiB
110
+ if file_size < SectorSize * 2:
111
+ file_size = SectorSize * 2
112
+ handler.truncate(file_size)
113
+
114
+
115
+ class AnvilRegion:
116
+ """
117
+ A class to read and write Minecraft Java Edition Region files.
118
+ Only one class should exist per region file at any given time otherwise bad things may happen.
119
+ """
120
+
121
+ region_regex = re.compile(r"r\.(?P<rx>-?\d+)\.(?P<rz>-?\d+)\.mca")
122
+
123
+ __slots__ = (
124
+ "_path",
125
+ "_rx",
126
+ "_rz",
127
+ "_mcc",
128
+ "_sector_manager",
129
+ "_chunk_locations",
130
+ "_lock",
131
+ )
132
+
133
+ # The path to the region file
134
+ _path: str
135
+
136
+ # The region coordinates
137
+ _rx: int
138
+ _rz: int
139
+
140
+ # Is support for .mcc files enabled
141
+ _mcc: bool
142
+
143
+ # A class to track which sectors are reserved
144
+ _sector_manager: SectorManager | None
145
+
146
+ # A dictionary mapping the chunk coordinate to the location on disk
147
+ _chunk_locations: dict[ChunkCoordinates, Sector]
148
+
149
+ # A lock to limit access to multiple threads
150
+ _lock: threading.RLock
151
+
152
+ @classmethod
153
+ def get_coords(cls, file_path: str) -> tuple[int, int]:
154
+ """Parse a region file path to get the region coordinates."""
155
+ file_path = os.path.basename(file_path)
156
+ match = cls.region_regex.fullmatch(file_path)
157
+ if match is None:
158
+ raise ValueError(f"{file_path} is not a valid region file path.")
159
+ return int(match.group("rx")), int(match.group("rz"))
160
+
161
+ def __init__(self, file_path: str, *, mcc: bool = False) -> None:
162
+ """
163
+ A class wrapper for a region file
164
+ :param file_path: The file path of the region file
165
+ :param create: bool - if true will create the region from scratch. If false will try loading from disk
166
+ """
167
+ self._path = file_path
168
+ self._rx, self._rz = self.get_coords(file_path)
169
+ self._mcc = mcc # create mcc file if the chunk is greater than 1MiB
170
+ self._sector_manager = None
171
+ self._chunk_locations = {}
172
+ self._lock = threading.RLock()
173
+
174
+ @property
175
+ def path(self) -> str:
176
+ """The file path to the region file."""
177
+ return self._path
178
+
179
+ @property
180
+ def rx(self) -> int:
181
+ """The region x coordinate."""
182
+ return self._rx
183
+
184
+ @property
185
+ def rz(self) -> int:
186
+ """The region z coordinate."""
187
+ return self._rz
188
+
189
+ def get_mcc_path(self, cx: int, cz: int) -> str:
190
+ """Get the mcc path. Coordinates are world chunk coordinates."""
191
+ return os.path.join(
192
+ os.path.dirname(self._path),
193
+ f"c.{cx}.{cz}.mcc",
194
+ )
195
+
196
+ def _load(self) -> None:
197
+ """Load region metadata. The lock must be acquired when calling this."""
198
+ if self._sector_manager is not None:
199
+ return
200
+
201
+ # Create the sector manager and ensure the header is not reservable
202
+ self._sector_manager = SectorManager(0, 0x2000)
203
+ self._sector_manager.reserve(HeaderSector)
204
+
205
+ if os.path.isfile(self._path):
206
+ # Load the file and populate the sector manager
207
+ with open(self._path, "rb+") as handler:
208
+ _sanitise_file(handler)
209
+ handler.seek(0)
210
+ location_table = numpy.fromfile(
211
+ handler, dtype=">u4", count=1024
212
+ ).reshape(32, 32)
213
+ for (cz, cx), sector_data in numpy.ndenumerate(location_table):
214
+ if sector_data:
215
+ sector_offset = (sector_data >> 8) * 0x1000
216
+ sector_size = (sector_data & 0xFF) * 0x1000
217
+ sector = Sector(sector_offset, sector_offset + sector_size)
218
+ self._sector_manager.reserve(sector)
219
+ self._chunk_locations[
220
+ (cx + self.rx * 32, cz + self.rz * 32)
221
+ ] = sector
222
+
223
+ def all_coords(self) -> Iterator[ChunkCoordinates]:
224
+ """An iterable of chunk coordinates in world space."""
225
+ with self._lock:
226
+ self._load()
227
+ coords = list(self._chunk_locations)
228
+ yield from coords
229
+
230
+ def has_data(self, cx: int, cz: int) -> bool:
231
+ """Does the chunk exists. Coords are in world space."""
232
+ with self._lock:
233
+ self._load()
234
+ return (cx, cz) in self._chunk_locations
235
+
236
+ def get_data(self, cx: int, cz: int) -> NamedTag:
237
+ with self._lock:
238
+ self._load()
239
+ sector = self._chunk_locations.get((cx, cz))
240
+ if sector is None:
241
+ raise ChunkDoesNotExist
242
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
243
+ with open(self._path, "rb+") as handler:
244
+ _sanitise_file(handler)
245
+ handler.seek(0, os.SEEK_END)
246
+ if handler.tell() < sector.stop:
247
+ # if the sector is beyond the end of the file
248
+ raise ChunkDoesNotExist
249
+
250
+ handler.seek(sector.start)
251
+ buffer_size_bytes: bytes = handler.read(4)
252
+ buffer_size = struct.unpack(">I", buffer_size_bytes)[0]
253
+ buffer: bytes = handler.read(buffer_size)
254
+
255
+ if buffer:
256
+ if buffer[0] & 128: # if the "external" bit is set
257
+ if self._mcc:
258
+ mcc_path = self.get_mcc_path(cx, cz)
259
+ if os.path.isfile(mcc_path):
260
+ with open(mcc_path, "rb") as f:
261
+ return _decompress(
262
+ bytes([buffer[0] & 127]) + f.read()
263
+ )
264
+ else:
265
+ return _decompress(buffer)
266
+ raise ChunkDoesNotExist
267
+
268
+ def _write_data(self, cx: int, cz: int, data: bytes | None) -> None:
269
+ assert (
270
+ self.rx * 32 <= cx < (self.rx + 1) * 32
271
+ and self.rz * 32 <= cz < (self.rz + 1) * 32
272
+ )
273
+ if isinstance(data, bytes) and len(data) + 4 > MaxRegionSize and not self._mcc:
274
+ # if the data is too large and mcc files are not supported then do nothing
275
+ log.error(
276
+ f"Could not save data {cx},{cz} in region file {self._path} because it was too large."
277
+ )
278
+ return
279
+
280
+ with self._lock:
281
+ self._load()
282
+ sector_manager = self._sector_manager
283
+ assert sector_manager is not None
284
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
285
+ handler: BinaryIO
286
+ with open(
287
+ self._path, "rb+" if os.path.isfile(self._path) else "wb+"
288
+ ) as handler:
289
+ _sanitise_file(handler)
290
+
291
+ old_sector = self._chunk_locations.pop((cx, cz), None)
292
+ if old_sector is not None:
293
+ # the chunk used to exist
294
+ handler.seek(old_sector.start + 4)
295
+ if self._mcc and handler.read(1)[0] & 127:
296
+ # if the file is stored externally delete the file
297
+ mcc_path = self.get_mcc_path(cx, cz)
298
+ if os.path.isfile(mcc_path):
299
+ os.remove(mcc_path)
300
+ sector_manager.free(old_sector)
301
+
302
+ location = b"\x00\x00\x00\x00"
303
+
304
+ if isinstance(data, bytes):
305
+ # find a memory location large enough to fit the data
306
+ if len(data) + 4 > MaxRegionSize:
307
+ # save externally (if mcc files are not supported the check at the top will filter large files out)
308
+ with open(self.get_mcc_path(cx, cz), "wb") as mcc:
309
+ mcc.write(data[1:])
310
+ data = bytes([data[0] | 128])
311
+ data = struct.pack(">I", len(data)) + data
312
+ sector_length = len(data)
313
+ if sector_length & 0xFFF:
314
+ sector_length = (sector_length | 0xFFF) + 1
315
+ sector = sector_manager.reserve_space(sector_length)
316
+ assert sector.start & 0xFFF == 0
317
+ self._chunk_locations[(cx, cz)] = sector
318
+ location = struct.pack(
319
+ ">I", (sector.start >> 4) + (sector_length >> 12)
320
+ )
321
+ handler.seek(sector.start)
322
+ handler.write(data)
323
+ _sanitise_file(handler)
324
+
325
+ # write the header data
326
+ handler.seek(4 * (cx - self.rx * 32 + (cz - self.rz * 32) * 32))
327
+ handler.write(location)
328
+ handler.seek(SectorSize - 4, os.SEEK_CUR)
329
+ handler.write(struct.pack(">I", int(time.time())))
330
+
331
+ def set_data(self, cx: int, cz: int, data: NamedTag) -> None:
332
+ """Write the data to the region file."""
333
+ bytes_data = _compress(data)
334
+ self._write_data(cx, cz, bytes_data)
335
+
336
+ def delete_data(self, cx: int, cz: int) -> None:
337
+ """Delete the data from the region file."""
338
+ self._write_data(cx, cz, None)
339
+
340
+ def compact(self) -> None:
341
+ """Compact the region file.
342
+ This moves all entries to the front of the file and deletes any unused space."""
343
+ with self._lock:
344
+ if not os.path.isfile(self._path):
345
+ # Do nothing if there is no file.
346
+ return
347
+
348
+ # All chunks in the region must be valid at all times.
349
+ # Load metadata
350
+ self._load()
351
+ sector_manager = self._sector_manager
352
+ assert sector_manager is not None
353
+
354
+ # Generate a list of sectors in sequential order
355
+ # location header index, chunk coordinate, sector
356
+ chunk_sectors: list[tuple[int, tuple[int, int], Sector]] = [
357
+ (4 * (cx - self.rx * 32 + (cz - self.rz * 32) * 32), (cx, cz), sector)
358
+ for (cx, cz), sector in sorted(
359
+ self._chunk_locations.items(), key=lambda item: item[1].start
360
+ )
361
+ ]
362
+
363
+ # Set the position to the end of the header
364
+ file_position = HeaderSector.stop
365
+ # The end of the last chunk or the end of the header if no chunks exist.
366
+ if chunk_sectors:
367
+ file_end = chunk_sectors[-1][2].stop
368
+ else:
369
+ file_end = HeaderSector.stop
370
+
371
+ with open(self._path, "rb+") as handler:
372
+ while chunk_sectors:
373
+ # While there are remaining sectors
374
+ # Get the first sector
375
+ header_index, chunk_coordinate, sector = chunk_sectors.pop(0)
376
+
377
+ if file_position == sector.start:
378
+ # There isn't any space before the sector. Do nothing.
379
+ file_position = sector.stop
380
+ else:
381
+ # There is space before the sector
382
+ if file_position + sector.length <= sector.start:
383
+ # There is enough space before the sector to fit the whole sector.
384
+ # Copy it to the new location
385
+ new_sector = Sector(
386
+ file_position, file_position + sector.length
387
+ )
388
+ file_position = new_sector.stop
389
+ else:
390
+ # There is space before the sector but not enough to fit the sector.
391
+ # Move it to the end for processing later.
392
+ new_sector = Sector(file_end, file_end + sector.length)
393
+ file_end = new_sector.stop
394
+ chunk_sectors.append(
395
+ (header_index, chunk_coordinate, new_sector)
396
+ )
397
+
398
+ # Read in the data
399
+ handler.seek(sector.start)
400
+ data = handler.read(sector.length)
401
+
402
+ # Reserve and write the data to the new sector
403
+ sector_manager.reserve(new_sector)
404
+ handler.seek(new_sector.start)
405
+ handler.write(data)
406
+
407
+ # Update the index
408
+ handler.seek(header_index)
409
+ handler.write(
410
+ struct.pack(
411
+ ">I",
412
+ (new_sector.start >> 4) + (new_sector.length >> 12),
413
+ )
414
+ )
415
+ self._chunk_locations[chunk_coordinate] = new_sector
416
+
417
+ # Free the old sector
418
+ sector_manager.free(sector)
419
+
420
+ # Delete any unused data at the end.
421
+ handler.truncate(file_position)
@@ -0,0 +1,197 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import gzip as gzip
5
+ import logging as logging
6
+ import os as os
7
+ import re as re
8
+ import struct as struct
9
+ import threading as threading
10
+ import time as time
11
+ import types
12
+ import typing
13
+ import zlib as zlib
14
+ from collections.abc import Iterator
15
+ from enum import IntEnum
16
+ from typing import BinaryIO
17
+
18
+ import _struct
19
+ import amulet.level.java.anvil._sector_manager
20
+ import numpy as numpy
21
+ from amulet.errors import ChunkDoesNotExist, ChunkLoadError
22
+ from amulet.level.java.anvil._sector_manager import Sector, SectorManager
23
+ from amulet_nbt import NamedTag, read_nbt
24
+ from lz4 import block as lz4_block
25
+
26
+ __all__ = [
27
+ "AnvilRegion",
28
+ "BinaryIO",
29
+ "COMPRESSION_METHOD_LZ4",
30
+ "COMPRESSION_METHOD_RAW",
31
+ "ChunkCoordinates",
32
+ "ChunkDoesNotExist",
33
+ "ChunkLoadError",
34
+ "HeaderSector",
35
+ "IntEnum",
36
+ "Iterator",
37
+ "LZ4_HEADER",
38
+ "LZ4_MAGIC",
39
+ "MaxRegionSize",
40
+ "NamedTag",
41
+ "RegionFileVersion",
42
+ "Sector",
43
+ "SectorManager",
44
+ "SectorSize",
45
+ "gzip",
46
+ "log",
47
+ "logging",
48
+ "lz4_block",
49
+ "numpy",
50
+ "os",
51
+ "re",
52
+ "read_nbt",
53
+ "struct",
54
+ "threading",
55
+ "time",
56
+ "zlib",
57
+ ]
58
+
59
+ class AnvilRegion:
60
+ """
61
+
62
+ A class to read and write Minecraft Java Edition Region files.
63
+ Only one class should exist per region file at any given time otherwise bad things may happen.
64
+
65
+ """
66
+
67
+ __slots__: typing.ClassVar[tuple] = (
68
+ "_path",
69
+ "_rx",
70
+ "_rz",
71
+ "_mcc",
72
+ "_sector_manager",
73
+ "_chunk_locations",
74
+ "_lock",
75
+ )
76
+ region_regex: typing.ClassVar[
77
+ re.Pattern
78
+ ] # value = re.compile('r\\.(?P<rx>-?\\d+)\\.(?P<rz>-?\\d+)\\.mca')
79
+ @classmethod
80
+ def get_coords(cls, file_path: str) -> tuple[int, int]:
81
+ """
82
+ Parse a region file path to get the region coordinates.
83
+ """
84
+
85
+ def __init__(self, file_path: str, *, mcc: bool = False) -> None:
86
+ """
87
+
88
+ A class wrapper for a region file
89
+ :param file_path: The file path of the region file
90
+ :param create: bool - if true will create the region from scratch. If false will try loading from disk
91
+
92
+ """
93
+
94
+ def _load(self) -> None:
95
+ """
96
+ Load region metadata. The lock must be acquired when calling this.
97
+ """
98
+
99
+ def _write_data(self, cx: int, cz: int, data: bytes | None) -> None: ...
100
+ def all_coords(self) -> typing.Iterator[ChunkCoordinates]:
101
+ """
102
+ An iterable of chunk coordinates in world space.
103
+ """
104
+
105
+ def compact(self) -> None:
106
+ """
107
+ Compact the region file.
108
+ This moves all entries to the front of the file and deletes any unused space.
109
+ """
110
+
111
+ def delete_data(self, cx: int, cz: int) -> None:
112
+ """
113
+ Delete the data from the region file.
114
+ """
115
+
116
+ def get_data(self, cx: int, cz: int) -> NamedTag: ...
117
+ def get_mcc_path(self, cx: int, cz: int) -> str:
118
+ """
119
+ Get the mcc path. Coordinates are world chunk coordinates.
120
+ """
121
+
122
+ def has_data(self, cx: int, cz: int) -> bool:
123
+ """
124
+ Does the chunk exists. Coords are in world space.
125
+ """
126
+
127
+ def set_data(self, cx: int, cz: int, data: NamedTag) -> None:
128
+ """
129
+ Write the data to the region file.
130
+ """
131
+
132
+ @property
133
+ def path(self) -> str:
134
+ """
135
+ The file path to the region file.
136
+ """
137
+
138
+ @property
139
+ def rx(self) -> int:
140
+ """
141
+ The region x coordinate.
142
+ """
143
+
144
+ @property
145
+ def rz(self) -> int:
146
+ """
147
+ The region z coordinate.
148
+ """
149
+
150
+ class RegionFileVersion(enum.IntEnum):
151
+ VERSION_DEFLATE: typing.ClassVar[
152
+ RegionFileVersion
153
+ ] # value = <RegionFileVersion.VERSION_DEFLATE: 2>
154
+ VERSION_GZIP: typing.ClassVar[
155
+ RegionFileVersion
156
+ ] # value = <RegionFileVersion.VERSION_GZIP: 1>
157
+ VERSION_LZ4: typing.ClassVar[
158
+ RegionFileVersion
159
+ ] # value = <RegionFileVersion.VERSION_LZ4: 4>
160
+ VERSION_NONE: typing.ClassVar[
161
+ RegionFileVersion
162
+ ] # value = <RegionFileVersion.VERSION_NONE: 3>
163
+ @classmethod
164
+ def __new__(cls, value): ...
165
+ def __format__(self, format_spec):
166
+ """
167
+ Convert to a string according to format_spec.
168
+ """
169
+
170
+ def _compress(tag: NamedTag) -> bytes:
171
+ """
172
+ Convert an NBTFile into a compressed bytes object
173
+ """
174
+
175
+ def _decompress(data: bytes) -> NamedTag:
176
+ """
177
+ Convert a bytes object into an NBTFile
178
+ """
179
+
180
+ def _decompress_lz4(data: bytes) -> bytes:
181
+ """
182
+ The LZ4 compression format is a sequence of LZ4 blocks with some header data.
183
+ """
184
+
185
+ def _sanitise_file(handler: BinaryIO) -> None: ...
186
+
187
+ COMPRESSION_METHOD_LZ4: int = 32
188
+ COMPRESSION_METHOD_RAW: int = 16
189
+ ChunkCoordinates: types.GenericAlias # value = tuple[int, int]
190
+ HeaderSector: (
191
+ amulet.level.java.anvil._sector_manager.Sector
192
+ ) # value = Sector(start=0, stop=8192)
193
+ LZ4_HEADER: _struct.Struct # value = <_struct.Struct object>
194
+ LZ4_MAGIC: bytes # value = b'LZ4Block'
195
+ MaxRegionSize: int = 1044480
196
+ SectorSize: int = 4096
197
+ log: logging.Logger # value = <Logger amulet.level.java.anvil._region (INFO)>