amulet-core 2.0a8__cp311-cp311-win_amd64.whl → 2.0.1a3.post250529101324__cp311-cp311-win_amd64.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 (271) hide show
  1. amulet/core/__init__.py +36 -0
  2. amulet/core/__pyinstaller/hook-amulet.core.py +4 -0
  3. amulet/core/_amulet_core.cp311-win_amd64.pyd +0 -0
  4. amulet/core/_amulet_core.pyi +7 -0
  5. amulet/{_version.py → core/_version.py} +3 -3
  6. amulet/core/amulet_core.dll +0 -0
  7. amulet/core/amulet_core.lib +0 -0
  8. amulet/core/amulet_coreConfig.cmake +18 -0
  9. amulet/{biome.pyi → core/biome/__init__.pyi} +3 -3
  10. amulet/core/biome/biome.hpp +53 -0
  11. amulet/{block.pyi → core/block/__init__.pyi} +25 -26
  12. amulet/core/block/block.hpp +156 -0
  13. amulet/{block_entity.pyi → core/block_entity/__init__.pyi} +7 -7
  14. amulet/core/block_entity/block_entity.hpp +84 -0
  15. amulet/{errors.py → core/chunk/__init__.pyi} +37 -33
  16. amulet/core/chunk/chunk.hpp +126 -0
  17. amulet/core/chunk/component/__init__.pyi +18 -0
  18. amulet/core/chunk/component/biome_3d_component.hpp +96 -0
  19. amulet/core/chunk/component/block_component.hpp +101 -0
  20. amulet/core/chunk/component/block_component.pyi +28 -0
  21. amulet/core/chunk/component/block_entity_component.hpp +119 -0
  22. amulet/core/chunk/component/section_array_map.hpp +129 -0
  23. amulet/{chunk_components.pyi → core/chunk/component/section_array_map.pyi} +4 -24
  24. amulet/core/dll.hpp +21 -0
  25. amulet/core/entity/__init__.pyi +105 -0
  26. amulet/core/entity/entity.hpp +100 -0
  27. amulet/{palette → core/palette}/__init__.pyi +2 -2
  28. amulet/core/palette/biome_palette.hpp +65 -0
  29. amulet/{palette → core/palette}/biome_palette.pyi +8 -8
  30. amulet/core/palette/block_palette.hpp +71 -0
  31. amulet/{palette → core/palette}/block_palette.pyi +12 -10
  32. amulet/core/selection/__init__.pyi +8 -0
  33. amulet/core/selection/box.hpp +86 -0
  34. amulet/core/selection/box.pyi +215 -0
  35. amulet/core/selection/group.hpp +80 -0
  36. amulet/core/selection/group.pyi +213 -0
  37. amulet/{version.pyi → core/version/__init__.pyi} +58 -10
  38. amulet/core/version/version.hpp +204 -0
  39. {amulet_core-2.0a8.dist-info → amulet_core-2.0.1a3.post250529101324.dist-info}/METADATA +25 -20
  40. amulet_core-2.0.1a3.post250529101324.dist-info/RECORD +45 -0
  41. {amulet_core-2.0a8.dist-info → amulet_core-2.0.1a3.post250529101324.dist-info}/WHEEL +1 -1
  42. amulet_core-2.0.1a3.post250529101324.dist-info/entry_points.txt +2 -0
  43. amulet/__init__.cp311-win_amd64.pyd +0 -0
  44. amulet/__init__.py.cpp +0 -45
  45. amulet/__init__.pyi +0 -30
  46. amulet/__pyinstaller/hook-amulet.py +0 -4
  47. amulet/_init.py +0 -26
  48. amulet/biome.cpp +0 -36
  49. amulet/biome.hpp +0 -43
  50. amulet/biome.py.cpp +0 -122
  51. amulet/block.cpp +0 -435
  52. amulet/block.hpp +0 -119
  53. amulet/block.py.cpp +0 -377
  54. amulet/block_entity.cpp +0 -12
  55. amulet/block_entity.hpp +0 -56
  56. amulet/block_entity.py.cpp +0 -115
  57. amulet/chunk.cpp +0 -16
  58. amulet/chunk.hpp +0 -100
  59. amulet/chunk.py.cpp +0 -80
  60. amulet/chunk.pyi +0 -28
  61. amulet/chunk_components/biome_3d_component.cpp +0 -5
  62. amulet/chunk_components/biome_3d_component.hpp +0 -79
  63. amulet/chunk_components/block_component.cpp +0 -41
  64. amulet/chunk_components/block_component.hpp +0 -88
  65. amulet/chunk_components/block_entity_component.cpp +0 -5
  66. amulet/chunk_components/block_entity_component.hpp +0 -147
  67. amulet/chunk_components/section_array_map.cpp +0 -129
  68. amulet/chunk_components/section_array_map.hpp +0 -147
  69. amulet/collections/eq.py.hpp +0 -37
  70. amulet/collections/hash.py.hpp +0 -27
  71. amulet/collections/holder.py.hpp +0 -37
  72. amulet/collections/iterator.py.hpp +0 -80
  73. amulet/collections/mapping.py.hpp +0 -199
  74. amulet/collections/mutable_mapping.py.hpp +0 -226
  75. amulet/collections/sequence.py.hpp +0 -163
  76. amulet/collections.pyi +0 -40
  77. amulet/data_types.py +0 -29
  78. amulet/entity.py +0 -182
  79. amulet/game/__init__.py +0 -7
  80. amulet/game/_game.py +0 -152
  81. amulet/game/_universal/__init__.py +0 -1
  82. amulet/game/_universal/_biome.py +0 -17
  83. amulet/game/_universal/_block.py +0 -47
  84. amulet/game/_universal/_version.py +0 -68
  85. amulet/game/abc/__init__.py +0 -22
  86. amulet/game/abc/_block_specification.py +0 -150
  87. amulet/game/abc/biome.py +0 -213
  88. amulet/game/abc/block.py +0 -331
  89. amulet/game/abc/game_version_container.py +0 -25
  90. amulet/game/abc/json_interface.py +0 -27
  91. amulet/game/abc/version.py +0 -44
  92. amulet/game/bedrock/__init__.py +0 -1
  93. amulet/game/bedrock/_biome.py +0 -35
  94. amulet/game/bedrock/_block.py +0 -42
  95. amulet/game/bedrock/_version.py +0 -165
  96. amulet/game/java/__init__.py +0 -2
  97. amulet/game/java/_biome.py +0 -35
  98. amulet/game/java/_block.py +0 -60
  99. amulet/game/java/_version.py +0 -176
  100. amulet/game/translate/__init__.py +0 -12
  101. amulet/game/translate/_functions/__init__.py +0 -15
  102. amulet/game/translate/_functions/_code_functions/__init__.py +0 -0
  103. amulet/game/translate/_functions/_code_functions/_text.py +0 -553
  104. amulet/game/translate/_functions/_code_functions/banner_pattern.py +0 -67
  105. amulet/game/translate/_functions/_code_functions/bedrock_chest_connection.py +0 -152
  106. amulet/game/translate/_functions/_code_functions/bedrock_moving_block_pos.py +0 -88
  107. amulet/game/translate/_functions/_code_functions/bedrock_sign.py +0 -152
  108. amulet/game/translate/_functions/_code_functions/bedrock_skull_rotation.py +0 -16
  109. amulet/game/translate/_functions/_code_functions/custom_name.py +0 -146
  110. amulet/game/translate/_functions/_frozen.py +0 -66
  111. amulet/game/translate/_functions/_state.py +0 -54
  112. amulet/game/translate/_functions/_typing.py +0 -98
  113. amulet/game/translate/_functions/abc.py +0 -123
  114. amulet/game/translate/_functions/carry_nbt.py +0 -160
  115. amulet/game/translate/_functions/carry_properties.py +0 -80
  116. amulet/game/translate/_functions/code.py +0 -143
  117. amulet/game/translate/_functions/map_block_name.py +0 -66
  118. amulet/game/translate/_functions/map_nbt.py +0 -111
  119. amulet/game/translate/_functions/map_properties.py +0 -93
  120. amulet/game/translate/_functions/multiblock.py +0 -112
  121. amulet/game/translate/_functions/new_block.py +0 -42
  122. amulet/game/translate/_functions/new_entity.py +0 -43
  123. amulet/game/translate/_functions/new_nbt.py +0 -206
  124. amulet/game/translate/_functions/new_properties.py +0 -64
  125. amulet/game/translate/_functions/sequence.py +0 -51
  126. amulet/game/translate/_functions/walk_input_nbt.py +0 -331
  127. amulet/game/translate/_translator.py +0 -433
  128. amulet/img/__init__.py +0 -10
  129. amulet/img/missing_no.png +0 -0
  130. amulet/img/missing_pack.png +0 -0
  131. amulet/img/missing_world.png +0 -0
  132. amulet/io/binary_reader.hpp +0 -45
  133. amulet/io/binary_writer.hpp +0 -30
  134. amulet/item.py +0 -75
  135. amulet/level/__init__.pyi +0 -23
  136. amulet/level/_load.py +0 -100
  137. amulet/level/abc/__init__.py +0 -12
  138. amulet/level/abc/_chunk_handle.py +0 -358
  139. amulet/level/abc/_dimension.py +0 -86
  140. amulet/level/abc/_history/__init__.py +0 -1
  141. amulet/level/abc/_history/_cache.py +0 -224
  142. amulet/level/abc/_history/_history_manager.py +0 -291
  143. amulet/level/abc/_level/__init__.py +0 -5
  144. amulet/level/abc/_level/_compactable_level.py +0 -10
  145. amulet/level/abc/_level/_creatable_level.py +0 -28
  146. amulet/level/abc/_level/_disk_level.py +0 -17
  147. amulet/level/abc/_level/_level.py +0 -449
  148. amulet/level/abc/_level/_loadable_level.py +0 -42
  149. amulet/level/abc/_player_storage.py +0 -7
  150. amulet/level/abc/_raw_level.py +0 -187
  151. amulet/level/abc/_registry.py +0 -40
  152. amulet/level/java/__init__.pyi +0 -16
  153. amulet/level/java/_chunk_handle.py +0 -17
  154. amulet/level/java/_dimension.py +0 -20
  155. amulet/level/java/_level.py +0 -184
  156. amulet/level/java/_raw/__init__.pyi +0 -15
  157. amulet/level/java/_raw/_chunk.pyi +0 -23
  158. amulet/level/java/_raw/_constant.py +0 -9
  159. amulet/level/java/_raw/_data_pack/__init__.py +0 -2
  160. amulet/level/java/_raw/_data_pack/data_pack.py +0 -241
  161. amulet/level/java/_raw/_data_pack/data_pack_manager.py +0 -77
  162. amulet/level/java/_raw/_dimension.py +0 -86
  163. amulet/level/java/_raw/_level.py +0 -507
  164. amulet/level/java/_raw/_typing.py +0 -3
  165. amulet/level/java/_raw/java_chunk_decode.cpp +0 -531
  166. amulet/level/java/_raw/java_chunk_decode.hpp +0 -23
  167. amulet/level/java/_raw/java_chunk_encode.cpp +0 -25
  168. amulet/level/java/_raw/java_chunk_encode.hpp +0 -23
  169. amulet/level/java/anvil/__init__.py +0 -2
  170. amulet/level/java/anvil/_dimension.py +0 -170
  171. amulet/level/java/anvil/_region.py +0 -421
  172. amulet/level/java/anvil/_sector_manager.py +0 -223
  173. amulet/level/java/chunk.pyi +0 -81
  174. amulet/level/java/chunk_/_chunk.py +0 -260
  175. amulet/level/java/chunk_/components/inhabited_time.py +0 -12
  176. amulet/level/java/chunk_/components/last_update.py +0 -12
  177. amulet/level/java/chunk_/components/legacy_version.py +0 -12
  178. amulet/level/java/chunk_/components/light_populated.py +0 -12
  179. amulet/level/java/chunk_/components/named_height_2d.py +0 -37
  180. amulet/level/java/chunk_/components/status.py +0 -11
  181. amulet/level/java/chunk_/components/terrain_populated.py +0 -12
  182. amulet/level/java/chunk_components/data_version_component.cpp +0 -32
  183. amulet/level/java/chunk_components/data_version_component.hpp +0 -31
  184. amulet/level/java/chunk_components/java_raw_chunk_component.cpp +0 -56
  185. amulet/level/java/chunk_components/java_raw_chunk_component.hpp +0 -45
  186. amulet/level/java/chunk_components.pyi +0 -22
  187. amulet/level/java/java_chunk.cpp +0 -170
  188. amulet/level/java/java_chunk.hpp +0 -141
  189. amulet/level/java/long_array.hpp +0 -175
  190. amulet/level/java/long_array.pyi +0 -39
  191. amulet/level/temporary_level/__init__.py +0 -1
  192. amulet/level/temporary_level/_level.py +0 -16
  193. amulet/mesh/__init__.py +0 -0
  194. amulet/mesh/block/__init__.pyi +0 -301
  195. amulet/mesh/block/_cube.py +0 -198
  196. amulet/mesh/block/_missing_block.py +0 -20
  197. amulet/mesh/block/block_mesh.cpp +0 -107
  198. amulet/mesh/block/block_mesh.hpp +0 -207
  199. amulet/mesh/util.py +0 -17
  200. amulet/palette/biome_palette.hpp +0 -85
  201. amulet/palette/block_palette.cpp +0 -32
  202. amulet/palette/block_palette.hpp +0 -93
  203. amulet/player.py +0 -62
  204. amulet/pybind11/collections.hpp +0 -118
  205. amulet/pybind11/numpy.hpp +0 -26
  206. amulet/pybind11/py_module.hpp +0 -34
  207. amulet/pybind11/type_hints.hpp +0 -51
  208. amulet/pybind11/types.hpp +0 -25
  209. amulet/pybind11/typing.hpp +0 -7
  210. amulet/resource_pack/__init__.py +0 -63
  211. amulet/resource_pack/abc/__init__.py +0 -2
  212. amulet/resource_pack/abc/resource_pack.py +0 -38
  213. amulet/resource_pack/abc/resource_pack_manager.py +0 -85
  214. amulet/resource_pack/java/__init__.py +0 -2
  215. amulet/resource_pack/java/download_resources.py +0 -212
  216. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_black.png +0 -0
  217. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_blue.png +0 -0
  218. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_brown.png +0 -0
  219. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_cyan.png +0 -0
  220. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_gray.png +0 -0
  221. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_green.png +0 -0
  222. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_light_blue.png +0 -0
  223. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_light_gray.png +0 -0
  224. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_lime.png +0 -0
  225. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_magenta.png +0 -0
  226. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_orange.png +0 -0
  227. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_pink.png +0 -0
  228. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_purple.png +0 -0
  229. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_red.png +0 -0
  230. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_white.png +0 -0
  231. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/banner/banner_yellow.png +0 -0
  232. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/barrier.png +0 -0
  233. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/end_portal.png +0 -0
  234. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/grass.png +0 -0
  235. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/lava.png +0 -0
  236. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/structure_void.png +0 -0
  237. amulet/resource_pack/java/java_vanilla_fix/assets/minecraft/textures/block/water.png +0 -0
  238. amulet/resource_pack/java/java_vanilla_fix/pack.png +0 -0
  239. amulet/resource_pack/java/resource_pack.py +0 -44
  240. amulet/resource_pack/java/resource_pack_manager.py +0 -563
  241. amulet/resource_pack/unknown_resource_pack.py +0 -10
  242. amulet/selection/__init__.py +0 -2
  243. amulet/selection/abstract_selection.py +0 -342
  244. amulet/selection/box.py +0 -852
  245. amulet/selection/group.py +0 -481
  246. amulet/utils/__init__.pyi +0 -23
  247. amulet/utils/call_spec/__init__.py +0 -24
  248. amulet/utils/call_spec/_call_spec.py +0 -257
  249. amulet/utils/cast.py +0 -10
  250. amulet/utils/comment_json.py +0 -188
  251. amulet/utils/format_utils.py +0 -41
  252. amulet/utils/generator.py +0 -18
  253. amulet/utils/matrix.py +0 -243
  254. amulet/utils/numpy.hpp +0 -36
  255. amulet/utils/numpy.pyi +0 -11
  256. amulet/utils/numpy_helpers.py +0 -19
  257. amulet/utils/shareable_lock.py +0 -335
  258. amulet/utils/signal/__init__.py +0 -10
  259. amulet/utils/signal/_signal.py +0 -228
  260. amulet/utils/task_manager.py +0 -235
  261. amulet/utils/typed_property.py +0 -111
  262. amulet/utils/weakref.py +0 -70
  263. amulet/utils/world_utils.py +0 -102
  264. amulet/version.cpp +0 -136
  265. amulet/version.hpp +0 -142
  266. amulet/version.py.cpp +0 -281
  267. amulet_core-2.0a8.dist-info/RECORD +0 -241
  268. amulet_core-2.0a8.dist-info/entry_points.txt +0 -2
  269. /amulet/{__pyinstaller → core/__pyinstaller}/__init__.py +0 -0
  270. /amulet/{py.typed → core/py.typed} +0 -0
  271. {amulet_core-2.0a8.dist-info → amulet_core-2.0.1a3.post250529101324.dist-info}/top_level.txt +0 -0
@@ -1,170 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TypeAlias
4
- import os
5
- from collections.abc import Iterator, Sequence
6
- import re
7
- import threading
8
-
9
- from amulet_nbt import NamedTag
10
-
11
- from amulet.utils import world_utils
12
- from amulet.errors import ChunkDoesNotExist
13
- from amulet.data_types import (
14
- ChunkCoordinates,
15
- RegionCoordinates,
16
- )
17
- from ._region import AnvilRegion
18
-
19
-
20
- RawChunkType: TypeAlias = dict[str, NamedTag]
21
-
22
-
23
- class AnvilDimensionLayer:
24
- """A class to manage a directory of region files."""
25
-
26
- def __init__(self, directory: str, *, mcc: bool = False):
27
- self._directory = directory
28
- self._regions: dict[RegionCoordinates, AnvilRegion] = {}
29
- self._mcc = mcc
30
- self._lock = threading.RLock()
31
-
32
- def _region_path(self, rx: int, rz: int) -> str:
33
- """Get the file path for a region file."""
34
- return os.path.join(self._directory, f"r.{rx}.{rz}.mca")
35
-
36
- def _has_region(self, rx: int, rz: int) -> bool:
37
- """Does a region file exist."""
38
- return os.path.isfile(self._region_path(rx, rz))
39
-
40
- def _get_region(self, rx: int, rz: int, create: bool = False) -> AnvilRegion:
41
- with self._lock:
42
- if (rx, rz) in self._regions:
43
- return self._regions[(rx, rz)]
44
- elif create or self._has_region(rx, rz):
45
- region = self._regions[(rx, rz)] = AnvilRegion(
46
- self._region_path(rx, rz), mcc=self._mcc
47
- )
48
- return region
49
- else:
50
- raise ChunkDoesNotExist
51
-
52
- def _iter_regions(self) -> Iterator[AnvilRegion]:
53
- if os.path.isdir(self._directory):
54
- for region_file_name in os.listdir(self._directory):
55
- try:
56
- rx, rz = AnvilRegion.get_coords(region_file_name)
57
- except ValueError:
58
- continue
59
- else:
60
- yield self._get_region(rx, rz)
61
-
62
- def all_chunk_coords(self) -> Iterator[ChunkCoordinates]:
63
- for region in self._iter_regions():
64
- yield from region.all_coords()
65
-
66
- def has_chunk(self, cx: int, cz: int) -> bool:
67
- try:
68
- region = self._get_region(
69
- *world_utils.chunk_coords_to_region_coords(cx, cz)
70
- )
71
- except ChunkDoesNotExist:
72
- return False
73
- else:
74
- return region.has_data(cx, cz)
75
-
76
- def get_chunk_data(self, cx: int, cz: int) -> NamedTag:
77
- """
78
- Get a NamedTag of a chunk from the database.
79
- Will raise ChunkDoesNotExist if the region or chunk does not exist
80
- """
81
- # get the region key
82
- return self._get_region(
83
- *world_utils.chunk_coords_to_region_coords(cx, cz)
84
- ).get_data(cx, cz)
85
-
86
- def put_chunk_data(self, cx: int, cz: int, data: NamedTag) -> None:
87
- """pass data to the region file class"""
88
- self._get_region(
89
- *world_utils.chunk_coords_to_region_coords(cx, cz), create=True
90
- ).set_data(cx, cz, data)
91
-
92
- def delete_chunk(self, cx: int, cz: int) -> None:
93
- try:
94
- region = self._get_region(
95
- *world_utils.chunk_coords_to_region_coords(cx, cz)
96
- )
97
- except ChunkDoesNotExist:
98
- pass
99
- else:
100
- region.delete_data(cx, cz)
101
-
102
- def compact(self) -> None:
103
- """Compact all region files in this layer"""
104
- for region in self._iter_regions():
105
- region.compact()
106
-
107
-
108
- class AnvilDimension:
109
- """
110
- A class to manage the data for a dimension.
111
- This can consist of multiple layers. Eg the region layer which contains chunk data and the entities layer which contains entities.
112
- """
113
-
114
- level_regex = re.compile(r"DIM(?P<level>-?\d+)")
115
-
116
- def __init__(
117
- self, directory: str, *, mcc: bool = False, layers: Sequence[str] = ("region",)
118
- ) -> None:
119
- self._directory = directory
120
- self._mcc = mcc
121
- self.__layers: dict[str, AnvilDimensionLayer] = {
122
- layer: AnvilDimensionLayer(
123
- os.path.join(self._directory, layer), mcc=self._mcc
124
- )
125
- for layer in layers
126
- }
127
- self.__default_layer = self.__layers[layers[0]]
128
-
129
- def all_chunk_coords(self) -> Iterator[ChunkCoordinates]:
130
- yield from self.__default_layer.all_chunk_coords()
131
-
132
- def has_chunk(self, cx: int, cz: int) -> bool:
133
- return self.__default_layer.has_chunk(cx, cz)
134
-
135
- def get_chunk_data(self, cx: int, cz: int) -> RawChunkType:
136
- """Get the chunk data for each layer"""
137
- chunk_data = {}
138
- for layer_name, layer in self.__layers.items():
139
- try:
140
- chunk_data[layer_name] = layer.get_chunk_data(cx, cz)
141
- except ChunkDoesNotExist:
142
- pass
143
-
144
- if chunk_data:
145
- return chunk_data
146
- else:
147
- raise ChunkDoesNotExist
148
-
149
- def put_chunk_data(self, cx: int, cz: int, data_layers: RawChunkType) -> None:
150
- """Put one or more layers of data"""
151
- for layer_name, data in data_layers.items():
152
- if (
153
- layer_name not in self.__layers
154
- and layer_name.isalpha()
155
- and layer_name.islower()
156
- ):
157
- self.__layers[layer_name] = AnvilDimensionLayer(
158
- os.path.join(self._directory, layer_name), mcc=self._mcc
159
- )
160
- if layer_name in self.__layers:
161
- self.__layers[layer_name].put_chunk_data(cx, cz, data)
162
-
163
- def delete_chunk(self, cx: int, cz: int) -> None:
164
- for layer in self.__layers.values():
165
- layer.delete_chunk(cx, cz)
166
-
167
- def compact(self) -> None:
168
- """Compact all region files in this dimension"""
169
- for layer in self.__layers.values():
170
- layer.compact()
@@ -1,421 +0,0 @@
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)