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.
- amulet/__init__.cpython-311-darwin.so +0 -0
- amulet/__init__.pyi +30 -0
- amulet/__pyinstaller/__init__.py +2 -0
- amulet/__pyinstaller/hook-amulet.py +4 -0
- amulet/_init.py +28 -0
- amulet/_version.py +21 -0
- amulet/biome.cpp +36 -0
- amulet/biome.hpp +43 -0
- amulet/biome.pyi +77 -0
- amulet/block.cpp +435 -0
- amulet/block.hpp +119 -0
- amulet/block.pyi +273 -0
- amulet/block_entity.cpp +12 -0
- amulet/block_entity.hpp +56 -0
- amulet/block_entity.pyi +80 -0
- amulet/chunk.cpp +16 -0
- amulet/chunk.hpp +99 -0
- amulet/chunk.pyi +30 -0
- amulet/chunk_/components/biome.py +155 -0
- amulet/chunk_/components/block_entity.py +117 -0
- amulet/chunk_/components/entity.py +64 -0
- amulet/chunk_/components/height_2d.py +16 -0
- amulet/chunk_components.pyi +95 -0
- amulet/collections.pyi +37 -0
- amulet/data_types.py +29 -0
- amulet/entity.py +180 -0
- amulet/errors.py +63 -0
- amulet/game/__init__.py +7 -0
- amulet/game/_game.py +152 -0
- amulet/game/_universal/__init__.py +1 -0
- amulet/game/_universal/_biome.py +17 -0
- amulet/game/_universal/_block.py +47 -0
- amulet/game/_universal/_version.py +68 -0
- amulet/game/abc/__init__.py +22 -0
- amulet/game/abc/_block_specification.py +150 -0
- amulet/game/abc/biome.py +213 -0
- amulet/game/abc/block.py +331 -0
- amulet/game/abc/game_version_container.py +25 -0
- amulet/game/abc/json_interface.py +27 -0
- amulet/game/abc/version.py +44 -0
- amulet/game/bedrock/__init__.py +1 -0
- amulet/game/bedrock/_biome.py +35 -0
- amulet/game/bedrock/_block.py +42 -0
- amulet/game/bedrock/_version.py +165 -0
- amulet/game/java/__init__.py +2 -0
- amulet/game/java/_biome.py +35 -0
- amulet/game/java/_block.py +60 -0
- amulet/game/java/_version.py +176 -0
- amulet/game/translate/__init__.py +12 -0
- amulet/game/translate/_functions/__init__.py +15 -0
- amulet/game/translate/_functions/_code_functions/__init__.py +0 -0
- amulet/game/translate/_functions/_code_functions/_text.py +553 -0
- amulet/game/translate/_functions/_code_functions/banner_pattern.py +67 -0
- amulet/game/translate/_functions/_code_functions/bedrock_chest_connection.py +152 -0
- amulet/game/translate/_functions/_code_functions/bedrock_moving_block_pos.py +88 -0
- amulet/game/translate/_functions/_code_functions/bedrock_sign.py +152 -0
- amulet/game/translate/_functions/_code_functions/bedrock_skull_rotation.py +16 -0
- amulet/game/translate/_functions/_code_functions/custom_name.py +146 -0
- amulet/game/translate/_functions/_frozen.py +66 -0
- amulet/game/translate/_functions/_state.py +54 -0
- amulet/game/translate/_functions/_typing.py +98 -0
- amulet/game/translate/_functions/abc.py +116 -0
- amulet/game/translate/_functions/carry_nbt.py +160 -0
- amulet/game/translate/_functions/carry_properties.py +80 -0
- amulet/game/translate/_functions/code.py +143 -0
- amulet/game/translate/_functions/map_block_name.py +66 -0
- amulet/game/translate/_functions/map_nbt.py +111 -0
- amulet/game/translate/_functions/map_properties.py +93 -0
- amulet/game/translate/_functions/multiblock.py +112 -0
- amulet/game/translate/_functions/new_block.py +42 -0
- amulet/game/translate/_functions/new_entity.py +43 -0
- amulet/game/translate/_functions/new_nbt.py +206 -0
- amulet/game/translate/_functions/new_properties.py +64 -0
- amulet/game/translate/_functions/sequence.py +51 -0
- amulet/game/translate/_functions/walk_input_nbt.py +331 -0
- amulet/game/translate/_translator.py +433 -0
- amulet/item.py +75 -0
- amulet/level/__init__.pyi +27 -0
- amulet/level/_load.py +100 -0
- amulet/level/abc/__init__.py +12 -0
- amulet/level/abc/_chunk_handle.py +335 -0
- amulet/level/abc/_dimension.py +86 -0
- amulet/level/abc/_history/__init__.py +1 -0
- amulet/level/abc/_history/_cache.py +224 -0
- amulet/level/abc/_history/_history_manager.py +291 -0
- amulet/level/abc/_level/__init__.py +5 -0
- amulet/level/abc/_level/_compactable_level.py +10 -0
- amulet/level/abc/_level/_creatable_level.py +29 -0
- amulet/level/abc/_level/_disk_level.py +17 -0
- amulet/level/abc/_level/_level.py +453 -0
- amulet/level/abc/_level/_loadable_level.py +42 -0
- amulet/level/abc/_player_storage.py +7 -0
- amulet/level/abc/_raw_level.py +187 -0
- amulet/level/abc/_registry.py +40 -0
- amulet/level/bedrock/__init__.py +2 -0
- amulet/level/bedrock/_chunk_handle.py +19 -0
- amulet/level/bedrock/_dimension.py +22 -0
- amulet/level/bedrock/_level.py +187 -0
- amulet/level/bedrock/_raw/__init__.py +5 -0
- amulet/level/bedrock/_raw/_actor_counter.py +53 -0
- amulet/level/bedrock/_raw/_chunk.py +54 -0
- amulet/level/bedrock/_raw/_chunk_decode.py +668 -0
- amulet/level/bedrock/_raw/_chunk_encode.py +602 -0
- amulet/level/bedrock/_raw/_constant.py +9 -0
- amulet/level/bedrock/_raw/_dimension.py +343 -0
- amulet/level/bedrock/_raw/_level.py +463 -0
- amulet/level/bedrock/_raw/_level_dat.py +90 -0
- amulet/level/bedrock/_raw/_typing.py +6 -0
- amulet/level/bedrock/_raw/leveldb_chunk_versions.py +83 -0
- amulet/level/bedrock/chunk/__init__.py +1 -0
- amulet/level/bedrock/chunk/_chunk.py +126 -0
- amulet/level/bedrock/chunk/components/__init__.py +0 -0
- amulet/level/bedrock/chunk/components/chunk_version.py +12 -0
- amulet/level/bedrock/chunk/components/finalised_state.py +13 -0
- amulet/level/bedrock/chunk/components/raw_chunk.py +15 -0
- amulet/level/construction/__init__.py +0 -0
- amulet/level/java/__init__.pyi +21 -0
- amulet/level/java/_chunk_handle.py +17 -0
- amulet/level/java/_chunk_handle.pyi +15 -0
- amulet/level/java/_dimension.py +20 -0
- amulet/level/java/_dimension.pyi +13 -0
- amulet/level/java/_level.py +184 -0
- amulet/level/java/_level.pyi +120 -0
- amulet/level/java/_raw/__init__.pyi +19 -0
- amulet/level/java/_raw/_chunk.pyi +23 -0
- amulet/level/java/_raw/_chunk_decode.py +561 -0
- amulet/level/java/_raw/_chunk_encode.py +463 -0
- amulet/level/java/_raw/_constant.py +9 -0
- amulet/level/java/_raw/_constant.pyi +20 -0
- amulet/level/java/_raw/_data_pack/__init__.py +2 -0
- amulet/level/java/_raw/_data_pack/__init__.pyi +8 -0
- amulet/level/java/_raw/_data_pack/data_pack.py +241 -0
- amulet/level/java/_raw/_data_pack/data_pack.pyi +197 -0
- amulet/level/java/_raw/_data_pack/data_pack_manager.py +77 -0
- amulet/level/java/_raw/_data_pack/data_pack_manager.pyi +75 -0
- amulet/level/java/_raw/_dimension.py +86 -0
- amulet/level/java/_raw/_dimension.pyi +72 -0
- amulet/level/java/_raw/_level.py +507 -0
- amulet/level/java/_raw/_level.pyi +238 -0
- amulet/level/java/_raw/_typing.py +3 -0
- amulet/level/java/_raw/_typing.pyi +5 -0
- amulet/level/java/anvil/__init__.py +2 -0
- amulet/level/java/anvil/__init__.pyi +11 -0
- amulet/level/java/anvil/_dimension.py +170 -0
- amulet/level/java/anvil/_dimension.pyi +109 -0
- amulet/level/java/anvil/_region.py +421 -0
- amulet/level/java/anvil/_region.pyi +197 -0
- amulet/level/java/anvil/_sector_manager.py +223 -0
- amulet/level/java/anvil/_sector_manager.pyi +142 -0
- amulet/level/java/chunk.pyi +81 -0
- amulet/level/java/chunk_/_chunk.py +260 -0
- amulet/level/java/chunk_/components/inhabited_time.py +12 -0
- amulet/level/java/chunk_/components/last_update.py +12 -0
- amulet/level/java/chunk_/components/legacy_version.py +12 -0
- amulet/level/java/chunk_/components/light_populated.py +12 -0
- amulet/level/java/chunk_/components/named_height_2d.py +37 -0
- amulet/level/java/chunk_/components/status.py +11 -0
- amulet/level/java/chunk_/components/terrain_populated.py +12 -0
- amulet/level/java/chunk_components.pyi +22 -0
- amulet/level/java/long_array.pyi +38 -0
- amulet/level/java_forge/__init__.py +0 -0
- amulet/level/mcstructure/__init__.py +0 -0
- amulet/level/nbt/__init__.py +0 -0
- amulet/level/schematic/__init__.py +0 -0
- amulet/level/sponge_schematic/__init__.py +0 -0
- amulet/level/temporary_level/__init__.py +1 -0
- amulet/level/temporary_level/_level.py +16 -0
- amulet/palette/__init__.pyi +8 -0
- amulet/palette/biome_palette.pyi +45 -0
- amulet/palette/block_palette.pyi +45 -0
- amulet/player.py +64 -0
- amulet/py.typed +0 -0
- amulet/selection/__init__.py +2 -0
- amulet/selection/abstract_selection.py +342 -0
- amulet/selection/box.py +852 -0
- amulet/selection/group.py +481 -0
- amulet/utils/__init__.pyi +28 -0
- amulet/utils/call_spec/__init__.py +24 -0
- amulet/utils/call_spec/__init__.pyi +53 -0
- amulet/utils/call_spec/_call_spec.py +262 -0
- amulet/utils/call_spec/_call_spec.pyi +272 -0
- amulet/utils/format_utils.py +41 -0
- amulet/utils/generator.py +18 -0
- amulet/utils/matrix.py +243 -0
- amulet/utils/matrix.pyi +177 -0
- amulet/utils/numpy.pyi +11 -0
- amulet/utils/numpy_helpers.py +19 -0
- amulet/utils/shareable_lock.py +335 -0
- amulet/utils/shareable_lock.pyi +190 -0
- amulet/utils/signal/__init__.py +10 -0
- amulet/utils/signal/__init__.pyi +25 -0
- amulet/utils/signal/_signal.py +228 -0
- amulet/utils/signal/_signal.pyi +84 -0
- amulet/utils/task_manager.py +235 -0
- amulet/utils/task_manager.pyi +168 -0
- amulet/utils/typed_property.py +111 -0
- amulet/utils/typing.py +4 -0
- amulet/utils/typing.pyi +6 -0
- amulet/utils/weakref.py +70 -0
- amulet/utils/weakref.pyi +50 -0
- amulet/utils/world_utils.py +102 -0
- amulet/utils/world_utils.pyi +109 -0
- amulet/version.cpp +136 -0
- amulet/version.hpp +142 -0
- amulet/version.pyi +94 -0
- amulet_core-2.0a5.dist-info/METADATA +103 -0
- amulet_core-2.0a5.dist-info/RECORD +210 -0
- amulet_core-2.0a5.dist-info/WHEEL +5 -0
- amulet_core-2.0a5.dist-info/entry_points.txt +2 -0
- 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)>
|