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,772 +1,772 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import (
|
|
5
|
-
Tuple,
|
|
6
|
-
Any,
|
|
7
|
-
Generator,
|
|
8
|
-
Dict,
|
|
9
|
-
List,
|
|
10
|
-
Optional,
|
|
11
|
-
TYPE_CHECKING,
|
|
12
|
-
Iterable,
|
|
13
|
-
Callable,
|
|
14
|
-
Type,
|
|
15
|
-
Union,
|
|
16
|
-
TypeVar,
|
|
17
|
-
Generic,
|
|
18
|
-
)
|
|
19
|
-
import copy
|
|
20
|
-
import numpy
|
|
21
|
-
import os
|
|
22
|
-
import warnings
|
|
23
|
-
import logging
|
|
24
|
-
|
|
25
|
-
import PyMCTranslate
|
|
26
|
-
|
|
27
|
-
from amulet.api import level as api_level, wrapper as api_wrapper
|
|
28
|
-
from amulet.api.chunk import Chunk
|
|
29
|
-
from amulet.api.registry import BlockManager
|
|
30
|
-
from amulet.api.errors import (
|
|
31
|
-
ChunkLoadError,
|
|
32
|
-
ChunkDoesNotExist,
|
|
33
|
-
ObjectReadError,
|
|
34
|
-
ObjectReadWriteError,
|
|
35
|
-
PlayerDoesNotExist,
|
|
36
|
-
PlayerLoadError,
|
|
37
|
-
EntryLoadError,
|
|
38
|
-
EntryDoesNotExist,
|
|
39
|
-
DimensionDoesNotExist,
|
|
40
|
-
)
|
|
41
|
-
from amulet.api.data_types import (
|
|
42
|
-
AnyNDArray,
|
|
43
|
-
VersionNumberAny,
|
|
44
|
-
ChunkCoordinates,
|
|
45
|
-
Dimension,
|
|
46
|
-
PlatformType,
|
|
47
|
-
)
|
|
48
|
-
from amulet.api.selection import SelectionGroup, SelectionBox
|
|
49
|
-
from amulet.api.player import Player
|
|
50
|
-
|
|
51
|
-
if TYPE_CHECKING:
|
|
52
|
-
from amulet.api.wrapper.chunk.translator import Translator
|
|
53
|
-
|
|
54
|
-
log = logging.getLogger(__name__)
|
|
55
|
-
|
|
56
|
-
DefaultSelection = SelectionGroup(
|
|
57
|
-
SelectionBox((-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000))
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
VersionNumberT = TypeVar("VersionNumberT", int, Tuple[int, ...])
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class FormatWrapper(Generic[VersionNumberT], ABC):
|
|
64
|
-
"""
|
|
65
|
-
The FormatWrapper class is a class that sits between the serialised world or structure data and the program using amulet-core.
|
|
66
|
-
|
|
67
|
-
It is used to access data from the serialised source in the universal format and write them back again.
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
_platform: Optional[PlatformType]
|
|
71
|
-
_version: Optional[VersionNumberT]
|
|
72
|
-
|
|
73
|
-
def __init__(self, path: str):
|
|
74
|
-
"""
|
|
75
|
-
Construct a new instance of :class:`FormatWrapper`.
|
|
76
|
-
|
|
77
|
-
This should not be used directly. You should instead use :func:`amulet.load_format`.
|
|
78
|
-
|
|
79
|
-
:param path: The file path to the serialised data.
|
|
80
|
-
"""
|
|
81
|
-
self._path = path
|
|
82
|
-
self._is_open = False
|
|
83
|
-
self._has_lock = False
|
|
84
|
-
self._translation_manager = None
|
|
85
|
-
self._platform = None
|
|
86
|
-
self._version = None
|
|
87
|
-
self._bounds: Dict[Dimension, SelectionGroup] = {}
|
|
88
|
-
self._changed: bool = False
|
|
89
|
-
|
|
90
|
-
@property
|
|
91
|
-
def sub_chunk_size(self) -> int:
|
|
92
|
-
"""
|
|
93
|
-
The dimensions of a sub-chunk.
|
|
94
|
-
"""
|
|
95
|
-
return 16
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def path(self) -> str:
|
|
99
|
-
"""The path to the data on disk."""
|
|
100
|
-
return self._path
|
|
101
|
-
|
|
102
|
-
@property
|
|
103
|
-
@abstractmethod
|
|
104
|
-
def level_name(self) -> str:
|
|
105
|
-
"""The name of the level."""
|
|
106
|
-
raise NotImplementedError
|
|
107
|
-
|
|
108
|
-
@property
|
|
109
|
-
def translation_manager(self) -> PyMCTranslate.TranslationManager:
|
|
110
|
-
"""The translation manager attached to the world."""
|
|
111
|
-
if self._translation_manager is None:
|
|
112
|
-
self._translation_manager = PyMCTranslate.new_translation_manager()
|
|
113
|
-
return self._translation_manager
|
|
114
|
-
|
|
115
|
-
@translation_manager.setter
|
|
116
|
-
def translation_manager(self, value: PyMCTranslate.TranslationManager):
|
|
117
|
-
# TODO: this should not be settable.
|
|
118
|
-
self._translation_manager = value
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
def exists(self) -> bool:
|
|
122
|
-
"""Does some data exist at the specified path."""
|
|
123
|
-
return os.path.exists(self.path)
|
|
124
|
-
|
|
125
|
-
@staticmethod
|
|
126
|
-
@abstractmethod
|
|
127
|
-
def is_valid(path: str) -> bool:
|
|
128
|
-
"""
|
|
129
|
-
Returns whether this format wrapper is able to load the given data.
|
|
130
|
-
|
|
131
|
-
:param path: The path of the data to load.
|
|
132
|
-
:return: True if the world can be loaded by this format wrapper, False otherwise.
|
|
133
|
-
"""
|
|
134
|
-
raise NotImplementedError
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
@abstractmethod
|
|
138
|
-
def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
|
|
139
|
-
"""
|
|
140
|
-
The valid platform and version combinations that this object can accept.
|
|
141
|
-
|
|
142
|
-
This is used when setting the platform and version in the create_and_open method
|
|
143
|
-
to verify that the platform and version are valid.
|
|
144
|
-
|
|
145
|
-
:return: A dictionary mapping the platform to a tuple of two booleans to determine if numerical and blockstate are valid respectively.
|
|
146
|
-
"""
|
|
147
|
-
raise NotImplementedError
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
def platform(self) -> PlatformType:
|
|
151
|
-
"""Platform string the data is stored in (eg "bedrock" / "java" / ...)"""
|
|
152
|
-
if self._platform is None:
|
|
153
|
-
raise Exception(
|
|
154
|
-
"Cannot access the game platform until the level has been loaded."
|
|
155
|
-
)
|
|
156
|
-
return self._platform
|
|
157
|
-
|
|
158
|
-
@property
|
|
159
|
-
def version(self) -> VersionNumberT:
|
|
160
|
-
"""The version number for the given platform the data is stored in eg (1, 16, 2)"""
|
|
161
|
-
if self._version is None:
|
|
162
|
-
raise Exception(
|
|
163
|
-
"Cannot access the game version until the level has been loaded."
|
|
164
|
-
)
|
|
165
|
-
return self._version
|
|
166
|
-
|
|
167
|
-
@property
|
|
168
|
-
def max_world_version(self) -> Tuple[PlatformType, VersionNumberT]:
|
|
169
|
-
"""
|
|
170
|
-
The version the world was last opened in.
|
|
171
|
-
|
|
172
|
-
This should be greater than or equal to the chunk versions found within.
|
|
173
|
-
"""
|
|
174
|
-
return self.platform, self.version
|
|
175
|
-
|
|
176
|
-
@property
|
|
177
|
-
def changed(self) -> bool:
|
|
178
|
-
"""Has any data been pushed to the format wrapper that has not been saved to disk."""
|
|
179
|
-
return self._changed
|
|
180
|
-
|
|
181
|
-
@property
|
|
182
|
-
@abstractmethod
|
|
183
|
-
def dimensions(self) -> List[Dimension]:
|
|
184
|
-
"""A list of all the dimensions contained in the world."""
|
|
185
|
-
raise NotImplementedError
|
|
186
|
-
|
|
187
|
-
@property
|
|
188
|
-
@abstractmethod
|
|
189
|
-
def can_add_dimension(self) -> bool:
|
|
190
|
-
"""
|
|
191
|
-
Can external code register a new dimension.
|
|
192
|
-
|
|
193
|
-
If False :meth:`register_dimension` will have no effect.
|
|
194
|
-
"""
|
|
195
|
-
raise NotImplementedError
|
|
196
|
-
|
|
197
|
-
@abstractmethod
|
|
198
|
-
def register_dimension(self, dimension_identifier: Any):
|
|
199
|
-
"""
|
|
200
|
-
Register a new dimension.
|
|
201
|
-
|
|
202
|
-
:param dimension_identifier: The identifier for the dimension.
|
|
203
|
-
"""
|
|
204
|
-
raise NotImplementedError
|
|
205
|
-
|
|
206
|
-
@property
|
|
207
|
-
def requires_selection(self) -> bool:
|
|
208
|
-
"""Does this object require that a selection be defined when creating it from scratch?"""
|
|
209
|
-
return False
|
|
210
|
-
|
|
211
|
-
@property
|
|
212
|
-
def multi_selection(self) -> bool:
|
|
213
|
-
"""
|
|
214
|
-
Does this object support having multiple selection boxes.
|
|
215
|
-
|
|
216
|
-
If False it will be given exactly 1 selection.
|
|
217
|
-
|
|
218
|
-
If True can be given 0 or more.
|
|
219
|
-
"""
|
|
220
|
-
return False
|
|
221
|
-
|
|
222
|
-
@property
|
|
223
|
-
def selection(self) -> SelectionGroup:
|
|
224
|
-
"""The area that all chunk data must fit within."""
|
|
225
|
-
warnings.warn(
|
|
226
|
-
"FormatWrapper.selection is depreciated and will be removed in the future. Please use FormatWrapper.bounds(dimension) instead",
|
|
227
|
-
DeprecationWarning,
|
|
228
|
-
)
|
|
229
|
-
return self.bounds(self.dimensions[0])
|
|
230
|
-
|
|
231
|
-
def bounds(self, dimension: Dimension) -> SelectionGroup:
|
|
232
|
-
if dimension not in self._bounds:
|
|
233
|
-
if dimension in self.dimensions:
|
|
234
|
-
raise Exception(
|
|
235
|
-
f'The dimension exists but there is no selection registered for it. Please report this to a developer "{dimension}" {self}'
|
|
236
|
-
)
|
|
237
|
-
else:
|
|
238
|
-
raise DimensionDoesNotExist
|
|
239
|
-
return self._bounds[dimension]
|
|
240
|
-
|
|
241
|
-
@abstractmethod
|
|
242
|
-
def _get_interface(
|
|
243
|
-
self, raw_chunk_data: Optional[Any] = None
|
|
244
|
-
) -> api_wrapper.Interface:
|
|
245
|
-
raise NotImplementedError
|
|
246
|
-
|
|
247
|
-
def _get_interface_and_translator(
|
|
248
|
-
self, raw_chunk_data=None
|
|
249
|
-
) -> Tuple[api_wrapper.Interface, "Translator", VersionNumberAny]:
|
|
250
|
-
interface = self._get_interface(raw_chunk_data)
|
|
251
|
-
translator, version_identifier = interface.get_translator(
|
|
252
|
-
self.max_world_version, raw_chunk_data
|
|
253
|
-
)
|
|
254
|
-
return interface, translator, version_identifier
|
|
255
|
-
|
|
256
|
-
def create_and_open(
|
|
257
|
-
self,
|
|
258
|
-
platform: PlatformType,
|
|
259
|
-
version: VersionNumberAny,
|
|
260
|
-
bounds: Union[
|
|
261
|
-
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
262
|
-
] = None,
|
|
263
|
-
overwrite: bool = False,
|
|
264
|
-
**kwargs,
|
|
265
|
-
):
|
|
266
|
-
"""
|
|
267
|
-
Remove the data at the path and set up a new database.
|
|
268
|
-
|
|
269
|
-
You might want to call :attr:`exists` to check if something exists at the path
|
|
270
|
-
and warn the user they are going to overwrite existing data before calling this method.
|
|
271
|
-
|
|
272
|
-
:param platform: The platform the data should use.
|
|
273
|
-
:param version: The version the data should use.
|
|
274
|
-
:param bounds: The bounds for each dimension. If one :class:`SelectionGroup` is given it will be applied to all dimensions.
|
|
275
|
-
:param overwrite: Should an existing database be overwritten. If this is False and one exists and error will be thrown.
|
|
276
|
-
:param kwargs: Extra arguments as each implementation requires.
|
|
277
|
-
:return:
|
|
278
|
-
"""
|
|
279
|
-
if self.is_open:
|
|
280
|
-
raise ObjectReadError(f"Cannot open {self} because it was already opened.")
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
platform not in self.valid_formats or len(self.valid_formats[platform]) < 2
|
|
284
|
-
): # check that the platform and version are valid
|
|
285
|
-
raise ObjectReadError(
|
|
286
|
-
f"{platform} is not a valid platform for this wrapper."
|
|
287
|
-
)
|
|
288
|
-
translator_version = self.translation_manager.get_version(platform, version)
|
|
289
|
-
if translator_version.has_abstract_format: # numerical
|
|
290
|
-
if not self.valid_formats[platform][0]:
|
|
291
|
-
raise ObjectReadError(
|
|
292
|
-
f"The version given ({version}) is from the numerical format but this wrapper does not support the numerical format."
|
|
293
|
-
)
|
|
294
|
-
else:
|
|
295
|
-
if not self.valid_formats[platform][1]:
|
|
296
|
-
raise ObjectReadError(
|
|
297
|
-
f"The version given ({version}) is from the blockstate format but this wrapper does not support the blockstate format."
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
self._platform = translator_version.platform
|
|
301
|
-
self._version = translator_version.version_number
|
|
302
|
-
self._create(overwrite, bounds, **kwargs)
|
|
303
|
-
|
|
304
|
-
def _clean_selection(self, selection: SelectionGroup) -> SelectionGroup:
|
|
305
|
-
if self.multi_selection:
|
|
306
|
-
return selection
|
|
307
|
-
else:
|
|
308
|
-
if selection:
|
|
309
|
-
return SelectionGroup(
|
|
310
|
-
sorted(
|
|
311
|
-
selection.selection_boxes,
|
|
312
|
-
reverse=True,
|
|
313
|
-
key=lambda b: b.volume,
|
|
314
|
-
)[0]
|
|
315
|
-
)
|
|
316
|
-
else:
|
|
317
|
-
raise ObjectReadError(
|
|
318
|
-
"A single selection was required but none were given."
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
@abstractmethod
|
|
322
|
-
def _create(
|
|
323
|
-
self,
|
|
324
|
-
overwrite: bool,
|
|
325
|
-
bounds: Union[
|
|
326
|
-
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
327
|
-
] = None,
|
|
328
|
-
**kwargs,
|
|
329
|
-
):
|
|
330
|
-
"""Set up the database from scratch."""
|
|
331
|
-
raise NotImplementedError
|
|
332
|
-
|
|
333
|
-
def open(self):
|
|
334
|
-
"""Open the database for reading and writing."""
|
|
335
|
-
if self.is_open:
|
|
336
|
-
raise ObjectReadError(f"Cannot open {self} because it was already opened.")
|
|
337
|
-
self._open()
|
|
338
|
-
|
|
339
|
-
@abstractmethod
|
|
340
|
-
def _open(self):
|
|
341
|
-
raise NotImplementedError
|
|
342
|
-
|
|
343
|
-
@property
|
|
344
|
-
def is_open(self) -> bool:
|
|
345
|
-
"""Has the object been opened."""
|
|
346
|
-
return self._is_open
|
|
347
|
-
|
|
348
|
-
@property
|
|
349
|
-
def has_lock(self) -> bool:
|
|
350
|
-
"""Verify that the world database can be read and written"""
|
|
351
|
-
return self._has_lock
|
|
352
|
-
|
|
353
|
-
def _verify_has_lock(self):
|
|
354
|
-
"""
|
|
355
|
-
Ensure that the FormatWrapper is open and has a lock on the object.
|
|
356
|
-
|
|
357
|
-
:raises:
|
|
358
|
-
ObjectReadWriteError: if the FormatWrapper does not have a lock on the object.
|
|
359
|
-
"""
|
|
360
|
-
if not self.is_open:
|
|
361
|
-
raise ObjectReadWriteError(
|
|
362
|
-
f"The object {self} was never opened. Call .open or .create_and_open to open it before accessing data."
|
|
363
|
-
)
|
|
364
|
-
elif not self.has_lock:
|
|
365
|
-
raise ObjectReadWriteError(
|
|
366
|
-
f"The lock on the object {self} has been lost. It was probably opened somewhere else."
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
@staticmethod
|
|
370
|
-
def pre_save_operation(level: api_level.BaseLevel) -> Generator[float, None, bool]:
|
|
371
|
-
"""
|
|
372
|
-
Logic to run before saving. Eg recalculating height maps or lighting.
|
|
373
|
-
|
|
374
|
-
Must be a generator that yields a number and returns a bool.
|
|
375
|
-
|
|
376
|
-
The yielded number is the progress from 0 to 1.
|
|
377
|
-
|
|
378
|
-
The returned bool is if changes have been made.
|
|
379
|
-
|
|
380
|
-
:param level: The level to apply modifications to.
|
|
381
|
-
:return: Have any modifications been made.
|
|
382
|
-
"""
|
|
383
|
-
yield 1
|
|
384
|
-
return False
|
|
385
|
-
|
|
386
|
-
def save(self):
|
|
387
|
-
"""Save the data back to the level."""
|
|
388
|
-
self._verify_has_lock()
|
|
389
|
-
self._save()
|
|
390
|
-
self._changed = False
|
|
391
|
-
|
|
392
|
-
@abstractmethod
|
|
393
|
-
def _save(self):
|
|
394
|
-
raise NotImplementedError
|
|
395
|
-
|
|
396
|
-
def close(self):
|
|
397
|
-
"""Close the level."""
|
|
398
|
-
if self.is_open:
|
|
399
|
-
self._is_open = False
|
|
400
|
-
self._has_lock = False
|
|
401
|
-
self._close()
|
|
402
|
-
|
|
403
|
-
@abstractmethod
|
|
404
|
-
def _close(self):
|
|
405
|
-
raise NotImplementedError
|
|
406
|
-
|
|
407
|
-
@abstractmethod
|
|
408
|
-
def unload(self):
|
|
409
|
-
"""Unload data stored in the FormatWrapper class"""
|
|
410
|
-
raise NotImplementedError
|
|
411
|
-
|
|
412
|
-
@abstractmethod
|
|
413
|
-
def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
|
|
414
|
-
"""A generator of all chunk coords in the given dimension."""
|
|
415
|
-
raise NotImplementedError
|
|
416
|
-
|
|
417
|
-
@abstractmethod
|
|
418
|
-
def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
|
|
419
|
-
"""
|
|
420
|
-
Does the chunk exist in the world database?
|
|
421
|
-
|
|
422
|
-
:param cx: The x coordinate of the chunk.
|
|
423
|
-
:param cz: The z coordinate of the chunk.
|
|
424
|
-
:param dimension: The dimension to load the chunk from.
|
|
425
|
-
:return: True if the chunk exists. Calling load_chunk on this chunk may still throw ChunkLoadError
|
|
426
|
-
"""
|
|
427
|
-
raise NotImplementedError
|
|
428
|
-
|
|
429
|
-
def _safe_load(
|
|
430
|
-
self,
|
|
431
|
-
meth: Callable,
|
|
432
|
-
args: Tuple[Any, ...],
|
|
433
|
-
msg: str,
|
|
434
|
-
load_error: Type[EntryLoadError],
|
|
435
|
-
does_not_exist_error: Type[EntryDoesNotExist],
|
|
436
|
-
):
|
|
437
|
-
try:
|
|
438
|
-
self._verify_has_lock()
|
|
439
|
-
except ObjectReadWriteError as e:
|
|
440
|
-
raise does_not_exist_error(e)
|
|
441
|
-
try:
|
|
442
|
-
return meth(*args)
|
|
443
|
-
except does_not_exist_error as e:
|
|
444
|
-
raise e
|
|
445
|
-
except Exception as e:
|
|
446
|
-
log.error(msg.format(*args), exc_info=True)
|
|
447
|
-
raise load_error(e) from e
|
|
448
|
-
|
|
449
|
-
def load_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
|
|
450
|
-
"""
|
|
451
|
-
Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
|
|
452
|
-
|
|
453
|
-
:param cx: The x coordinate of the chunk.
|
|
454
|
-
:param cz: The z coordinate of the chunk.
|
|
455
|
-
:param dimension: The dimension to load the chunk from.
|
|
456
|
-
:return: The chunk at the given coordinates.
|
|
457
|
-
:raises:
|
|
458
|
-
ChunkDoesNotExist: If the chunk does not exist (was deleted or never created)
|
|
459
|
-
ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
|
|
460
|
-
"""
|
|
461
|
-
return self._safe_load(
|
|
462
|
-
self._load_chunk,
|
|
463
|
-
(cx, cz, dimension),
|
|
464
|
-
"Error loading chunk {} {} {}",
|
|
465
|
-
ChunkLoadError,
|
|
466
|
-
ChunkDoesNotExist,
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
def _load_chunk(
|
|
470
|
-
self, cx: int, cz: int, dimension: Dimension, recurse: bool = True
|
|
471
|
-
) -> Chunk:
|
|
472
|
-
"""
|
|
473
|
-
Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
|
|
474
|
-
|
|
475
|
-
:param cx: The x coordinate of the chunk.
|
|
476
|
-
:param cz: The z coordinate of the chunk.
|
|
477
|
-
:param dimension: The dimension to load the chunk from.
|
|
478
|
-
:param recurse: bool: look in boundary chunks if required to fully define data
|
|
479
|
-
:return: The chunk at the given coordinates.
|
|
480
|
-
"""
|
|
481
|
-
|
|
482
|
-
# Gets an interface (the code that actually reads the chunk data)
|
|
483
|
-
raw_chunk_data = self._get_raw_chunk_data(cx, cz, dimension)
|
|
484
|
-
interface, translator, game_version = self._get_interface_and_translator(
|
|
485
|
-
raw_chunk_data
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
# decode the raw chunk data into the universal format
|
|
489
|
-
chunk, block_palette = self._decode(
|
|
490
|
-
interface, dimension, cx, cz, raw_chunk_data
|
|
491
|
-
)
|
|
492
|
-
block_palette: AnyNDArray
|
|
493
|
-
chunk = self._unpack(translator, game_version, chunk, block_palette)
|
|
494
|
-
return self._convert_to_load(
|
|
495
|
-
chunk, translator, game_version, dimension, recurse=recurse
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
@staticmethod
|
|
499
|
-
def _decode(
|
|
500
|
-
interface: api_wrapper.Interface,
|
|
501
|
-
dimension: Dimension,
|
|
502
|
-
cx: int,
|
|
503
|
-
cz: int,
|
|
504
|
-
raw_chunk_data: Any,
|
|
505
|
-
) -> Tuple[Chunk, AnyNDArray]:
|
|
506
|
-
return interface.decode(cx, cz, raw_chunk_data)
|
|
507
|
-
|
|
508
|
-
def _unpack(
|
|
509
|
-
self,
|
|
510
|
-
translator: "Translator",
|
|
511
|
-
game_version: VersionNumberAny,
|
|
512
|
-
chunk: Chunk,
|
|
513
|
-
block_palette: AnyNDArray,
|
|
514
|
-
) -> Chunk:
|
|
515
|
-
return translator.unpack(
|
|
516
|
-
game_version, self.translation_manager, chunk, block_palette
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
def _convert_to_load(
|
|
520
|
-
self,
|
|
521
|
-
chunk: Chunk,
|
|
522
|
-
translator: "Translator",
|
|
523
|
-
game_version: VersionNumberAny,
|
|
524
|
-
dimension: Dimension,
|
|
525
|
-
recurse: bool = True,
|
|
526
|
-
) -> Chunk:
|
|
527
|
-
# set up a callback that translator can use to get chunk data
|
|
528
|
-
cx, cz = chunk.cx, chunk.cz
|
|
529
|
-
if recurse:
|
|
530
|
-
chunk_cache: Dict[ChunkCoordinates, Chunk] = {}
|
|
531
|
-
|
|
532
|
-
def get_chunk_callback(x: int, z: int) -> Chunk:
|
|
533
|
-
cx_, cz_ = cx + x, cz + z
|
|
534
|
-
if (cx_, cz_) not in chunk_cache:
|
|
535
|
-
chunk_cache[(cx_, cz_)] = self._load_chunk(
|
|
536
|
-
cx_, cz_, dimension, recurse=False
|
|
537
|
-
)
|
|
538
|
-
return chunk_cache[(cx_, cz_)]
|
|
539
|
-
|
|
540
|
-
else:
|
|
541
|
-
get_chunk_callback = None
|
|
542
|
-
|
|
543
|
-
# translate the data to universal format
|
|
544
|
-
chunk = translator.to_universal(
|
|
545
|
-
game_version, self.translation_manager, chunk, get_chunk_callback, recurse
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
chunk.changed = False
|
|
549
|
-
return chunk
|
|
550
|
-
|
|
551
|
-
def commit_chunk(self, chunk: Chunk, dimension: Dimension):
|
|
552
|
-
"""
|
|
553
|
-
Save a universal format chunk to the FormatWrapper database (not the level database)
|
|
554
|
-
|
|
555
|
-
Call save method to write changed chunks back to the level.
|
|
556
|
-
|
|
557
|
-
:param chunk: The chunk object to translate and save.
|
|
558
|
-
:param dimension: The dimension to commit the chunk to.
|
|
559
|
-
"""
|
|
560
|
-
try:
|
|
561
|
-
self._verify_has_lock()
|
|
562
|
-
except ObjectReadWriteError as e:
|
|
563
|
-
log.error(e)
|
|
564
|
-
try:
|
|
565
|
-
self._commit_chunk(copy.deepcopy(chunk), dimension)
|
|
566
|
-
except Exception:
|
|
567
|
-
log.error(f"Error saving chunk {chunk}", exc_info=True)
|
|
568
|
-
self._changed = True
|
|
569
|
-
|
|
570
|
-
def _commit_chunk(self, chunk: Chunk, dimension: Dimension, recurse: bool = True):
|
|
571
|
-
"""
|
|
572
|
-
Saves a universal :class:`~amulet.api.chunk.Chunk` object.
|
|
573
|
-
|
|
574
|
-
Calls the interface then the translator.
|
|
575
|
-
|
|
576
|
-
It then calls _put_chunk_data to store the data returned by the interface
|
|
577
|
-
"""
|
|
578
|
-
# get the coordinates for later
|
|
579
|
-
cx, cz = chunk.cx, chunk.cz
|
|
580
|
-
|
|
581
|
-
# Gets an interface, translator and most recent chunk version for the game version.
|
|
582
|
-
interface, translator, chunk_version = self._get_interface_and_translator()
|
|
583
|
-
|
|
584
|
-
chunk = self._convert_to_save(chunk, chunk_version, translator, recurse)
|
|
585
|
-
chunk, chunk_palette = self._pack(chunk, translator, chunk_version)
|
|
586
|
-
raw_chunk_data = self._encode(interface, chunk, dimension, chunk_palette)
|
|
587
|
-
|
|
588
|
-
self._put_raw_chunk_data(cx, cz, raw_chunk_data, dimension)
|
|
589
|
-
|
|
590
|
-
def _convert_to_save(
|
|
591
|
-
self,
|
|
592
|
-
chunk: Chunk,
|
|
593
|
-
chunk_version: VersionNumberAny,
|
|
594
|
-
translator: "Translator",
|
|
595
|
-
recurse: bool = True,
|
|
596
|
-
) -> Chunk:
|
|
597
|
-
"""Convert the Chunk in Universal format to a Chunk in the version specific format."""
|
|
598
|
-
# create a new streamlined block block_palette and remap the data
|
|
599
|
-
palette: List[numpy.ndarray] = []
|
|
600
|
-
palette_len = 0
|
|
601
|
-
for cy in chunk.blocks.sub_chunks:
|
|
602
|
-
sub_chunk_palette, sub_chunk = numpy.unique(
|
|
603
|
-
chunk.blocks.get_sub_chunk(cy), return_inverse=True
|
|
604
|
-
)
|
|
605
|
-
chunk.blocks.add_sub_chunk(
|
|
606
|
-
cy, sub_chunk.astype(numpy.uint32).reshape((16, 16, 16)) + palette_len
|
|
607
|
-
)
|
|
608
|
-
palette_len += len(sub_chunk_palette)
|
|
609
|
-
palette.append(sub_chunk_palette)
|
|
610
|
-
|
|
611
|
-
if palette:
|
|
612
|
-
chunk_palette, lut = numpy.unique(
|
|
613
|
-
numpy.concatenate(palette), return_inverse=True
|
|
614
|
-
)
|
|
615
|
-
for cy in chunk.blocks.sub_chunks:
|
|
616
|
-
chunk.blocks.add_sub_chunk(
|
|
617
|
-
cy, lut.astype(numpy.uint32)[chunk.blocks.get_sub_chunk(cy)]
|
|
618
|
-
)
|
|
619
|
-
chunk._block_palette = BlockManager(
|
|
620
|
-
numpy.vectorize(chunk.block_palette.__getitem__)(chunk_palette)
|
|
621
|
-
)
|
|
622
|
-
else:
|
|
623
|
-
chunk._block_palette = BlockManager()
|
|
624
|
-
|
|
625
|
-
def get_chunk_callback(_: int, __: int) -> Chunk:
|
|
626
|
-
# conversion from universal should not require any data outside the block
|
|
627
|
-
return chunk
|
|
628
|
-
|
|
629
|
-
# translate from universal format to version format
|
|
630
|
-
return translator.from_universal(
|
|
631
|
-
chunk_version, self.translation_manager, chunk, get_chunk_callback, recurse
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
def _pack(
|
|
635
|
-
self, chunk: Chunk, translator: "Translator", chunk_version: VersionNumberAny
|
|
636
|
-
) -> Tuple[Chunk, AnyNDArray]:
|
|
637
|
-
"""Pack the chunk data into the format required by the encoder.
|
|
638
|
-
This includes converting the string names to numerical formats for the versions that require it.
|
|
639
|
-
"""
|
|
640
|
-
return translator.pack(chunk_version, self.translation_manager, chunk)
|
|
641
|
-
|
|
642
|
-
def _encode(
|
|
643
|
-
self,
|
|
644
|
-
interface: api_wrapper.Interface,
|
|
645
|
-
chunk: Chunk,
|
|
646
|
-
dimension: Dimension,
|
|
647
|
-
chunk_palette: AnyNDArray,
|
|
648
|
-
) -> Any:
|
|
649
|
-
"""Encode the data to the raw format as saved on disk."""
|
|
650
|
-
raise NotImplementedError
|
|
651
|
-
|
|
652
|
-
def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
653
|
-
"""
|
|
654
|
-
Delete the given chunk from the level.
|
|
655
|
-
|
|
656
|
-
:param cx: The x coordinate of the chunk.
|
|
657
|
-
:param cz: The z coordinate of the chunk.
|
|
658
|
-
:param dimension: The dimension to load the data from.
|
|
659
|
-
"""
|
|
660
|
-
self._delete_chunk(cx, cz, dimension)
|
|
661
|
-
self._changed = True
|
|
662
|
-
|
|
663
|
-
@abstractmethod
|
|
664
|
-
def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
665
|
-
raise NotImplementedError
|
|
666
|
-
|
|
667
|
-
def put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
|
|
668
|
-
"""
|
|
669
|
-
Commit the raw chunk data to the FormatWrapper cache.
|
|
670
|
-
|
|
671
|
-
Call :meth:`save` to push all the cache data to the level.
|
|
672
|
-
|
|
673
|
-
:param cx: The x coordinate of the chunk.
|
|
674
|
-
:param cz: The z coordinate of the chunk.
|
|
675
|
-
:param data: The raw data to commit to the level.
|
|
676
|
-
:param dimension: The dimension to load the data from.
|
|
677
|
-
"""
|
|
678
|
-
self._verify_has_lock()
|
|
679
|
-
self._put_raw_chunk_data(cx, cz, data, dimension)
|
|
680
|
-
|
|
681
|
-
@abstractmethod
|
|
682
|
-
def _put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
|
|
683
|
-
raise NotImplementedError
|
|
684
|
-
|
|
685
|
-
def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
|
|
686
|
-
"""
|
|
687
|
-
Return the raw data as loaded from disk.
|
|
688
|
-
|
|
689
|
-
:param cx: The x coordinate of the chunk.
|
|
690
|
-
:param cz: The z coordinate of the chunk.
|
|
691
|
-
:param dimension: The dimension to load the data from.
|
|
692
|
-
:return: The raw chunk data.
|
|
693
|
-
"""
|
|
694
|
-
return self._safe_load(
|
|
695
|
-
self._get_raw_chunk_data,
|
|
696
|
-
(cx, cz, dimension),
|
|
697
|
-
"Error loading chunk {} {} {}",
|
|
698
|
-
ChunkLoadError,
|
|
699
|
-
ChunkDoesNotExist,
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
@abstractmethod
|
|
703
|
-
def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
|
|
704
|
-
"""
|
|
705
|
-
Return the raw data as loaded from disk.
|
|
706
|
-
|
|
707
|
-
:param cx: The x coordinate of the chunk.
|
|
708
|
-
:param cz: The z coordinate of the chunk.
|
|
709
|
-
:param dimension: The dimension to load the data from.
|
|
710
|
-
:return: The raw chunk data.
|
|
711
|
-
"""
|
|
712
|
-
raise NotImplementedError
|
|
713
|
-
|
|
714
|
-
@abstractmethod
|
|
715
|
-
def all_player_ids(self) -> Iterable[str]:
|
|
716
|
-
"""
|
|
717
|
-
Returns a set of all player ids that are present in the level
|
|
718
|
-
"""
|
|
719
|
-
return NotImplemented
|
|
720
|
-
|
|
721
|
-
@abstractmethod
|
|
722
|
-
def has_player(self, player_id: str) -> bool:
|
|
723
|
-
"""
|
|
724
|
-
Test if a player id is present in the level.
|
|
725
|
-
"""
|
|
726
|
-
return NotImplemented
|
|
727
|
-
|
|
728
|
-
def load_player(self, player_id: str) -> "Player":
|
|
729
|
-
"""
|
|
730
|
-
Gets the :class:`Player` object that belongs to the specified player id
|
|
731
|
-
|
|
732
|
-
If no parameter is supplied, the data of the local player should be returned
|
|
733
|
-
|
|
734
|
-
:param player_id: The desired player id
|
|
735
|
-
:return: A Player instance
|
|
736
|
-
"""
|
|
737
|
-
return self._safe_load(
|
|
738
|
-
self._load_player,
|
|
739
|
-
(player_id,),
|
|
740
|
-
"Error loading player {}",
|
|
741
|
-
PlayerLoadError,
|
|
742
|
-
PlayerDoesNotExist,
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
@abstractmethod
|
|
746
|
-
def _load_player(self, player_id: str) -> "Player":
|
|
747
|
-
"""
|
|
748
|
-
Get the raw player data and unpack it into a Player class.
|
|
749
|
-
|
|
750
|
-
:param player_id: The id of the player to get.
|
|
751
|
-
:return:
|
|
752
|
-
"""
|
|
753
|
-
raise NotImplementedError
|
|
754
|
-
|
|
755
|
-
def get_raw_player_data(self, player_id: str) -> Any:
|
|
756
|
-
"""
|
|
757
|
-
Get the player data in the lowest level form.
|
|
758
|
-
|
|
759
|
-
:param player_id: The id of the player to get.
|
|
760
|
-
:return:
|
|
761
|
-
"""
|
|
762
|
-
return self._safe_load(
|
|
763
|
-
self._get_raw_player_data,
|
|
764
|
-
(player_id,),
|
|
765
|
-
"Error loading player {}",
|
|
766
|
-
PlayerLoadError,
|
|
767
|
-
PlayerDoesNotExist,
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
@abstractmethod
|
|
771
|
-
def _get_raw_player_data(self, player_id: str) -> Any:
|
|
772
|
-
raise NotImplementedError
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import (
|
|
5
|
+
Tuple,
|
|
6
|
+
Any,
|
|
7
|
+
Generator,
|
|
8
|
+
Dict,
|
|
9
|
+
List,
|
|
10
|
+
Optional,
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Iterable,
|
|
13
|
+
Callable,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
TypeVar,
|
|
17
|
+
Generic,
|
|
18
|
+
)
|
|
19
|
+
import copy
|
|
20
|
+
import numpy
|
|
21
|
+
import os
|
|
22
|
+
import warnings
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
import PyMCTranslate
|
|
26
|
+
|
|
27
|
+
from amulet.api import level as api_level, wrapper as api_wrapper
|
|
28
|
+
from amulet.api.chunk import Chunk
|
|
29
|
+
from amulet.api.registry import BlockManager
|
|
30
|
+
from amulet.api.errors import (
|
|
31
|
+
ChunkLoadError,
|
|
32
|
+
ChunkDoesNotExist,
|
|
33
|
+
ObjectReadError,
|
|
34
|
+
ObjectReadWriteError,
|
|
35
|
+
PlayerDoesNotExist,
|
|
36
|
+
PlayerLoadError,
|
|
37
|
+
EntryLoadError,
|
|
38
|
+
EntryDoesNotExist,
|
|
39
|
+
DimensionDoesNotExist,
|
|
40
|
+
)
|
|
41
|
+
from amulet.api.data_types import (
|
|
42
|
+
AnyNDArray,
|
|
43
|
+
VersionNumberAny,
|
|
44
|
+
ChunkCoordinates,
|
|
45
|
+
Dimension,
|
|
46
|
+
PlatformType,
|
|
47
|
+
)
|
|
48
|
+
from amulet.api.selection import SelectionGroup, SelectionBox
|
|
49
|
+
from amulet.api.player import Player
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from amulet.api.wrapper.chunk.translator import Translator
|
|
53
|
+
|
|
54
|
+
log = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
DefaultSelection = SelectionGroup(
|
|
57
|
+
SelectionBox((-30_000_000, 0, -30_000_000), (30_000_000, 256, 30_000_000))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
VersionNumberT = TypeVar("VersionNumberT", int, Tuple[int, ...])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FormatWrapper(Generic[VersionNumberT], ABC):
|
|
64
|
+
"""
|
|
65
|
+
The FormatWrapper class is a class that sits between the serialised world or structure data and the program using amulet-core.
|
|
66
|
+
|
|
67
|
+
It is used to access data from the serialised source in the universal format and write them back again.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
_platform: Optional[PlatformType]
|
|
71
|
+
_version: Optional[VersionNumberT]
|
|
72
|
+
|
|
73
|
+
def __init__(self, path: str):
|
|
74
|
+
"""
|
|
75
|
+
Construct a new instance of :class:`FormatWrapper`.
|
|
76
|
+
|
|
77
|
+
This should not be used directly. You should instead use :func:`amulet.load_format`.
|
|
78
|
+
|
|
79
|
+
:param path: The file path to the serialised data.
|
|
80
|
+
"""
|
|
81
|
+
self._path = path
|
|
82
|
+
self._is_open = False
|
|
83
|
+
self._has_lock = False
|
|
84
|
+
self._translation_manager = None
|
|
85
|
+
self._platform = None
|
|
86
|
+
self._version = None
|
|
87
|
+
self._bounds: Dict[Dimension, SelectionGroup] = {}
|
|
88
|
+
self._changed: bool = False
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def sub_chunk_size(self) -> int:
|
|
92
|
+
"""
|
|
93
|
+
The dimensions of a sub-chunk.
|
|
94
|
+
"""
|
|
95
|
+
return 16
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def path(self) -> str:
|
|
99
|
+
"""The path to the data on disk."""
|
|
100
|
+
return self._path
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def level_name(self) -> str:
|
|
105
|
+
"""The name of the level."""
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def translation_manager(self) -> PyMCTranslate.TranslationManager:
|
|
110
|
+
"""The translation manager attached to the world."""
|
|
111
|
+
if self._translation_manager is None:
|
|
112
|
+
self._translation_manager = PyMCTranslate.new_translation_manager()
|
|
113
|
+
return self._translation_manager
|
|
114
|
+
|
|
115
|
+
@translation_manager.setter
|
|
116
|
+
def translation_manager(self, value: PyMCTranslate.TranslationManager):
|
|
117
|
+
# TODO: this should not be settable.
|
|
118
|
+
self._translation_manager = value
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def exists(self) -> bool:
|
|
122
|
+
"""Does some data exist at the specified path."""
|
|
123
|
+
return os.path.exists(self.path)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def is_valid(path: str) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Returns whether this format wrapper is able to load the given data.
|
|
130
|
+
|
|
131
|
+
:param path: The path of the data to load.
|
|
132
|
+
:return: True if the world can be loaded by this format wrapper, False otherwise.
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
|
|
139
|
+
"""
|
|
140
|
+
The valid platform and version combinations that this object can accept.
|
|
141
|
+
|
|
142
|
+
This is used when setting the platform and version in the create_and_open method
|
|
143
|
+
to verify that the platform and version are valid.
|
|
144
|
+
|
|
145
|
+
:return: A dictionary mapping the platform to a tuple of two booleans to determine if numerical and blockstate are valid respectively.
|
|
146
|
+
"""
|
|
147
|
+
raise NotImplementedError
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def platform(self) -> PlatformType:
|
|
151
|
+
"""Platform string the data is stored in (eg "bedrock" / "java" / ...)"""
|
|
152
|
+
if self._platform is None:
|
|
153
|
+
raise Exception(
|
|
154
|
+
"Cannot access the game platform until the level has been loaded."
|
|
155
|
+
)
|
|
156
|
+
return self._platform
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def version(self) -> VersionNumberT:
|
|
160
|
+
"""The version number for the given platform the data is stored in eg (1, 16, 2)"""
|
|
161
|
+
if self._version is None:
|
|
162
|
+
raise Exception(
|
|
163
|
+
"Cannot access the game version until the level has been loaded."
|
|
164
|
+
)
|
|
165
|
+
return self._version
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def max_world_version(self) -> Tuple[PlatformType, VersionNumberT]:
|
|
169
|
+
"""
|
|
170
|
+
The version the world was last opened in.
|
|
171
|
+
|
|
172
|
+
This should be greater than or equal to the chunk versions found within.
|
|
173
|
+
"""
|
|
174
|
+
return self.platform, self.version
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def changed(self) -> bool:
|
|
178
|
+
"""Has any data been pushed to the format wrapper that has not been saved to disk."""
|
|
179
|
+
return self._changed
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def dimensions(self) -> List[Dimension]:
|
|
184
|
+
"""A list of all the dimensions contained in the world."""
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def can_add_dimension(self) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Can external code register a new dimension.
|
|
192
|
+
|
|
193
|
+
If False :meth:`register_dimension` will have no effect.
|
|
194
|
+
"""
|
|
195
|
+
raise NotImplementedError
|
|
196
|
+
|
|
197
|
+
@abstractmethod
|
|
198
|
+
def register_dimension(self, dimension_identifier: Any):
|
|
199
|
+
"""
|
|
200
|
+
Register a new dimension.
|
|
201
|
+
|
|
202
|
+
:param dimension_identifier: The identifier for the dimension.
|
|
203
|
+
"""
|
|
204
|
+
raise NotImplementedError
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def requires_selection(self) -> bool:
|
|
208
|
+
"""Does this object require that a selection be defined when creating it from scratch?"""
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def multi_selection(self) -> bool:
|
|
213
|
+
"""
|
|
214
|
+
Does this object support having multiple selection boxes.
|
|
215
|
+
|
|
216
|
+
If False it will be given exactly 1 selection.
|
|
217
|
+
|
|
218
|
+
If True can be given 0 or more.
|
|
219
|
+
"""
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def selection(self) -> SelectionGroup:
|
|
224
|
+
"""The area that all chunk data must fit within."""
|
|
225
|
+
warnings.warn(
|
|
226
|
+
"FormatWrapper.selection is depreciated and will be removed in the future. Please use FormatWrapper.bounds(dimension) instead",
|
|
227
|
+
DeprecationWarning,
|
|
228
|
+
)
|
|
229
|
+
return self.bounds(self.dimensions[0])
|
|
230
|
+
|
|
231
|
+
def bounds(self, dimension: Dimension) -> SelectionGroup:
|
|
232
|
+
if dimension not in self._bounds:
|
|
233
|
+
if dimension in self.dimensions:
|
|
234
|
+
raise Exception(
|
|
235
|
+
f'The dimension exists but there is no selection registered for it. Please report this to a developer "{dimension}" {self}'
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
raise DimensionDoesNotExist
|
|
239
|
+
return self._bounds[dimension]
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def _get_interface(
|
|
243
|
+
self, raw_chunk_data: Optional[Any] = None
|
|
244
|
+
) -> api_wrapper.Interface:
|
|
245
|
+
raise NotImplementedError
|
|
246
|
+
|
|
247
|
+
def _get_interface_and_translator(
|
|
248
|
+
self, raw_chunk_data=None
|
|
249
|
+
) -> Tuple[api_wrapper.Interface, "Translator", VersionNumberAny]:
|
|
250
|
+
interface = self._get_interface(raw_chunk_data)
|
|
251
|
+
translator, version_identifier = interface.get_translator(
|
|
252
|
+
self.max_world_version, raw_chunk_data
|
|
253
|
+
)
|
|
254
|
+
return interface, translator, version_identifier
|
|
255
|
+
|
|
256
|
+
def create_and_open(
|
|
257
|
+
self,
|
|
258
|
+
platform: PlatformType,
|
|
259
|
+
version: VersionNumberAny,
|
|
260
|
+
bounds: Union[
|
|
261
|
+
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
262
|
+
] = None,
|
|
263
|
+
overwrite: bool = False,
|
|
264
|
+
**kwargs,
|
|
265
|
+
):
|
|
266
|
+
"""
|
|
267
|
+
Remove the data at the path and set up a new database.
|
|
268
|
+
|
|
269
|
+
You might want to call :attr:`exists` to check if something exists at the path
|
|
270
|
+
and warn the user they are going to overwrite existing data before calling this method.
|
|
271
|
+
|
|
272
|
+
:param platform: The platform the data should use.
|
|
273
|
+
:param version: The version the data should use.
|
|
274
|
+
:param bounds: The bounds for each dimension. If one :class:`SelectionGroup` is given it will be applied to all dimensions.
|
|
275
|
+
:param overwrite: Should an existing database be overwritten. If this is False and one exists and error will be thrown.
|
|
276
|
+
:param kwargs: Extra arguments as each implementation requires.
|
|
277
|
+
:return:
|
|
278
|
+
"""
|
|
279
|
+
if self.is_open:
|
|
280
|
+
raise ObjectReadError(f"Cannot open {self} because it was already opened.")
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
platform not in self.valid_formats or len(self.valid_formats[platform]) < 2
|
|
284
|
+
): # check that the platform and version are valid
|
|
285
|
+
raise ObjectReadError(
|
|
286
|
+
f"{platform} is not a valid platform for this wrapper."
|
|
287
|
+
)
|
|
288
|
+
translator_version = self.translation_manager.get_version(platform, version)
|
|
289
|
+
if translator_version.has_abstract_format: # numerical
|
|
290
|
+
if not self.valid_formats[platform][0]:
|
|
291
|
+
raise ObjectReadError(
|
|
292
|
+
f"The version given ({version}) is from the numerical format but this wrapper does not support the numerical format."
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
if not self.valid_formats[platform][1]:
|
|
296
|
+
raise ObjectReadError(
|
|
297
|
+
f"The version given ({version}) is from the blockstate format but this wrapper does not support the blockstate format."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self._platform = translator_version.platform
|
|
301
|
+
self._version = translator_version.version_number
|
|
302
|
+
self._create(overwrite, bounds, **kwargs)
|
|
303
|
+
|
|
304
|
+
def _clean_selection(self, selection: SelectionGroup) -> SelectionGroup:
|
|
305
|
+
if self.multi_selection:
|
|
306
|
+
return selection
|
|
307
|
+
else:
|
|
308
|
+
if selection:
|
|
309
|
+
return SelectionGroup(
|
|
310
|
+
sorted(
|
|
311
|
+
selection.selection_boxes,
|
|
312
|
+
reverse=True,
|
|
313
|
+
key=lambda b: b.volume,
|
|
314
|
+
)[0]
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
raise ObjectReadError(
|
|
318
|
+
"A single selection was required but none were given."
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
@abstractmethod
|
|
322
|
+
def _create(
|
|
323
|
+
self,
|
|
324
|
+
overwrite: bool,
|
|
325
|
+
bounds: Union[
|
|
326
|
+
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
327
|
+
] = None,
|
|
328
|
+
**kwargs,
|
|
329
|
+
):
|
|
330
|
+
"""Set up the database from scratch."""
|
|
331
|
+
raise NotImplementedError
|
|
332
|
+
|
|
333
|
+
def open(self):
|
|
334
|
+
"""Open the database for reading and writing."""
|
|
335
|
+
if self.is_open:
|
|
336
|
+
raise ObjectReadError(f"Cannot open {self} because it was already opened.")
|
|
337
|
+
self._open()
|
|
338
|
+
|
|
339
|
+
@abstractmethod
|
|
340
|
+
def _open(self):
|
|
341
|
+
raise NotImplementedError
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def is_open(self) -> bool:
|
|
345
|
+
"""Has the object been opened."""
|
|
346
|
+
return self._is_open
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def has_lock(self) -> bool:
|
|
350
|
+
"""Verify that the world database can be read and written"""
|
|
351
|
+
return self._has_lock
|
|
352
|
+
|
|
353
|
+
def _verify_has_lock(self):
|
|
354
|
+
"""
|
|
355
|
+
Ensure that the FormatWrapper is open and has a lock on the object.
|
|
356
|
+
|
|
357
|
+
:raises:
|
|
358
|
+
ObjectReadWriteError: if the FormatWrapper does not have a lock on the object.
|
|
359
|
+
"""
|
|
360
|
+
if not self.is_open:
|
|
361
|
+
raise ObjectReadWriteError(
|
|
362
|
+
f"The object {self} was never opened. Call .open or .create_and_open to open it before accessing data."
|
|
363
|
+
)
|
|
364
|
+
elif not self.has_lock:
|
|
365
|
+
raise ObjectReadWriteError(
|
|
366
|
+
f"The lock on the object {self} has been lost. It was probably opened somewhere else."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def pre_save_operation(level: api_level.BaseLevel) -> Generator[float, None, bool]:
|
|
371
|
+
"""
|
|
372
|
+
Logic to run before saving. Eg recalculating height maps or lighting.
|
|
373
|
+
|
|
374
|
+
Must be a generator that yields a number and returns a bool.
|
|
375
|
+
|
|
376
|
+
The yielded number is the progress from 0 to 1.
|
|
377
|
+
|
|
378
|
+
The returned bool is if changes have been made.
|
|
379
|
+
|
|
380
|
+
:param level: The level to apply modifications to.
|
|
381
|
+
:return: Have any modifications been made.
|
|
382
|
+
"""
|
|
383
|
+
yield 1
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
def save(self):
|
|
387
|
+
"""Save the data back to the level."""
|
|
388
|
+
self._verify_has_lock()
|
|
389
|
+
self._save()
|
|
390
|
+
self._changed = False
|
|
391
|
+
|
|
392
|
+
@abstractmethod
|
|
393
|
+
def _save(self):
|
|
394
|
+
raise NotImplementedError
|
|
395
|
+
|
|
396
|
+
def close(self):
|
|
397
|
+
"""Close the level."""
|
|
398
|
+
if self.is_open:
|
|
399
|
+
self._is_open = False
|
|
400
|
+
self._has_lock = False
|
|
401
|
+
self._close()
|
|
402
|
+
|
|
403
|
+
@abstractmethod
|
|
404
|
+
def _close(self):
|
|
405
|
+
raise NotImplementedError
|
|
406
|
+
|
|
407
|
+
@abstractmethod
|
|
408
|
+
def unload(self):
|
|
409
|
+
"""Unload data stored in the FormatWrapper class"""
|
|
410
|
+
raise NotImplementedError
|
|
411
|
+
|
|
412
|
+
@abstractmethod
|
|
413
|
+
def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
|
|
414
|
+
"""A generator of all chunk coords in the given dimension."""
|
|
415
|
+
raise NotImplementedError
|
|
416
|
+
|
|
417
|
+
@abstractmethod
|
|
418
|
+
def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
|
|
419
|
+
"""
|
|
420
|
+
Does the chunk exist in the world database?
|
|
421
|
+
|
|
422
|
+
:param cx: The x coordinate of the chunk.
|
|
423
|
+
:param cz: The z coordinate of the chunk.
|
|
424
|
+
:param dimension: The dimension to load the chunk from.
|
|
425
|
+
:return: True if the chunk exists. Calling load_chunk on this chunk may still throw ChunkLoadError
|
|
426
|
+
"""
|
|
427
|
+
raise NotImplementedError
|
|
428
|
+
|
|
429
|
+
def _safe_load(
|
|
430
|
+
self,
|
|
431
|
+
meth: Callable,
|
|
432
|
+
args: Tuple[Any, ...],
|
|
433
|
+
msg: str,
|
|
434
|
+
load_error: Type[EntryLoadError],
|
|
435
|
+
does_not_exist_error: Type[EntryDoesNotExist],
|
|
436
|
+
):
|
|
437
|
+
try:
|
|
438
|
+
self._verify_has_lock()
|
|
439
|
+
except ObjectReadWriteError as e:
|
|
440
|
+
raise does_not_exist_error(e)
|
|
441
|
+
try:
|
|
442
|
+
return meth(*args)
|
|
443
|
+
except does_not_exist_error as e:
|
|
444
|
+
raise e
|
|
445
|
+
except Exception as e:
|
|
446
|
+
log.error(msg.format(*args), exc_info=True)
|
|
447
|
+
raise load_error(e) from e
|
|
448
|
+
|
|
449
|
+
def load_chunk(self, cx: int, cz: int, dimension: Dimension) -> Chunk:
|
|
450
|
+
"""
|
|
451
|
+
Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
|
|
452
|
+
|
|
453
|
+
:param cx: The x coordinate of the chunk.
|
|
454
|
+
:param cz: The z coordinate of the chunk.
|
|
455
|
+
:param dimension: The dimension to load the chunk from.
|
|
456
|
+
:return: The chunk at the given coordinates.
|
|
457
|
+
:raises:
|
|
458
|
+
ChunkDoesNotExist: If the chunk does not exist (was deleted or never created)
|
|
459
|
+
ChunkLoadError: If the chunk was not able to be loaded. Eg. If the chunk is corrupt or some error occurred when loading.
|
|
460
|
+
"""
|
|
461
|
+
return self._safe_load(
|
|
462
|
+
self._load_chunk,
|
|
463
|
+
(cx, cz, dimension),
|
|
464
|
+
"Error loading chunk {} {} {}",
|
|
465
|
+
ChunkLoadError,
|
|
466
|
+
ChunkDoesNotExist,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def _load_chunk(
|
|
470
|
+
self, cx: int, cz: int, dimension: Dimension, recurse: bool = True
|
|
471
|
+
) -> Chunk:
|
|
472
|
+
"""
|
|
473
|
+
Loads and creates a universal :class:`~amulet.api.chunk.Chunk` object from chunk coordinates.
|
|
474
|
+
|
|
475
|
+
:param cx: The x coordinate of the chunk.
|
|
476
|
+
:param cz: The z coordinate of the chunk.
|
|
477
|
+
:param dimension: The dimension to load the chunk from.
|
|
478
|
+
:param recurse: bool: look in boundary chunks if required to fully define data
|
|
479
|
+
:return: The chunk at the given coordinates.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
# Gets an interface (the code that actually reads the chunk data)
|
|
483
|
+
raw_chunk_data = self._get_raw_chunk_data(cx, cz, dimension)
|
|
484
|
+
interface, translator, game_version = self._get_interface_and_translator(
|
|
485
|
+
raw_chunk_data
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# decode the raw chunk data into the universal format
|
|
489
|
+
chunk, block_palette = self._decode(
|
|
490
|
+
interface, dimension, cx, cz, raw_chunk_data
|
|
491
|
+
)
|
|
492
|
+
block_palette: AnyNDArray
|
|
493
|
+
chunk = self._unpack(translator, game_version, chunk, block_palette)
|
|
494
|
+
return self._convert_to_load(
|
|
495
|
+
chunk, translator, game_version, dimension, recurse=recurse
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def _decode(
|
|
500
|
+
interface: api_wrapper.Interface,
|
|
501
|
+
dimension: Dimension,
|
|
502
|
+
cx: int,
|
|
503
|
+
cz: int,
|
|
504
|
+
raw_chunk_data: Any,
|
|
505
|
+
) -> Tuple[Chunk, AnyNDArray]:
|
|
506
|
+
return interface.decode(cx, cz, raw_chunk_data)
|
|
507
|
+
|
|
508
|
+
def _unpack(
|
|
509
|
+
self,
|
|
510
|
+
translator: "Translator",
|
|
511
|
+
game_version: VersionNumberAny,
|
|
512
|
+
chunk: Chunk,
|
|
513
|
+
block_palette: AnyNDArray,
|
|
514
|
+
) -> Chunk:
|
|
515
|
+
return translator.unpack(
|
|
516
|
+
game_version, self.translation_manager, chunk, block_palette
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def _convert_to_load(
|
|
520
|
+
self,
|
|
521
|
+
chunk: Chunk,
|
|
522
|
+
translator: "Translator",
|
|
523
|
+
game_version: VersionNumberAny,
|
|
524
|
+
dimension: Dimension,
|
|
525
|
+
recurse: bool = True,
|
|
526
|
+
) -> Chunk:
|
|
527
|
+
# set up a callback that translator can use to get chunk data
|
|
528
|
+
cx, cz = chunk.cx, chunk.cz
|
|
529
|
+
if recurse:
|
|
530
|
+
chunk_cache: Dict[ChunkCoordinates, Chunk] = {}
|
|
531
|
+
|
|
532
|
+
def get_chunk_callback(x: int, z: int) -> Chunk:
|
|
533
|
+
cx_, cz_ = cx + x, cz + z
|
|
534
|
+
if (cx_, cz_) not in chunk_cache:
|
|
535
|
+
chunk_cache[(cx_, cz_)] = self._load_chunk(
|
|
536
|
+
cx_, cz_, dimension, recurse=False
|
|
537
|
+
)
|
|
538
|
+
return chunk_cache[(cx_, cz_)]
|
|
539
|
+
|
|
540
|
+
else:
|
|
541
|
+
get_chunk_callback = None
|
|
542
|
+
|
|
543
|
+
# translate the data to universal format
|
|
544
|
+
chunk = translator.to_universal(
|
|
545
|
+
game_version, self.translation_manager, chunk, get_chunk_callback, recurse
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
chunk.changed = False
|
|
549
|
+
return chunk
|
|
550
|
+
|
|
551
|
+
def commit_chunk(self, chunk: Chunk, dimension: Dimension):
|
|
552
|
+
"""
|
|
553
|
+
Save a universal format chunk to the FormatWrapper database (not the level database)
|
|
554
|
+
|
|
555
|
+
Call save method to write changed chunks back to the level.
|
|
556
|
+
|
|
557
|
+
:param chunk: The chunk object to translate and save.
|
|
558
|
+
:param dimension: The dimension to commit the chunk to.
|
|
559
|
+
"""
|
|
560
|
+
try:
|
|
561
|
+
self._verify_has_lock()
|
|
562
|
+
except ObjectReadWriteError as e:
|
|
563
|
+
log.error(e)
|
|
564
|
+
try:
|
|
565
|
+
self._commit_chunk(copy.deepcopy(chunk), dimension)
|
|
566
|
+
except Exception:
|
|
567
|
+
log.error(f"Error saving chunk {chunk}", exc_info=True)
|
|
568
|
+
self._changed = True
|
|
569
|
+
|
|
570
|
+
def _commit_chunk(self, chunk: Chunk, dimension: Dimension, recurse: bool = True):
|
|
571
|
+
"""
|
|
572
|
+
Saves a universal :class:`~amulet.api.chunk.Chunk` object.
|
|
573
|
+
|
|
574
|
+
Calls the interface then the translator.
|
|
575
|
+
|
|
576
|
+
It then calls _put_chunk_data to store the data returned by the interface
|
|
577
|
+
"""
|
|
578
|
+
# get the coordinates for later
|
|
579
|
+
cx, cz = chunk.cx, chunk.cz
|
|
580
|
+
|
|
581
|
+
# Gets an interface, translator and most recent chunk version for the game version.
|
|
582
|
+
interface, translator, chunk_version = self._get_interface_and_translator()
|
|
583
|
+
|
|
584
|
+
chunk = self._convert_to_save(chunk, chunk_version, translator, recurse)
|
|
585
|
+
chunk, chunk_palette = self._pack(chunk, translator, chunk_version)
|
|
586
|
+
raw_chunk_data = self._encode(interface, chunk, dimension, chunk_palette)
|
|
587
|
+
|
|
588
|
+
self._put_raw_chunk_data(cx, cz, raw_chunk_data, dimension)
|
|
589
|
+
|
|
590
|
+
def _convert_to_save(
|
|
591
|
+
self,
|
|
592
|
+
chunk: Chunk,
|
|
593
|
+
chunk_version: VersionNumberAny,
|
|
594
|
+
translator: "Translator",
|
|
595
|
+
recurse: bool = True,
|
|
596
|
+
) -> Chunk:
|
|
597
|
+
"""Convert the Chunk in Universal format to a Chunk in the version specific format."""
|
|
598
|
+
# create a new streamlined block block_palette and remap the data
|
|
599
|
+
palette: List[numpy.ndarray] = []
|
|
600
|
+
palette_len = 0
|
|
601
|
+
for cy in chunk.blocks.sub_chunks:
|
|
602
|
+
sub_chunk_palette, sub_chunk = numpy.unique(
|
|
603
|
+
chunk.blocks.get_sub_chunk(cy), return_inverse=True
|
|
604
|
+
)
|
|
605
|
+
chunk.blocks.add_sub_chunk(
|
|
606
|
+
cy, sub_chunk.astype(numpy.uint32).reshape((16, 16, 16)) + palette_len
|
|
607
|
+
)
|
|
608
|
+
palette_len += len(sub_chunk_palette)
|
|
609
|
+
palette.append(sub_chunk_palette)
|
|
610
|
+
|
|
611
|
+
if palette:
|
|
612
|
+
chunk_palette, lut = numpy.unique(
|
|
613
|
+
numpy.concatenate(palette), return_inverse=True
|
|
614
|
+
)
|
|
615
|
+
for cy in chunk.blocks.sub_chunks:
|
|
616
|
+
chunk.blocks.add_sub_chunk(
|
|
617
|
+
cy, lut.astype(numpy.uint32)[chunk.blocks.get_sub_chunk(cy)]
|
|
618
|
+
)
|
|
619
|
+
chunk._block_palette = BlockManager(
|
|
620
|
+
numpy.vectorize(chunk.block_palette.__getitem__)(chunk_palette)
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
chunk._block_palette = BlockManager()
|
|
624
|
+
|
|
625
|
+
def get_chunk_callback(_: int, __: int) -> Chunk:
|
|
626
|
+
# conversion from universal should not require any data outside the block
|
|
627
|
+
return chunk
|
|
628
|
+
|
|
629
|
+
# translate from universal format to version format
|
|
630
|
+
return translator.from_universal(
|
|
631
|
+
chunk_version, self.translation_manager, chunk, get_chunk_callback, recurse
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def _pack(
|
|
635
|
+
self, chunk: Chunk, translator: "Translator", chunk_version: VersionNumberAny
|
|
636
|
+
) -> Tuple[Chunk, AnyNDArray]:
|
|
637
|
+
"""Pack the chunk data into the format required by the encoder.
|
|
638
|
+
This includes converting the string names to numerical formats for the versions that require it.
|
|
639
|
+
"""
|
|
640
|
+
return translator.pack(chunk_version, self.translation_manager, chunk)
|
|
641
|
+
|
|
642
|
+
def _encode(
|
|
643
|
+
self,
|
|
644
|
+
interface: api_wrapper.Interface,
|
|
645
|
+
chunk: Chunk,
|
|
646
|
+
dimension: Dimension,
|
|
647
|
+
chunk_palette: AnyNDArray,
|
|
648
|
+
) -> Any:
|
|
649
|
+
"""Encode the data to the raw format as saved on disk."""
|
|
650
|
+
raise NotImplementedError
|
|
651
|
+
|
|
652
|
+
def delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
653
|
+
"""
|
|
654
|
+
Delete the given chunk from the level.
|
|
655
|
+
|
|
656
|
+
:param cx: The x coordinate of the chunk.
|
|
657
|
+
:param cz: The z coordinate of the chunk.
|
|
658
|
+
:param dimension: The dimension to load the data from.
|
|
659
|
+
"""
|
|
660
|
+
self._delete_chunk(cx, cz, dimension)
|
|
661
|
+
self._changed = True
|
|
662
|
+
|
|
663
|
+
@abstractmethod
|
|
664
|
+
def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
665
|
+
raise NotImplementedError
|
|
666
|
+
|
|
667
|
+
def put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
|
|
668
|
+
"""
|
|
669
|
+
Commit the raw chunk data to the FormatWrapper cache.
|
|
670
|
+
|
|
671
|
+
Call :meth:`save` to push all the cache data to the level.
|
|
672
|
+
|
|
673
|
+
:param cx: The x coordinate of the chunk.
|
|
674
|
+
:param cz: The z coordinate of the chunk.
|
|
675
|
+
:param data: The raw data to commit to the level.
|
|
676
|
+
:param dimension: The dimension to load the data from.
|
|
677
|
+
"""
|
|
678
|
+
self._verify_has_lock()
|
|
679
|
+
self._put_raw_chunk_data(cx, cz, data, dimension)
|
|
680
|
+
|
|
681
|
+
@abstractmethod
|
|
682
|
+
def _put_raw_chunk_data(self, cx: int, cz: int, data: Any, dimension: Dimension):
|
|
683
|
+
raise NotImplementedError
|
|
684
|
+
|
|
685
|
+
def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
|
|
686
|
+
"""
|
|
687
|
+
Return the raw data as loaded from disk.
|
|
688
|
+
|
|
689
|
+
:param cx: The x coordinate of the chunk.
|
|
690
|
+
:param cz: The z coordinate of the chunk.
|
|
691
|
+
:param dimension: The dimension to load the data from.
|
|
692
|
+
:return: The raw chunk data.
|
|
693
|
+
"""
|
|
694
|
+
return self._safe_load(
|
|
695
|
+
self._get_raw_chunk_data,
|
|
696
|
+
(cx, cz, dimension),
|
|
697
|
+
"Error loading chunk {} {} {}",
|
|
698
|
+
ChunkLoadError,
|
|
699
|
+
ChunkDoesNotExist,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
@abstractmethod
|
|
703
|
+
def _get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> Any:
|
|
704
|
+
"""
|
|
705
|
+
Return the raw data as loaded from disk.
|
|
706
|
+
|
|
707
|
+
:param cx: The x coordinate of the chunk.
|
|
708
|
+
:param cz: The z coordinate of the chunk.
|
|
709
|
+
:param dimension: The dimension to load the data from.
|
|
710
|
+
:return: The raw chunk data.
|
|
711
|
+
"""
|
|
712
|
+
raise NotImplementedError
|
|
713
|
+
|
|
714
|
+
@abstractmethod
|
|
715
|
+
def all_player_ids(self) -> Iterable[str]:
|
|
716
|
+
"""
|
|
717
|
+
Returns a set of all player ids that are present in the level
|
|
718
|
+
"""
|
|
719
|
+
return NotImplemented
|
|
720
|
+
|
|
721
|
+
@abstractmethod
|
|
722
|
+
def has_player(self, player_id: str) -> bool:
|
|
723
|
+
"""
|
|
724
|
+
Test if a player id is present in the level.
|
|
725
|
+
"""
|
|
726
|
+
return NotImplemented
|
|
727
|
+
|
|
728
|
+
def load_player(self, player_id: str) -> "Player":
|
|
729
|
+
"""
|
|
730
|
+
Gets the :class:`Player` object that belongs to the specified player id
|
|
731
|
+
|
|
732
|
+
If no parameter is supplied, the data of the local player should be returned
|
|
733
|
+
|
|
734
|
+
:param player_id: The desired player id
|
|
735
|
+
:return: A Player instance
|
|
736
|
+
"""
|
|
737
|
+
return self._safe_load(
|
|
738
|
+
self._load_player,
|
|
739
|
+
(player_id,),
|
|
740
|
+
"Error loading player {}",
|
|
741
|
+
PlayerLoadError,
|
|
742
|
+
PlayerDoesNotExist,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
@abstractmethod
|
|
746
|
+
def _load_player(self, player_id: str) -> "Player":
|
|
747
|
+
"""
|
|
748
|
+
Get the raw player data and unpack it into a Player class.
|
|
749
|
+
|
|
750
|
+
:param player_id: The id of the player to get.
|
|
751
|
+
:return:
|
|
752
|
+
"""
|
|
753
|
+
raise NotImplementedError
|
|
754
|
+
|
|
755
|
+
def get_raw_player_data(self, player_id: str) -> Any:
|
|
756
|
+
"""
|
|
757
|
+
Get the player data in the lowest level form.
|
|
758
|
+
|
|
759
|
+
:param player_id: The id of the player to get.
|
|
760
|
+
:return:
|
|
761
|
+
"""
|
|
762
|
+
return self._safe_load(
|
|
763
|
+
self._get_raw_player_data,
|
|
764
|
+
(player_id,),
|
|
765
|
+
"Error loading player {}",
|
|
766
|
+
PlayerLoadError,
|
|
767
|
+
PlayerDoesNotExist,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
@abstractmethod
|
|
771
|
+
def _get_raw_player_data(self, player_id: str) -> Any:
|
|
772
|
+
raise NotImplementedError
|