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.
- amulet/__init__.py +27 -27
- amulet/__pyinstaller/__init__.py +2 -2
- amulet/__pyinstaller/hook-amulet.py +4 -4
- amulet/_version.py +21 -21
- amulet/api/__init__.py +2 -2
- amulet/api/abstract_base_entity.py +128 -128
- amulet/api/block.py +630 -630
- amulet/api/block_entity.py +71 -71
- amulet/api/cache.py +107 -107
- amulet/api/chunk/__init__.py +6 -6
- amulet/api/chunk/biomes.py +207 -207
- amulet/api/chunk/block_entity_dict.py +175 -175
- amulet/api/chunk/blocks.py +46 -46
- amulet/api/chunk/chunk.py +389 -389
- amulet/api/chunk/entity_list.py +75 -75
- amulet/api/chunk/status.py +167 -167
- amulet/api/data_types/__init__.py +4 -4
- amulet/api/data_types/generic_types.py +4 -4
- amulet/api/data_types/operation_types.py +16 -16
- amulet/api/data_types/world_types.py +49 -49
- amulet/api/data_types/wrapper_types.py +71 -71
- amulet/api/entity.py +74 -74
- amulet/api/errors.py +119 -119
- amulet/api/history/__init__.py +36 -36
- amulet/api/history/base/__init__.py +3 -3
- amulet/api/history/base/base_history.py +26 -26
- amulet/api/history/base/history_manager.py +63 -63
- amulet/api/history/base/revision_manager.py +73 -73
- amulet/api/history/changeable.py +15 -15
- amulet/api/history/data_types.py +7 -7
- amulet/api/history/history_manager/__init__.py +3 -3
- amulet/api/history/history_manager/container.py +102 -102
- amulet/api/history/history_manager/database.py +279 -279
- amulet/api/history/history_manager/meta.py +93 -93
- amulet/api/history/history_manager/object.py +116 -116
- amulet/api/history/revision_manager/__init__.py +2 -2
- amulet/api/history/revision_manager/disk.py +33 -33
- amulet/api/history/revision_manager/ram.py +12 -12
- amulet/api/item.py +75 -75
- amulet/api/level/__init__.py +4 -4
- amulet/api/level/base_level/__init__.py +1 -1
- amulet/api/level/base_level/base_level.py +1035 -1026
- amulet/api/level/base_level/chunk_manager.py +227 -227
- amulet/api/level/base_level/clone.py +389 -389
- amulet/api/level/base_level/player_manager.py +101 -101
- amulet/api/level/immutable_structure/__init__.py +1 -1
- amulet/api/level/immutable_structure/immutable_structure.py +94 -94
- amulet/api/level/immutable_structure/void_format_wrapper.py +117 -117
- amulet/api/level/structure.py +22 -22
- amulet/api/level/world.py +19 -19
- amulet/api/partial_3d_array/__init__.py +2 -2
- amulet/api/partial_3d_array/base_partial_3d_array.py +263 -263
- amulet/api/partial_3d_array/bounded_partial_3d_array.py +528 -528
- amulet/api/partial_3d_array/data_types.py +15 -15
- amulet/api/partial_3d_array/unbounded_partial_3d_array.py +229 -229
- amulet/api/partial_3d_array/util.py +152 -152
- amulet/api/player.py +65 -65
- amulet/api/registry/__init__.py +2 -2
- amulet/api/registry/base_registry.py +34 -34
- amulet/api/registry/biome_manager.py +153 -153
- amulet/api/registry/block_manager.py +156 -156
- amulet/api/selection/__init__.py +2 -2
- amulet/api/selection/abstract_selection.py +315 -315
- amulet/api/selection/box.py +805 -805
- amulet/api/selection/group.py +488 -488
- amulet/api/structure.py +37 -37
- amulet/api/wrapper/__init__.py +8 -8
- amulet/api/wrapper/chunk/interface.py +441 -441
- amulet/api/wrapper/chunk/translator.py +567 -567
- amulet/api/wrapper/format_wrapper.py +772 -772
- amulet/api/wrapper/structure_format_wrapper.py +116 -116
- amulet/api/wrapper/world_format_wrapper.py +63 -63
- amulet/level/__init__.py +1 -1
- amulet/level/formats/anvil_forge_world.py +40 -40
- amulet/level/formats/anvil_world/__init__.py +3 -3
- amulet/level/formats/anvil_world/_sector_manager.py +291 -384
- amulet/level/formats/anvil_world/data_pack/__init__.py +2 -2
- amulet/level/formats/anvil_world/data_pack/data_pack.py +224 -224
- amulet/level/formats/anvil_world/data_pack/data_pack_manager.py +77 -77
- amulet/level/formats/anvil_world/dimension.py +177 -177
- amulet/level/formats/anvil_world/format.py +769 -769
- amulet/level/formats/anvil_world/region.py +384 -384
- amulet/level/formats/construction/__init__.py +3 -3
- amulet/level/formats/construction/format_wrapper.py +515 -515
- amulet/level/formats/construction/interface.py +134 -134
- amulet/level/formats/construction/section.py +60 -60
- amulet/level/formats/construction/util.py +165 -165
- amulet/level/formats/leveldb_world/__init__.py +3 -3
- amulet/level/formats/leveldb_world/chunk.py +33 -33
- amulet/level/formats/leveldb_world/dimension.py +385 -419
- amulet/level/formats/leveldb_world/format.py +659 -641
- amulet/level/formats/leveldb_world/interface/chunk/__init__.py +36 -36
- amulet/level/formats/leveldb_world/interface/chunk/base_leveldb_interface.py +836 -836
- amulet/level/formats/leveldb_world/interface/chunk/generate_interface.py +31 -31
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_0.py +30 -30
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_1.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_10.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_11.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_12.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_13.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_14.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_15.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_16.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_17.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_18.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_19.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_2.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_20.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_21.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_22.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_23.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_24.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_25.py +24 -24
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_26.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_27.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_28.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_29.py +33 -33
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_3.py +57 -57
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_30.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_31.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_32.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_33.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_34.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_35.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_36.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_37.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_38.py +10 -10
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_39.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_4.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_40.py +16 -16
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_5.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_6.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_7.py +12 -12
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_8.py +180 -180
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_9.py +18 -18
- amulet/level/formats/leveldb_world/interface/chunk/leveldb_chunk_versions.py +79 -79
- amulet/level/formats/mcstructure/__init__.py +3 -3
- amulet/level/formats/mcstructure/chunk.py +50 -50
- amulet/level/formats/mcstructure/format_wrapper.py +408 -408
- amulet/level/formats/mcstructure/interface.py +175 -175
- amulet/level/formats/schematic/__init__.py +3 -3
- amulet/level/formats/schematic/chunk.py +55 -55
- amulet/level/formats/schematic/data_types.py +4 -4
- amulet/level/formats/schematic/format_wrapper.py +373 -373
- amulet/level/formats/schematic/interface.py +142 -142
- amulet/level/formats/sponge_schem/__init__.py +4 -4
- amulet/level/formats/sponge_schem/chunk.py +62 -62
- amulet/level/formats/sponge_schem/format_wrapper.py +463 -463
- amulet/level/formats/sponge_schem/interface.py +118 -118
- amulet/level/formats/sponge_schem/varint/__init__.py +1 -1
- amulet/level/formats/sponge_schem/varint/varint.py +87 -87
- amulet/level/interfaces/chunk/anvil/anvil_0.py +72 -72
- amulet/level/interfaces/chunk/anvil/anvil_1444.py +336 -336
- amulet/level/interfaces/chunk/anvil/anvil_1466.py +94 -94
- amulet/level/interfaces/chunk/anvil/anvil_1467.py +37 -37
- amulet/level/interfaces/chunk/anvil/anvil_1484.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1503.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1519.py +34 -34
- amulet/level/interfaces/chunk/anvil/anvil_1901.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1908.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_1912.py +21 -21
- amulet/level/interfaces/chunk/anvil/anvil_1934.py +20 -20
- amulet/level/interfaces/chunk/anvil/anvil_2203.py +69 -69
- amulet/level/interfaces/chunk/anvil/anvil_2529.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2681.py +76 -76
- amulet/level/interfaces/chunk/anvil/anvil_2709.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_2844.py +267 -267
- amulet/level/interfaces/chunk/anvil/anvil_3463.py +19 -19
- amulet/level/interfaces/chunk/anvil/anvil_na.py +607 -607
- amulet/level/interfaces/chunk/anvil/base_anvil_interface.py +326 -326
- amulet/level/load.py +59 -59
- amulet/level/loader.py +95 -95
- amulet/level/translators/chunk/bedrock/__init__.py +267 -267
- amulet/level/translators/chunk/bedrock/bedrock_nbt_blockstate_translator.py +46 -46
- amulet/level/translators/chunk/bedrock/bedrock_numerical_translator.py +39 -39
- amulet/level/translators/chunk/bedrock/bedrock_psudo_numerical_translator.py +37 -37
- amulet/level/translators/chunk/java/java_1_18_translator.py +40 -40
- amulet/level/translators/chunk/java/java_blockstate_translator.py +94 -94
- amulet/level/translators/chunk/java/java_numerical_translator.py +62 -62
- amulet/libs/leveldb/__init__.py +7 -7
- amulet/operations/__init__.py +5 -5
- amulet/operations/clone.py +18 -18
- amulet/operations/delete_chunk.py +32 -32
- amulet/operations/fill.py +30 -30
- amulet/operations/paste.py +65 -65
- amulet/operations/replace.py +58 -58
- amulet/utils/__init__.py +14 -14
- amulet/utils/format_utils.py +41 -41
- amulet/utils/generator.py +15 -15
- amulet/utils/matrix.py +243 -243
- amulet/utils/numpy_helpers.py +46 -46
- amulet/utils/world_utils.py +349 -349
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/METADATA +97 -97
- amulet_core-1.9.20.dist-info/RECORD +208 -0
- amulet_core-1.9.19.dist-info/RECORD +0 -208
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/WHEEL +0 -0
- {amulet_core-1.9.19.dist-info → amulet_core-1.9.20.dist-info}/entry_points.txt +0 -0
- {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
|