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,384 +1,384 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import struct
5
- import warnings
6
- import zlib
7
- import gzip
8
- from typing import Tuple, Dict, Union, Optional, BinaryIO, Generator
9
- import numpy
10
- import time
11
- import re
12
- import threading
13
- import logging
14
- from enum import IntEnum
15
-
16
- from amulet_nbt import NamedTag, load as load_nbt
17
-
18
- from amulet.api.errors import ChunkDoesNotExist, ChunkLoadError
19
- from amulet.api.data_types import (
20
- ChunkCoordinates,
21
- )
22
- from ._sector_manager import SectorManager, Sector
23
-
24
- Depreciated = object()
25
-
26
- SectorSize = 0x1000
27
- MaxRegionSize = 255 * SectorSize # the maximum size data in the region file can be
28
-
29
- log = logging.getLogger(__name__)
30
-
31
-
32
- class RegionFileVersion(IntEnum):
33
- VERSION_GZIP = 1
34
- VERSION_DEFLATE = 2
35
- VERSION_NONE = 3
36
-
37
-
38
- def _validate_region_coords(cx: int, cz: int):
39
- """Make sure that the coordinates are in region space."""
40
- if not (0 <= cx <= 32 and 0 <= cz <= 32):
41
- raise ValueError("coordinates must be in region space")
42
-
43
-
44
- def _compress(data: NamedTag) -> bytes:
45
- """Convert an NBTFile into a compressed bytes object"""
46
- data = data.save_to(compressed=False)
47
- return b"\x02" + zlib.compress(data)
48
-
49
-
50
- def _decompress(data: bytes) -> NamedTag:
51
- """Convert a bytes object into an NBTFile"""
52
- compress_type, data = data[0], data[1:]
53
- if compress_type == RegionFileVersion.VERSION_GZIP:
54
- return load_nbt(gzip.decompress(data), compressed=False)
55
- elif compress_type == RegionFileVersion.VERSION_DEFLATE:
56
- return load_nbt(zlib.decompress(data), compressed=False)
57
- elif compress_type == RegionFileVersion.VERSION_NONE:
58
- return load_nbt(data, compressed=False)
59
- raise ChunkLoadError(f"Invalid compression type {compress_type}")
60
-
61
-
62
- def _sanitise_file(handler: BinaryIO):
63
- handler.seek(0, os.SEEK_END)
64
- file_size = handler.tell()
65
- if file_size & 0xFFF:
66
- # ensure the file is a multiple of 4096 bytes
67
- file_size = (file_size | 0xFFF) + 1
68
- handler.truncate(file_size)
69
-
70
- # if the length of the region file is less than 8KiB extend it to 8KiB
71
- if file_size < SectorSize * 2:
72
- file_size = SectorSize * 2
73
- handler.truncate(file_size)
74
-
75
-
76
- class AnvilRegionInterface:
77
- """
78
- A class to read and write Minecraft Java Edition Region files.
79
- Only one class should exist per region file at any given time otherwise bad things may happen.
80
- """
81
-
82
- region_regex = re.compile(r"r\.(?P<rx>-?\d+)\.(?P<rz>-?\d+)\.mca")
83
-
84
- __slots__ = (
85
- "_path",
86
- "_rx",
87
- "_rz",
88
- "_mcc",
89
- "_sector_manager",
90
- "_chunk_locations",
91
- "_lock",
92
- )
93
-
94
- # The path to the region file
95
- _path: str
96
-
97
- # The region coordinates
98
- _rx: int
99
- _rz: int
100
-
101
- # Is support for .mcc files enabled
102
- _mcc: bool
103
-
104
- # A class to track which sectors are reserved
105
- _sector_manager: Optional[SectorManager]
106
-
107
- # A dictionary mapping the chunk coordinate to the location on disk
108
- # Key is a Sector if stored in the region file and str to the file path if stored externally
109
- _chunk_locations: Dict[ChunkCoordinates, Sector]
110
-
111
- # A lock to limit access to multiple threads
112
- _lock: threading.RLock
113
-
114
- @staticmethod
115
- def get_coords(file_path: str) -> Union[Tuple[None, None], Tuple[int, int]]:
116
- """Parse a region file path to get the region coordinates."""
117
- file_path = os.path.basename(file_path)
118
- match = AnvilRegion.region_regex.fullmatch(file_path)
119
- if match is None:
120
- return None, None
121
- return int(match.group("rx")), int(match.group("rz"))
122
-
123
- def __init__(self, file_path: str, create=Depreciated, mcc=False):
124
- """
125
- A class wrapper for a region file
126
- :param file_path: The file path of the region file
127
- :param create: bool - if true will create the region from scratch. If false will try loading from disk
128
- """
129
- self._path = file_path
130
- self._rx, self._rz = self.get_coords(file_path)
131
- self._mcc = mcc # create mcc file if the chunk is greater than 1MiB
132
- self._sector_manager = None
133
- self._chunk_locations = {}
134
- self._lock = threading.RLock()
135
-
136
- if create is not Depreciated:
137
- warnings.warn(
138
- "create argument is depreciated. The region will always be created upon writing if it does not exist."
139
- )
140
-
141
- @property
142
- def path(self) -> str:
143
- """The file path to the region file."""
144
- return self._path
145
-
146
- @property
147
- def rx(self) -> int:
148
- """The region x coordinate."""
149
- return self._rx
150
-
151
- @property
152
- def rz(self) -> int:
153
- """The region z coordinate."""
154
- return self._rz
155
-
156
- def get_mcc_path(self, cx: int, cz: int):
157
- """Get the mcc path. Coordinates are global chunk coordinates."""
158
- return os.path.join(
159
- os.path.dirname(self._path),
160
- f"c.{cx + self._rx * 32}.{cz + self._rz * 32}.mcc",
161
- )
162
-
163
- def _load(self):
164
- with self._lock:
165
- if self._sector_manager is not None:
166
- return
167
-
168
- # Create the sector manager and ensure the header is not reservable
169
- self._sector_manager = SectorManager(0, 0x2000)
170
- self._sector_manager.reserve(Sector(0, 0x2000))
171
-
172
- if os.path.isfile(self._path):
173
- # Load the file and populate the sector manager
174
- with open(self._path, "rb+") as handler:
175
- _sanitise_file(handler)
176
- handler.seek(0)
177
- location_table = numpy.fromfile(
178
- handler, dtype=">u4", count=1024
179
- ).reshape(32, 32)
180
- for (z, x), sector_data in numpy.ndenumerate(location_table):
181
- if sector_data:
182
- sector_offset = (sector_data >> 8) * 0x1000
183
- sector_size = (sector_data & 0xFF) * 0x1000
184
- sector = Sector(sector_offset, sector_offset + sector_size)
185
- self._sector_manager.reserve(sector)
186
- self._chunk_locations[(x, z)] = sector
187
-
188
- def all_chunk_coords(self) -> Generator[ChunkCoordinates, None, None]:
189
- """An iterable of chunk coordinates in world space."""
190
- self._load()
191
- for cx, cz in list(self._chunk_locations):
192
- yield cx + self.rx * 32, cz + self.rz * 32
193
-
194
- def has_chunk(self, cx: int, cz: int) -> bool:
195
- """Does the chunk exists. Coords are in region space."""
196
- _validate_region_coords(cx, cz)
197
- self._load()
198
- return (cx, cz) in self._chunk_locations
199
-
200
- def unload(self):
201
- """Unload the data if it is not being used."""
202
- with self._lock:
203
- self._sector_manager = None
204
- self._chunk_locations.clear()
205
-
206
- def get_data(self, cx: int, cz: int) -> NamedTag:
207
- _validate_region_coords(cx, cz)
208
- self._load()
209
- sector = self._chunk_locations.get((cx, cz))
210
- if sector is None:
211
- raise ChunkDoesNotExist
212
- with self._lock:
213
- os.makedirs(os.path.dirname(self._path), exist_ok=True)
214
- with open(self._path, "rb+") as handler:
215
- _sanitise_file(handler)
216
- handler.seek(0, os.SEEK_END)
217
- if handler.tell() < sector.stop:
218
- # if the sector is beyond the end of the file
219
- raise ChunkDoesNotExist
220
-
221
- handler.seek(sector.start)
222
- buffer_size_bytes: bytes = handler.read(4)
223
- buffer_size = struct.unpack(">I", buffer_size_bytes)[0]
224
- buffer: bytes = handler.read(buffer_size)
225
-
226
- if buffer:
227
- if buffer[0] & 128: # if the "external" bit is set
228
- if self._mcc:
229
- mcc_path = self.get_mcc_path(cx, cz)
230
- if os.path.isfile(mcc_path):
231
- with open(mcc_path, "rb") as f:
232
- return _decompress(
233
- bytes([buffer[0] & 127]) + f.read()
234
- )
235
- else:
236
- return _decompress(buffer)
237
- raise ChunkDoesNotExist
238
-
239
- def _write_data(self, cx: int, cz: int, data: Optional[bytes]):
240
- _validate_region_coords(cx, cz)
241
- if isinstance(data, bytes) and len(data) + 4 > MaxRegionSize and not self._mcc:
242
- # if the data is too large and mcc files are not supported then do nothing
243
- log.error(
244
- f"Could not save data {cx},{cz} in region file {self._path} because it was too large."
245
- )
246
- return
247
-
248
- self._load()
249
- with self._lock:
250
- os.makedirs(os.path.dirname(self._path), exist_ok=True)
251
- with open(
252
- self._path, "rb+" if os.path.isfile(self._path) else "wb+"
253
- ) as handler:
254
- handler: BinaryIO
255
- _sanitise_file(handler)
256
-
257
- old_sector = self._chunk_locations.pop((cx, cz), None)
258
- if old_sector is not None:
259
- # the chunk used to exist
260
- handler.seek(old_sector.start + 4)
261
- if self._mcc and handler.read(1)[0] & 127:
262
- # if the file is stored externally delete the file
263
- mcc_path = self.get_mcc_path(cx, cz)
264
- if os.path.isfile(mcc_path):
265
- os.remove(mcc_path)
266
- self._sector_manager.free(old_sector)
267
-
268
- location = b"\x00\x00\x00\x00"
269
-
270
- if isinstance(data, bytes):
271
- # find a memory location large enough to fit the data
272
- if len(data) + 4 > MaxRegionSize:
273
- # save externally (if mcc files are not supported the check at the top will filter large files out)
274
- with open(self.get_mcc_path(cx, cz), "wb") as mcc:
275
- mcc.write(data[1:])
276
- data = bytes([data[0] | 128])
277
- data = struct.pack(">I", len(data)) + data
278
- sector_length = (len(data) | 0xFFF) + 1
279
- sector = self._sector_manager.reserve_space(sector_length)
280
- assert sector.start & 0xFFF == 0
281
- self._chunk_locations[(cx, cz)] = sector
282
- location = struct.pack(
283
- ">I", (sector.start >> 4) + (sector_length >> 12)
284
- )
285
- handler.seek(sector.start)
286
- handler.write(data)
287
- _sanitise_file(handler)
288
-
289
- # write the header data
290
- handler.seek(4 * (cx + cz * 32))
291
- handler.write(location)
292
- handler.seek(SectorSize - 4, os.SEEK_CUR)
293
- handler.write(struct.pack(">I", int(time.time())))
294
-
295
- def write_data(self, cx: int, cz: int, data: NamedTag):
296
- """Write the data to the region file."""
297
- bytes_data = _compress(data)
298
- self._write_data(cx, cz, bytes_data)
299
-
300
- def delete_data(self, cx: int, cz: int):
301
- """Delete the data from the region file."""
302
- self._write_data(cx, cz, None)
303
-
304
-
305
- class BufferedAnvilRegionInterface(AnvilRegionInterface):
306
- """An interface to an anvil region file with a buffer before writing."""
307
-
308
- __slots__ = ("_buffer",)
309
-
310
- _buffer: Dict[ChunkCoordinates, Optional[bytes]]
311
-
312
- def __init__(self, *args, **kwargs):
313
- warnings.warn(
314
- "BufferedAnvilRegionInterface aka AnvilRegion is depreciated and replaced by AnvilRegionInterface",
315
- DeprecationWarning,
316
- )
317
- super().__init__(*args, **kwargs)
318
- self._buffer = {}
319
-
320
- def all_chunk_coords(self) -> Generator[ChunkCoordinates, None, None]:
321
- """An iterable of chunk coordinates in world space."""
322
- self._load()
323
- with self._lock:
324
- chunks = [
325
- (cx + self.rx * 32, cz + self.rz * 32)
326
- for cx, cz in self._chunk_locations
327
- if (cx, cz) not in self._buffer
328
- ] + [
329
- (cx + self.rx * 32, cz + self.rz * 32)
330
- for (cx, cz), value in self._buffer.items()
331
- if value is not None
332
- ]
333
- yield from chunks
334
-
335
- def unload(self):
336
- with self._lock:
337
- super().unload()
338
- self._buffer.clear()
339
-
340
- def has_chunk(self, cx: int, cz: int) -> bool:
341
- """Does the chunk exists. Coords are in region space."""
342
- _validate_region_coords(cx, cz)
343
- with self._lock:
344
- if (cx, cz) in self._buffer:
345
- return self._buffer[(cx, cz)] is not None
346
- else:
347
- self._load()
348
- return (cx, cz) in self._chunk_locations
349
-
350
- def get_chunk_data(self, cx: int, cz: int) -> NamedTag:
351
- """Get chunk data. Coords are in region space."""
352
- _validate_region_coords(cx, cz)
353
- data = self._buffer.get((cx, cz))
354
- if data is not None:
355
- return _decompress(data)
356
- return self.get_data(cx, cz)
357
-
358
- def put_chunk_data(self, cx: int, cz: int, data: NamedTag):
359
- """
360
- Put data to be added to the region file. `save` will push the changes to disk.
361
- Coords are in region space.
362
- """
363
- _validate_region_coords(cx, cz)
364
- with self._lock:
365
- self._buffer[(cx, cz)] = _compress(data)
366
-
367
- def delete_chunk_data(self, cx: int, cz: int):
368
- """
369
- Mark the data for deletion. `save` will push the changes to disk.
370
- Coords are in region space.
371
- """
372
- _validate_region_coords(cx, cz)
373
- with self._lock:
374
- self._buffer[(cx, cz)] = None
375
-
376
- def save(self):
377
- """Write the buffered data to the file and clear the buffer."""
378
- with self._lock:
379
- for (cx, cz), data in self._buffer.items():
380
- self._write_data(cx, cz, data)
381
- self._buffer.clear()
382
-
383
-
384
- AnvilRegion = BufferedAnvilRegionInterface
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import struct
5
+ import warnings
6
+ import zlib
7
+ import gzip
8
+ from typing import Tuple, Dict, Union, Optional, BinaryIO, Generator
9
+ import numpy
10
+ import time
11
+ import re
12
+ import threading
13
+ import logging
14
+ from enum import IntEnum
15
+
16
+ from amulet_nbt import NamedTag, load as load_nbt
17
+
18
+ from amulet.api.errors import ChunkDoesNotExist, ChunkLoadError
19
+ from amulet.api.data_types import (
20
+ ChunkCoordinates,
21
+ )
22
+ from ._sector_manager import SectorManager, Sector
23
+
24
+ Depreciated = object()
25
+
26
+ SectorSize = 0x1000
27
+ MaxRegionSize = 255 * SectorSize # the maximum size data in the region file can be
28
+
29
+ log = logging.getLogger(__name__)
30
+
31
+
32
+ class RegionFileVersion(IntEnum):
33
+ VERSION_GZIP = 1
34
+ VERSION_DEFLATE = 2
35
+ VERSION_NONE = 3
36
+
37
+
38
+ def _validate_region_coords(cx: int, cz: int):
39
+ """Make sure that the coordinates are in region space."""
40
+ if not (0 <= cx <= 32 and 0 <= cz <= 32):
41
+ raise ValueError("coordinates must be in region space")
42
+
43
+
44
+ def _compress(data: NamedTag) -> bytes:
45
+ """Convert an NBTFile into a compressed bytes object"""
46
+ data = data.save_to(compressed=False)
47
+ return b"\x02" + zlib.compress(data)
48
+
49
+
50
+ def _decompress(data: bytes) -> NamedTag:
51
+ """Convert a bytes object into an NBTFile"""
52
+ compress_type, data = data[0], data[1:]
53
+ if compress_type == RegionFileVersion.VERSION_GZIP:
54
+ return load_nbt(gzip.decompress(data), compressed=False)
55
+ elif compress_type == RegionFileVersion.VERSION_DEFLATE:
56
+ return load_nbt(zlib.decompress(data), compressed=False)
57
+ elif compress_type == RegionFileVersion.VERSION_NONE:
58
+ return load_nbt(data, compressed=False)
59
+ raise ChunkLoadError(f"Invalid compression type {compress_type}")
60
+
61
+
62
+ def _sanitise_file(handler: BinaryIO):
63
+ handler.seek(0, os.SEEK_END)
64
+ file_size = handler.tell()
65
+ if file_size & 0xFFF:
66
+ # ensure the file is a multiple of 4096 bytes
67
+ file_size = (file_size | 0xFFF) + 1
68
+ handler.truncate(file_size)
69
+
70
+ # if the length of the region file is less than 8KiB extend it to 8KiB
71
+ if file_size < SectorSize * 2:
72
+ file_size = SectorSize * 2
73
+ handler.truncate(file_size)
74
+
75
+
76
+ class AnvilRegionInterface:
77
+ """
78
+ A class to read and write Minecraft Java Edition Region files.
79
+ Only one class should exist per region file at any given time otherwise bad things may happen.
80
+ """
81
+
82
+ region_regex = re.compile(r"r\.(?P<rx>-?\d+)\.(?P<rz>-?\d+)\.mca")
83
+
84
+ __slots__ = (
85
+ "_path",
86
+ "_rx",
87
+ "_rz",
88
+ "_mcc",
89
+ "_sector_manager",
90
+ "_chunk_locations",
91
+ "_lock",
92
+ )
93
+
94
+ # The path to the region file
95
+ _path: str
96
+
97
+ # The region coordinates
98
+ _rx: int
99
+ _rz: int
100
+
101
+ # Is support for .mcc files enabled
102
+ _mcc: bool
103
+
104
+ # A class to track which sectors are reserved
105
+ _sector_manager: Optional[SectorManager]
106
+
107
+ # A dictionary mapping the chunk coordinate to the location on disk
108
+ # Key is a Sector if stored in the region file and str to the file path if stored externally
109
+ _chunk_locations: Dict[ChunkCoordinates, Sector]
110
+
111
+ # A lock to limit access to multiple threads
112
+ _lock: threading.RLock
113
+
114
+ @staticmethod
115
+ def get_coords(file_path: str) -> Union[Tuple[None, None], Tuple[int, int]]:
116
+ """Parse a region file path to get the region coordinates."""
117
+ file_path = os.path.basename(file_path)
118
+ match = AnvilRegion.region_regex.fullmatch(file_path)
119
+ if match is None:
120
+ return None, None
121
+ return int(match.group("rx")), int(match.group("rz"))
122
+
123
+ def __init__(self, file_path: str, create=Depreciated, mcc=False):
124
+ """
125
+ A class wrapper for a region file
126
+ :param file_path: The file path of the region file
127
+ :param create: bool - if true will create the region from scratch. If false will try loading from disk
128
+ """
129
+ self._path = file_path
130
+ self._rx, self._rz = self.get_coords(file_path)
131
+ self._mcc = mcc # create mcc file if the chunk is greater than 1MiB
132
+ self._sector_manager = None
133
+ self._chunk_locations = {}
134
+ self._lock = threading.RLock()
135
+
136
+ if create is not Depreciated:
137
+ warnings.warn(
138
+ "create argument is depreciated. The region will always be created upon writing if it does not exist."
139
+ )
140
+
141
+ @property
142
+ def path(self) -> str:
143
+ """The file path to the region file."""
144
+ return self._path
145
+
146
+ @property
147
+ def rx(self) -> int:
148
+ """The region x coordinate."""
149
+ return self._rx
150
+
151
+ @property
152
+ def rz(self) -> int:
153
+ """The region z coordinate."""
154
+ return self._rz
155
+
156
+ def get_mcc_path(self, cx: int, cz: int):
157
+ """Get the mcc path. Coordinates are global chunk coordinates."""
158
+ return os.path.join(
159
+ os.path.dirname(self._path),
160
+ f"c.{cx + self._rx * 32}.{cz + self._rz * 32}.mcc",
161
+ )
162
+
163
+ def _load(self):
164
+ with self._lock:
165
+ if self._sector_manager is not None:
166
+ return
167
+
168
+ # Create the sector manager and ensure the header is not reservable
169
+ self._sector_manager = SectorManager(0, 0x2000)
170
+ self._sector_manager.reserve(Sector(0, 0x2000))
171
+
172
+ if os.path.isfile(self._path):
173
+ # Load the file and populate the sector manager
174
+ with open(self._path, "rb+") as handler:
175
+ _sanitise_file(handler)
176
+ handler.seek(0)
177
+ location_table = numpy.fromfile(
178
+ handler, dtype=">u4", count=1024
179
+ ).reshape(32, 32)
180
+ for (z, x), sector_data in numpy.ndenumerate(location_table):
181
+ if sector_data:
182
+ sector_offset = (sector_data >> 8) * 0x1000
183
+ sector_size = (sector_data & 0xFF) * 0x1000
184
+ sector = Sector(sector_offset, sector_offset + sector_size)
185
+ self._sector_manager.reserve(sector)
186
+ self._chunk_locations[(x, z)] = sector
187
+
188
+ def all_chunk_coords(self) -> Generator[ChunkCoordinates, None, None]:
189
+ """An iterable of chunk coordinates in world space."""
190
+ self._load()
191
+ for cx, cz in list(self._chunk_locations):
192
+ yield cx + self.rx * 32, cz + self.rz * 32
193
+
194
+ def has_chunk(self, cx: int, cz: int) -> bool:
195
+ """Does the chunk exists. Coords are in region space."""
196
+ _validate_region_coords(cx, cz)
197
+ self._load()
198
+ return (cx, cz) in self._chunk_locations
199
+
200
+ def unload(self):
201
+ """Unload the data if it is not being used."""
202
+ with self._lock:
203
+ self._sector_manager = None
204
+ self._chunk_locations.clear()
205
+
206
+ def get_data(self, cx: int, cz: int) -> NamedTag:
207
+ _validate_region_coords(cx, cz)
208
+ self._load()
209
+ sector = self._chunk_locations.get((cx, cz))
210
+ if sector is None:
211
+ raise ChunkDoesNotExist
212
+ with self._lock:
213
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
214
+ with open(self._path, "rb+") as handler:
215
+ _sanitise_file(handler)
216
+ handler.seek(0, os.SEEK_END)
217
+ if handler.tell() < sector.stop:
218
+ # if the sector is beyond the end of the file
219
+ raise ChunkDoesNotExist
220
+
221
+ handler.seek(sector.start)
222
+ buffer_size_bytes: bytes = handler.read(4)
223
+ buffer_size = struct.unpack(">I", buffer_size_bytes)[0]
224
+ buffer: bytes = handler.read(buffer_size)
225
+
226
+ if buffer:
227
+ if buffer[0] & 128: # if the "external" bit is set
228
+ if self._mcc:
229
+ mcc_path = self.get_mcc_path(cx, cz)
230
+ if os.path.isfile(mcc_path):
231
+ with open(mcc_path, "rb") as f:
232
+ return _decompress(
233
+ bytes([buffer[0] & 127]) + f.read()
234
+ )
235
+ else:
236
+ return _decompress(buffer)
237
+ raise ChunkDoesNotExist
238
+
239
+ def _write_data(self, cx: int, cz: int, data: Optional[bytes]):
240
+ _validate_region_coords(cx, cz)
241
+ if isinstance(data, bytes) and len(data) + 4 > MaxRegionSize and not self._mcc:
242
+ # if the data is too large and mcc files are not supported then do nothing
243
+ log.error(
244
+ f"Could not save data {cx},{cz} in region file {self._path} because it was too large."
245
+ )
246
+ return
247
+
248
+ self._load()
249
+ with self._lock:
250
+ os.makedirs(os.path.dirname(self._path), exist_ok=True)
251
+ with open(
252
+ self._path, "rb+" if os.path.isfile(self._path) else "wb+"
253
+ ) as handler:
254
+ handler: BinaryIO
255
+ _sanitise_file(handler)
256
+
257
+ old_sector = self._chunk_locations.pop((cx, cz), None)
258
+ if old_sector is not None:
259
+ # the chunk used to exist
260
+ handler.seek(old_sector.start + 4)
261
+ if self._mcc and handler.read(1)[0] & 127:
262
+ # if the file is stored externally delete the file
263
+ mcc_path = self.get_mcc_path(cx, cz)
264
+ if os.path.isfile(mcc_path):
265
+ os.remove(mcc_path)
266
+ self._sector_manager.free(old_sector)
267
+
268
+ location = b"\x00\x00\x00\x00"
269
+
270
+ if isinstance(data, bytes):
271
+ # find a memory location large enough to fit the data
272
+ if len(data) + 4 > MaxRegionSize:
273
+ # save externally (if mcc files are not supported the check at the top will filter large files out)
274
+ with open(self.get_mcc_path(cx, cz), "wb") as mcc:
275
+ mcc.write(data[1:])
276
+ data = bytes([data[0] | 128])
277
+ data = struct.pack(">I", len(data)) + data
278
+ sector_length = (len(data) | 0xFFF) + 1
279
+ sector = self._sector_manager.reserve_space(sector_length)
280
+ assert sector.start & 0xFFF == 0
281
+ self._chunk_locations[(cx, cz)] = sector
282
+ location = struct.pack(
283
+ ">I", (sector.start >> 4) + (sector_length >> 12)
284
+ )
285
+ handler.seek(sector.start)
286
+ handler.write(data)
287
+ _sanitise_file(handler)
288
+
289
+ # write the header data
290
+ handler.seek(4 * (cx + cz * 32))
291
+ handler.write(location)
292
+ handler.seek(SectorSize - 4, os.SEEK_CUR)
293
+ handler.write(struct.pack(">I", int(time.time())))
294
+
295
+ def write_data(self, cx: int, cz: int, data: NamedTag):
296
+ """Write the data to the region file."""
297
+ bytes_data = _compress(data)
298
+ self._write_data(cx, cz, bytes_data)
299
+
300
+ def delete_data(self, cx: int, cz: int):
301
+ """Delete the data from the region file."""
302
+ self._write_data(cx, cz, None)
303
+
304
+
305
+ class BufferedAnvilRegionInterface(AnvilRegionInterface):
306
+ """An interface to an anvil region file with a buffer before writing."""
307
+
308
+ __slots__ = ("_buffer",)
309
+
310
+ _buffer: Dict[ChunkCoordinates, Optional[bytes]]
311
+
312
+ def __init__(self, *args, **kwargs):
313
+ warnings.warn(
314
+ "BufferedAnvilRegionInterface aka AnvilRegion is depreciated and replaced by AnvilRegionInterface",
315
+ DeprecationWarning,
316
+ )
317
+ super().__init__(*args, **kwargs)
318
+ self._buffer = {}
319
+
320
+ def all_chunk_coords(self) -> Generator[ChunkCoordinates, None, None]:
321
+ """An iterable of chunk coordinates in world space."""
322
+ self._load()
323
+ with self._lock:
324
+ chunks = [
325
+ (cx + self.rx * 32, cz + self.rz * 32)
326
+ for cx, cz in self._chunk_locations
327
+ if (cx, cz) not in self._buffer
328
+ ] + [
329
+ (cx + self.rx * 32, cz + self.rz * 32)
330
+ for (cx, cz), value in self._buffer.items()
331
+ if value is not None
332
+ ]
333
+ yield from chunks
334
+
335
+ def unload(self):
336
+ with self._lock:
337
+ super().unload()
338
+ self._buffer.clear()
339
+
340
+ def has_chunk(self, cx: int, cz: int) -> bool:
341
+ """Does the chunk exists. Coords are in region space."""
342
+ _validate_region_coords(cx, cz)
343
+ with self._lock:
344
+ if (cx, cz) in self._buffer:
345
+ return self._buffer[(cx, cz)] is not None
346
+ else:
347
+ self._load()
348
+ return (cx, cz) in self._chunk_locations
349
+
350
+ def get_chunk_data(self, cx: int, cz: int) -> NamedTag:
351
+ """Get chunk data. Coords are in region space."""
352
+ _validate_region_coords(cx, cz)
353
+ data = self._buffer.get((cx, cz))
354
+ if data is not None:
355
+ return _decompress(data)
356
+ return self.get_data(cx, cz)
357
+
358
+ def put_chunk_data(self, cx: int, cz: int, data: NamedTag):
359
+ """
360
+ Put data to be added to the region file. `save` will push the changes to disk.
361
+ Coords are in region space.
362
+ """
363
+ _validate_region_coords(cx, cz)
364
+ with self._lock:
365
+ self._buffer[(cx, cz)] = _compress(data)
366
+
367
+ def delete_chunk_data(self, cx: int, cz: int):
368
+ """
369
+ Mark the data for deletion. `save` will push the changes to disk.
370
+ Coords are in region space.
371
+ """
372
+ _validate_region_coords(cx, cz)
373
+ with self._lock:
374
+ self._buffer[(cx, cz)] = None
375
+
376
+ def save(self):
377
+ """Write the buffered data to the file and clear the buffer."""
378
+ with self._lock:
379
+ for (cx, cz), data in self._buffer.items():
380
+ self._write_data(cx, cz, data)
381
+ self._buffer.clear()
382
+
383
+
384
+ AnvilRegion = BufferedAnvilRegionInterface