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,769 +1,769 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import struct
|
|
5
|
-
from typing import (
|
|
6
|
-
Tuple,
|
|
7
|
-
Dict,
|
|
8
|
-
Generator,
|
|
9
|
-
Optional,
|
|
10
|
-
List,
|
|
11
|
-
Union,
|
|
12
|
-
Iterable,
|
|
13
|
-
BinaryIO,
|
|
14
|
-
Any,
|
|
15
|
-
)
|
|
16
|
-
import time
|
|
17
|
-
import glob
|
|
18
|
-
import shutil
|
|
19
|
-
import json
|
|
20
|
-
import logging
|
|
21
|
-
|
|
22
|
-
import portalocker
|
|
23
|
-
|
|
24
|
-
from amulet_nbt import (
|
|
25
|
-
IntTag,
|
|
26
|
-
LongTag,
|
|
27
|
-
DoubleTag,
|
|
28
|
-
StringTag,
|
|
29
|
-
ListTag,
|
|
30
|
-
CompoundTag,
|
|
31
|
-
NamedTag,
|
|
32
|
-
load as load_nbt,
|
|
33
|
-
)
|
|
34
|
-
from amulet.api.player import Player, LOCAL_PLAYER
|
|
35
|
-
from amulet.api.chunk import Chunk
|
|
36
|
-
from amulet.api.selection import SelectionGroup, SelectionBox
|
|
37
|
-
from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
|
|
38
|
-
from amulet.utils.format_utils import check_all_exist
|
|
39
|
-
from amulet.api.errors import (
|
|
40
|
-
DimensionDoesNotExist,
|
|
41
|
-
ObjectWriteError,
|
|
42
|
-
ChunkLoadError,
|
|
43
|
-
ChunkDoesNotExist,
|
|
44
|
-
PlayerDoesNotExist,
|
|
45
|
-
)
|
|
46
|
-
from amulet.api.data_types import (
|
|
47
|
-
ChunkCoordinates,
|
|
48
|
-
VersionNumberInt,
|
|
49
|
-
PlatformType,
|
|
50
|
-
DimensionCoordinates,
|
|
51
|
-
AnyNDArray,
|
|
52
|
-
Dimension,
|
|
53
|
-
)
|
|
54
|
-
from .dimension import AnvilDimensionManager, ChunkDataType
|
|
55
|
-
from amulet.api import level as api_level
|
|
56
|
-
from amulet.level.interfaces.chunk.anvil.base_anvil_interface import BaseAnvilInterface
|
|
57
|
-
from .data_pack import DataPack, DataPackManager
|
|
58
|
-
|
|
59
|
-
log = logging.getLogger(__name__)
|
|
60
|
-
|
|
61
|
-
InternalDimension = str
|
|
62
|
-
OVERWORLD = "minecraft:overworld"
|
|
63
|
-
THE_NETHER = "minecraft:the_nether"
|
|
64
|
-
THE_END = "minecraft:the_end"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
|
|
68
|
-
"""
|
|
69
|
-
This FormatWrapper class exists to interface with the Java world format.
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def __init__(self, path: str):
|
|
73
|
-
"""
|
|
74
|
-
Construct a new instance of :class:`AnvilFormat`.
|
|
75
|
-
|
|
76
|
-
This should not be used directly. You should instead use :func:`amulet.load_format`.
|
|
77
|
-
|
|
78
|
-
:param path: The file path to the serialised data.
|
|
79
|
-
"""
|
|
80
|
-
super().__init__(path)
|
|
81
|
-
self._platform = "java"
|
|
82
|
-
self._root_tag: NamedTag = NamedTag()
|
|
83
|
-
self._levels: Dict[InternalDimension, AnvilDimensionManager] = {}
|
|
84
|
-
self._dimension_name_map: Dict[Dimension, InternalDimension] = {}
|
|
85
|
-
self._mcc_support: Optional[bool] = None
|
|
86
|
-
self._lock_time: Optional[bytes] = None
|
|
87
|
-
self._lock: Optional[BinaryIO] = None
|
|
88
|
-
self._data_pack: Optional[DataPackManager] = None
|
|
89
|
-
self._shallow_load()
|
|
90
|
-
|
|
91
|
-
def __del__(self):
|
|
92
|
-
self.close()
|
|
93
|
-
|
|
94
|
-
def _shallow_load(self):
|
|
95
|
-
try:
|
|
96
|
-
self._load_level_dat()
|
|
97
|
-
except:
|
|
98
|
-
pass
|
|
99
|
-
|
|
100
|
-
def _load_level_dat(self):
|
|
101
|
-
"""Load the level.dat file and check the image file"""
|
|
102
|
-
if os.path.isfile(os.path.join(self.path, "icon.png")):
|
|
103
|
-
self._world_image_path = os.path.join(self.path, "icon.png")
|
|
104
|
-
else:
|
|
105
|
-
self._world_image_path = self._missing_world_icon
|
|
106
|
-
self.root_tag = load_nbt(os.path.join(self.path, "level.dat"))
|
|
107
|
-
|
|
108
|
-
@staticmethod
|
|
109
|
-
def is_valid(path: str) -> bool:
|
|
110
|
-
if not check_all_exist(path, "level.dat"):
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
try:
|
|
114
|
-
level_dat_root = load_nbt(os.path.join(path, "level.dat")).compound
|
|
115
|
-
except:
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
return "Data" in level_dat_root and "FML" not in level_dat_root
|
|
119
|
-
|
|
120
|
-
@property
|
|
121
|
-
def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
|
|
122
|
-
return {"java": (True, True)}
|
|
123
|
-
|
|
124
|
-
@property
|
|
125
|
-
def version(self) -> VersionNumberInt:
|
|
126
|
-
"""The data version number that the world was last opened in. eg 2578"""
|
|
127
|
-
if self._version is None:
|
|
128
|
-
self._version = self._get_version()
|
|
129
|
-
return self._version
|
|
130
|
-
|
|
131
|
-
def _get_version(self) -> VersionNumberInt:
|
|
132
|
-
return (
|
|
133
|
-
self.root_tag.compound.get_compound("Data", CompoundTag())
|
|
134
|
-
.get_int("DataVersion", IntTag(-1))
|
|
135
|
-
.py_int
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
@property
|
|
139
|
-
def root_tag(self) -> NamedTag:
|
|
140
|
-
"""The level.dat data for the level."""
|
|
141
|
-
return self._root_tag
|
|
142
|
-
|
|
143
|
-
@root_tag.setter
|
|
144
|
-
def root_tag(self, root_tag: Union[NamedTag, CompoundTag]):
|
|
145
|
-
if isinstance(root_tag, CompoundTag):
|
|
146
|
-
self._root_tag = NamedTag(root_tag)
|
|
147
|
-
elif isinstance(root_tag, NamedTag):
|
|
148
|
-
self._root_tag = root_tag
|
|
149
|
-
else:
|
|
150
|
-
raise ValueError("root_tag must be a CompoundTag or NamedTag")
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def level_name(self) -> str:
|
|
154
|
-
return (
|
|
155
|
-
self.root_tag.compound.get_compound("Data").get_string("LevelName").py_str
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
@level_name.setter
|
|
159
|
-
def level_name(self, value: str):
|
|
160
|
-
self.root_tag.compound.setdefault_compound("Data")["LevelName"] = StringTag(
|
|
161
|
-
value
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
@property
|
|
165
|
-
def last_played(self) -> int:
|
|
166
|
-
return (
|
|
167
|
-
self.root_tag.compound.get_compound("Data")
|
|
168
|
-
.get_long("LastPlayed", LongTag())
|
|
169
|
-
.py_int
|
|
170
|
-
// 1000
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
@property
|
|
174
|
-
def game_version_string(self) -> str:
|
|
175
|
-
try:
|
|
176
|
-
return f'Java {self.root_tag.compound.get_compound("Data").get_compound("Version").get_string("Name").py_str}'
|
|
177
|
-
except Exception:
|
|
178
|
-
return f"Java Unknown Version"
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def data_pack(self) -> DataPackManager:
|
|
182
|
-
if self._data_pack is None:
|
|
183
|
-
packs = []
|
|
184
|
-
enabled_packs = (
|
|
185
|
-
self.root_tag.compound.get_compound("Data")
|
|
186
|
-
.get_compound("DataPacks", CompoundTag())
|
|
187
|
-
.get_list("Enabled", ListTag())
|
|
188
|
-
)
|
|
189
|
-
for pack in enabled_packs:
|
|
190
|
-
if isinstance(pack, StringTag):
|
|
191
|
-
pack_name: str = pack.py_str
|
|
192
|
-
if pack_name == "vanilla":
|
|
193
|
-
pass
|
|
194
|
-
elif pack_name.startswith("file/"):
|
|
195
|
-
path = os.path.join(self.path, "datapacks", pack_name[5:])
|
|
196
|
-
if DataPack.is_path_valid(path):
|
|
197
|
-
packs.append(DataPack(path))
|
|
198
|
-
self._data_pack = DataPackManager(packs)
|
|
199
|
-
return self._data_pack
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def dimensions(self) -> List[Dimension]:
|
|
203
|
-
return list(self._dimension_name_map.keys())
|
|
204
|
-
|
|
205
|
-
def _register_dimension(
|
|
206
|
-
self,
|
|
207
|
-
relative_dimension_path: InternalDimension,
|
|
208
|
-
dimension_name: Optional[Dimension] = None,
|
|
209
|
-
):
|
|
210
|
-
"""
|
|
211
|
-
Register a new dimension.
|
|
212
|
-
|
|
213
|
-
:param relative_dimension_path: The relative path to the dimension directory from the world root. "" for the world root.
|
|
214
|
-
:param dimension_name: The name of the dimension shown to the user
|
|
215
|
-
"""
|
|
216
|
-
if dimension_name is None:
|
|
217
|
-
dimension_name: Dimension = relative_dimension_path
|
|
218
|
-
|
|
219
|
-
if relative_dimension_path:
|
|
220
|
-
path = os.path.join(self.path, relative_dimension_path)
|
|
221
|
-
else:
|
|
222
|
-
path = self.path
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
relative_dimension_path not in self._levels
|
|
226
|
-
and dimension_name not in self._dimension_name_map
|
|
227
|
-
):
|
|
228
|
-
self._levels[relative_dimension_path] = AnvilDimensionManager(
|
|
229
|
-
path,
|
|
230
|
-
mcc=self._mcc_support,
|
|
231
|
-
layers=("region",) + ("entities",) * (self.version >= 2681),
|
|
232
|
-
)
|
|
233
|
-
self._dimension_name_map[dimension_name] = relative_dimension_path
|
|
234
|
-
self._bounds[dimension_name] = self._get_dimenion_bounds(dimension_name)
|
|
235
|
-
|
|
236
|
-
def _get_dimenion_bounds(self, dimension_type_str: Dimension) -> SelectionGroup:
|
|
237
|
-
if self.version >= 2709: # This number might be smaller
|
|
238
|
-
# If in a version that supports custom height data packs
|
|
239
|
-
dimension_settings = (
|
|
240
|
-
self.root_tag.compound.get_compound("Data", CompoundTag())
|
|
241
|
-
.get_compound("WorldGenSettings", CompoundTag())
|
|
242
|
-
.get_compound("dimensions", CompoundTag())
|
|
243
|
-
.get_compound(dimension_type_str, CompoundTag())
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
# "type" can be a reference (string) or inline (compound) dimension-type data.
|
|
247
|
-
dimension_type = dimension_settings.get("type")
|
|
248
|
-
|
|
249
|
-
if isinstance(dimension_type, StringTag):
|
|
250
|
-
# Reference type. Load the dimension data
|
|
251
|
-
dimension_type_str = dimension_type.py_str
|
|
252
|
-
if ":" in dimension_type_str:
|
|
253
|
-
namespace, base_name = dimension_type_str.split(":", 1)
|
|
254
|
-
else:
|
|
255
|
-
namespace = "minecraft"
|
|
256
|
-
base_name = dimension_type_str
|
|
257
|
-
name_tuple = namespace, base_name
|
|
258
|
-
|
|
259
|
-
# First try and load the reference from the data pack and then from defaults
|
|
260
|
-
dimension_path = f"data/{namespace}/dimension_type/{base_name}.json"
|
|
261
|
-
if self.data_pack.has_file(dimension_path):
|
|
262
|
-
with self.data_pack.open(dimension_path) as d:
|
|
263
|
-
try:
|
|
264
|
-
dimension_settings_json = json.load(d)
|
|
265
|
-
except json.JSONDecodeError:
|
|
266
|
-
pass
|
|
267
|
-
else:
|
|
268
|
-
if "min_y" in dimension_settings_json and isinstance(
|
|
269
|
-
dimension_settings_json["min_y"], int
|
|
270
|
-
):
|
|
271
|
-
min_y = dimension_settings_json["min_y"]
|
|
272
|
-
if min_y % 16:
|
|
273
|
-
min_y = 16 * (min_y // 16)
|
|
274
|
-
else:
|
|
275
|
-
min_y = 0
|
|
276
|
-
if "height" in dimension_settings_json and isinstance(
|
|
277
|
-
dimension_settings_json["height"], int
|
|
278
|
-
):
|
|
279
|
-
height = dimension_settings_json["height"]
|
|
280
|
-
if height % 16:
|
|
281
|
-
height = -16 * (-height // 16)
|
|
282
|
-
else:
|
|
283
|
-
height = 256
|
|
284
|
-
|
|
285
|
-
return SelectionGroup(
|
|
286
|
-
SelectionBox(
|
|
287
|
-
(-30_000_000, min_y, -30_000_000),
|
|
288
|
-
(30_000_000, min_y + height, 30_000_000),
|
|
289
|
-
)
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
elif name_tuple in {
|
|
293
|
-
("minecraft", "overworld"),
|
|
294
|
-
("minecraft", "overworld_caves"),
|
|
295
|
-
}:
|
|
296
|
-
if self.version >= 2825:
|
|
297
|
-
# If newer than the height change version
|
|
298
|
-
return SelectionGroup(
|
|
299
|
-
SelectionBox(
|
|
300
|
-
(-30_000_000, -64, -30_000_000),
|
|
301
|
-
(30_000_000, 320, 30_000_000),
|
|
302
|
-
)
|
|
303
|
-
)
|
|
304
|
-
else:
|
|
305
|
-
return DefaultSelection
|
|
306
|
-
elif name_tuple in {
|
|
307
|
-
("minecraft", "the_nether"),
|
|
308
|
-
("minecraft", "the_end"),
|
|
309
|
-
}:
|
|
310
|
-
return DefaultSelection
|
|
311
|
-
else:
|
|
312
|
-
log.error(f"Could not find dimension_type {':'.join(name_tuple)}")
|
|
313
|
-
|
|
314
|
-
elif isinstance(dimension_type, CompoundTag):
|
|
315
|
-
# Inline type
|
|
316
|
-
dimension_type_compound = dimension_type
|
|
317
|
-
min_y = (
|
|
318
|
-
dimension_type_compound.get_int("min_y", IntTag()).py_int // 16
|
|
319
|
-
) * 16
|
|
320
|
-
height = (
|
|
321
|
-
-dimension_type_compound.get_int("height", IntTag(256)).py_int // 16
|
|
322
|
-
) * -16
|
|
323
|
-
return SelectionGroup(
|
|
324
|
-
SelectionBox(
|
|
325
|
-
(-30_000_000, min_y, -30_000_000),
|
|
326
|
-
(30_000_000, min_y + height, 30_000_000),
|
|
327
|
-
)
|
|
328
|
-
)
|
|
329
|
-
else:
|
|
330
|
-
log.error(
|
|
331
|
-
f'level_dat["Data"]["WorldGenSettings"]["dimensions"]["{dimension_type_str}"]["type"] was not a StringTag or CompoundTag.'
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
# Return the default if nothing else returned
|
|
335
|
-
return DefaultSelection
|
|
336
|
-
|
|
337
|
-
def _get_interface(self, raw_chunk_data: Optional[Any] = None) -> "Interface":
|
|
338
|
-
from amulet.level.loader import Interfaces
|
|
339
|
-
|
|
340
|
-
key = self._get_interface_key(raw_chunk_data)
|
|
341
|
-
return Interfaces.get(key)
|
|
342
|
-
|
|
343
|
-
def _get_interface_key(
|
|
344
|
-
self, raw_chunk_data: Optional[ChunkDataType] = None
|
|
345
|
-
) -> Tuple[str, int]:
|
|
346
|
-
if raw_chunk_data is None:
|
|
347
|
-
return self.max_world_version
|
|
348
|
-
else:
|
|
349
|
-
return (
|
|
350
|
-
self.platform,
|
|
351
|
-
raw_chunk_data.get("region", NamedTag())
|
|
352
|
-
.compound.get_int("DataVersion", IntTag(-1))
|
|
353
|
-
.py_int,
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
def _decode(
|
|
357
|
-
self,
|
|
358
|
-
interface: BaseAnvilInterface,
|
|
359
|
-
dimension: Dimension,
|
|
360
|
-
cx: int,
|
|
361
|
-
cz: int,
|
|
362
|
-
raw_chunk_data: ChunkDataType,
|
|
363
|
-
) -> Tuple[Chunk, AnyNDArray]:
|
|
364
|
-
bounds = self.bounds(dimension).bounds
|
|
365
|
-
return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
|
|
366
|
-
|
|
367
|
-
def _encode(
|
|
368
|
-
self,
|
|
369
|
-
interface: BaseAnvilInterface,
|
|
370
|
-
chunk: Chunk,
|
|
371
|
-
dimension: Dimension,
|
|
372
|
-
chunk_palette: AnyNDArray,
|
|
373
|
-
) -> ChunkDataType:
|
|
374
|
-
bounds = self.bounds(dimension).bounds
|
|
375
|
-
return interface.encode(
|
|
376
|
-
chunk, chunk_palette, self.max_world_version, (bounds[0][1], bounds[1][1])
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
def _reload_world(self):
|
|
380
|
-
# reload the level.dat in case it has changed
|
|
381
|
-
self._load_level_dat()
|
|
382
|
-
|
|
383
|
-
# create the session.lock file (this has mostly been lifted from MCEdit)
|
|
384
|
-
try:
|
|
385
|
-
# open the file for writing and reading and lock it
|
|
386
|
-
self._lock = open(os.path.join(self.path, "session.lock"), "wb+")
|
|
387
|
-
portalocker.lock(self._lock, portalocker.LockFlags.EXCLUSIVE)
|
|
388
|
-
|
|
389
|
-
# write the current time to the file
|
|
390
|
-
self._lock_time = struct.pack(">Q", int(time.time() * 1000))
|
|
391
|
-
self._lock.write(self._lock_time)
|
|
392
|
-
|
|
393
|
-
# flush the changes to disk
|
|
394
|
-
self._lock.flush()
|
|
395
|
-
os.fsync(self._lock.fileno())
|
|
396
|
-
|
|
397
|
-
except Exception as e:
|
|
398
|
-
self._lock_time = None
|
|
399
|
-
if self._lock is not None:
|
|
400
|
-
self._lock.close()
|
|
401
|
-
self._lock = None
|
|
402
|
-
|
|
403
|
-
self._is_open = False
|
|
404
|
-
self._has_lock = False
|
|
405
|
-
raise Exception(
|
|
406
|
-
f"Could not access session.lock. The world may be open somewhere else.\n{e}"
|
|
407
|
-
) from e
|
|
408
|
-
|
|
409
|
-
self._is_open = True
|
|
410
|
-
self._has_lock = True
|
|
411
|
-
|
|
412
|
-
# the real number might actually be lower
|
|
413
|
-
self._mcc_support = self.version > 2203
|
|
414
|
-
|
|
415
|
-
self._levels.clear()
|
|
416
|
-
self._bounds.clear()
|
|
417
|
-
|
|
418
|
-
# load all the levels
|
|
419
|
-
self._register_dimension("", OVERWORLD)
|
|
420
|
-
self._register_dimension("DIM-1", THE_NETHER)
|
|
421
|
-
self._register_dimension("DIM1", THE_END)
|
|
422
|
-
|
|
423
|
-
for level_path in glob.glob(os.path.join(glob.escape(self.path), "DIM*")):
|
|
424
|
-
if os.path.isdir(level_path):
|
|
425
|
-
dir_name = os.path.basename(level_path)
|
|
426
|
-
if AnvilDimensionManager.level_regex.fullmatch(dir_name) is None:
|
|
427
|
-
continue
|
|
428
|
-
self._register_dimension(dir_name)
|
|
429
|
-
|
|
430
|
-
for region_path in glob.glob(
|
|
431
|
-
os.path.join(
|
|
432
|
-
glob.escape(self.path), "dimensions", "*", "*", "**", "region"
|
|
433
|
-
),
|
|
434
|
-
recursive=True,
|
|
435
|
-
):
|
|
436
|
-
if not os.path.isdir(region_path):
|
|
437
|
-
continue
|
|
438
|
-
dimension_path = os.path.dirname(region_path)
|
|
439
|
-
rel_dim_path = os.path.relpath(dimension_path, self.path)
|
|
440
|
-
_, dimension, *base_name = rel_dim_path.split(os.sep)
|
|
441
|
-
|
|
442
|
-
dimension_name = f"{dimension}:{'/'.join(base_name)}"
|
|
443
|
-
self._register_dimension(rel_dim_path, dimension_name)
|
|
444
|
-
|
|
445
|
-
def _open(self):
|
|
446
|
-
"""Open the database for reading and writing"""
|
|
447
|
-
self._reload_world()
|
|
448
|
-
|
|
449
|
-
def _create(
|
|
450
|
-
self,
|
|
451
|
-
overwrite: bool,
|
|
452
|
-
bounds: Union[
|
|
453
|
-
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
454
|
-
] = None,
|
|
455
|
-
**kwargs,
|
|
456
|
-
):
|
|
457
|
-
if os.path.isdir(self.path):
|
|
458
|
-
if overwrite:
|
|
459
|
-
shutil.rmtree(self.path)
|
|
460
|
-
else:
|
|
461
|
-
raise ObjectWriteError(
|
|
462
|
-
f"A world already exists at the path {self.path}"
|
|
463
|
-
)
|
|
464
|
-
self._version = self.translation_manager.get_version(
|
|
465
|
-
self.platform, self.version
|
|
466
|
-
).data_version
|
|
467
|
-
|
|
468
|
-
self.root_tag = root = CompoundTag()
|
|
469
|
-
root["Data"] = data = CompoundTag()
|
|
470
|
-
data["version"] = IntTag(19133)
|
|
471
|
-
data["DataVersion"] = IntTag(self._version)
|
|
472
|
-
data["LastPlayed"] = LongTag(int(time.time() * 1000))
|
|
473
|
-
data["LevelName"] = StringTag("World Created By Amulet")
|
|
474
|
-
|
|
475
|
-
os.makedirs(self.path, exist_ok=True)
|
|
476
|
-
self.root_tag.save_to(os.path.join(self.path, "level.dat"))
|
|
477
|
-
self._reload_world()
|
|
478
|
-
|
|
479
|
-
@property
|
|
480
|
-
def has_lock(self) -> bool:
|
|
481
|
-
if self._has_lock:
|
|
482
|
-
self._lock.seek(0)
|
|
483
|
-
return self._lock.read(8) == self._lock_time
|
|
484
|
-
return False
|
|
485
|
-
|
|
486
|
-
def pre_save_operation(
|
|
487
|
-
self, level: api_level.BaseLevel
|
|
488
|
-
) -> Generator[float, None, bool]:
|
|
489
|
-
changed_chunks = list(level.chunks.changed_chunks())
|
|
490
|
-
height = self._calculate_height(level, changed_chunks)
|
|
491
|
-
try:
|
|
492
|
-
while True:
|
|
493
|
-
yield next(height) / 2
|
|
494
|
-
except StopIteration as e:
|
|
495
|
-
height_changed = e.value
|
|
496
|
-
|
|
497
|
-
# light = self._calculate_light(level, changed_chunks)
|
|
498
|
-
# try:
|
|
499
|
-
# while True:
|
|
500
|
-
# yield next(light) / 2
|
|
501
|
-
# except StopIteration as e:
|
|
502
|
-
# light_changed = e.value
|
|
503
|
-
|
|
504
|
-
return height_changed # or light_changed
|
|
505
|
-
|
|
506
|
-
@staticmethod
|
|
507
|
-
def _calculate_height(
|
|
508
|
-
level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
|
|
509
|
-
) -> Generator[float, None, bool]:
|
|
510
|
-
"""Calculate the height values for chunks."""
|
|
511
|
-
chunk_count = len(chunks)
|
|
512
|
-
# it looks like the game recalculates the height value if not defined.
|
|
513
|
-
# Just delete the stored height values so that they do not get written back.
|
|
514
|
-
# tested as of 1.12.2. This may not be true for older versions.
|
|
515
|
-
changed = False
|
|
516
|
-
for i, (dimension, cx, cz) in enumerate(chunks):
|
|
517
|
-
try:
|
|
518
|
-
chunk = level.get_chunk(cx, cz, dimension)
|
|
519
|
-
except ChunkLoadError:
|
|
520
|
-
pass
|
|
521
|
-
else:
|
|
522
|
-
changed_ = False
|
|
523
|
-
changed_ |= chunk.misc.pop("height_mapC", None) is not None
|
|
524
|
-
changed_ |= chunk.misc.pop("height_map256IA", None) is not None
|
|
525
|
-
if changed_:
|
|
526
|
-
changed = True
|
|
527
|
-
chunk.changed = True
|
|
528
|
-
yield i / chunk_count
|
|
529
|
-
return changed
|
|
530
|
-
|
|
531
|
-
@staticmethod
|
|
532
|
-
def _calculate_light(
|
|
533
|
-
level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
|
|
534
|
-
) -> Generator[float, None, bool]:
|
|
535
|
-
"""Calculate the height values for chunks."""
|
|
536
|
-
# this is needed for before 1.14
|
|
537
|
-
chunk_count = len(chunks)
|
|
538
|
-
changed = False
|
|
539
|
-
if level.level_wrapper.version < 1934:
|
|
540
|
-
# the version may be less than 1934 but is at least 1924
|
|
541
|
-
# calculate the light values
|
|
542
|
-
pass
|
|
543
|
-
# TODO
|
|
544
|
-
else:
|
|
545
|
-
# the game will recalculate the light levels
|
|
546
|
-
for i, (dimension, cx, cz) in enumerate(chunks):
|
|
547
|
-
try:
|
|
548
|
-
chunk = level.get_chunk(cx, cz, dimension)
|
|
549
|
-
except ChunkLoadError:
|
|
550
|
-
pass
|
|
551
|
-
else:
|
|
552
|
-
changed_ = False
|
|
553
|
-
changed_ |= chunk.misc.pop("block_light", None) is not None
|
|
554
|
-
changed_ |= chunk.misc.pop("sky_light", None) is not None
|
|
555
|
-
if changed_:
|
|
556
|
-
changed = True
|
|
557
|
-
chunk.changed = True
|
|
558
|
-
yield i / chunk_count
|
|
559
|
-
return changed
|
|
560
|
-
|
|
561
|
-
def _save(self):
|
|
562
|
-
"""Save the data back to the disk database"""
|
|
563
|
-
os.makedirs(self.path, exist_ok=True)
|
|
564
|
-
self.root_tag.save_to(os.path.join(self.path, "level.dat"))
|
|
565
|
-
# TODO: save other world data
|
|
566
|
-
|
|
567
|
-
def _close(self):
|
|
568
|
-
"""Close the disk database"""
|
|
569
|
-
if self._lock is not None:
|
|
570
|
-
portalocker.unlock(self._lock)
|
|
571
|
-
self._lock.close()
|
|
572
|
-
|
|
573
|
-
def unload(self):
|
|
574
|
-
for level in self._levels.values():
|
|
575
|
-
level.unload()
|
|
576
|
-
|
|
577
|
-
def _has_dimension(self, dimension: Dimension):
|
|
578
|
-
return (
|
|
579
|
-
dimension in self._dimension_name_map
|
|
580
|
-
and self._dimension_name_map[dimension] in self._levels
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
def _get_dimension(self, dimension: Dimension):
|
|
584
|
-
self._verify_has_lock()
|
|
585
|
-
if self._has_dimension(dimension):
|
|
586
|
-
return self._levels[self._dimension_name_map[dimension]]
|
|
587
|
-
else:
|
|
588
|
-
raise DimensionDoesNotExist(dimension)
|
|
589
|
-
|
|
590
|
-
def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
|
|
591
|
-
if self._has_dimension(dimension):
|
|
592
|
-
yield from self._get_dimension(dimension).all_chunk_coords()
|
|
593
|
-
|
|
594
|
-
def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
|
|
595
|
-
return self._has_dimension(dimension) and self._get_dimension(
|
|
596
|
-
dimension
|
|
597
|
-
).has_chunk(cx, cz)
|
|
598
|
-
|
|
599
|
-
def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
600
|
-
"""Delete a chunk from a given dimension"""
|
|
601
|
-
if self._has_dimension(dimension):
|
|
602
|
-
self._get_dimension(dimension).delete_chunk(cx, cz)
|
|
603
|
-
|
|
604
|
-
# TODO: add a new version of this method that handles all the raw data
|
|
605
|
-
def put_raw_chunk_data(
|
|
606
|
-
self, cx: int, cz: int, data: NamedTag, dimension: Dimension
|
|
607
|
-
):
|
|
608
|
-
"""
|
|
609
|
-
Commit the raw chunk data to the FormatWrapper cache.
|
|
610
|
-
|
|
611
|
-
Call :meth:`save` to push all the cache data to the level.
|
|
612
|
-
|
|
613
|
-
:param cx: The x coordinate of the chunk.
|
|
614
|
-
:param cz: The z coordinate of the chunk.
|
|
615
|
-
:param data: The raw data to commit to the level.
|
|
616
|
-
:param dimension: The dimension to load the data from.
|
|
617
|
-
"""
|
|
618
|
-
self._verify_has_lock()
|
|
619
|
-
self._put_raw_chunk_data(cx, cz, {"region": data}, dimension)
|
|
620
|
-
|
|
621
|
-
def _put_raw_chunk_data(
|
|
622
|
-
self, cx: int, cz: int, data: ChunkDataType, dimension: Dimension
|
|
623
|
-
):
|
|
624
|
-
self._get_dimension(dimension).put_chunk_data_layers(cx, cz, data)
|
|
625
|
-
|
|
626
|
-
# TODO: add a new version of this method that handles all the raw data
|
|
627
|
-
def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> NamedTag:
|
|
628
|
-
"""
|
|
629
|
-
Return the raw data as loaded from disk.
|
|
630
|
-
|
|
631
|
-
:param cx: The x coordinate of the chunk.
|
|
632
|
-
:param cz: The z coordinate of the chunk.
|
|
633
|
-
:param dimension: The dimension to load the data from.
|
|
634
|
-
:return: The raw chunk data.
|
|
635
|
-
"""
|
|
636
|
-
# TODO: make this return layer data
|
|
637
|
-
return self._safe_load(
|
|
638
|
-
self._legacy_get_raw_chunk_data,
|
|
639
|
-
(cx, cz, dimension),
|
|
640
|
-
"Error loading chunk {} {} {}",
|
|
641
|
-
ChunkLoadError,
|
|
642
|
-
ChunkDoesNotExist,
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
def _legacy_get_raw_chunk_data(
|
|
646
|
-
self, cx: int, cz: int, dimension: Dimension
|
|
647
|
-
) -> NamedTag:
|
|
648
|
-
layers = self._get_raw_chunk_data(cx, cz, dimension)
|
|
649
|
-
if "region" in layers:
|
|
650
|
-
return layers["region"]
|
|
651
|
-
else:
|
|
652
|
-
raise ChunkDoesNotExist
|
|
653
|
-
|
|
654
|
-
def _get_raw_chunk_data(
|
|
655
|
-
self, cx: int, cz: int, dimension: Dimension
|
|
656
|
-
) -> ChunkDataType:
|
|
657
|
-
"""
|
|
658
|
-
Return the raw data as loaded from disk.
|
|
659
|
-
|
|
660
|
-
:param cx: The x coordinate of the chunk.
|
|
661
|
-
:param cz: The z coordinate of the chunk.
|
|
662
|
-
:param dimension: The dimension to load the data from.
|
|
663
|
-
:return: The raw chunk data.
|
|
664
|
-
"""
|
|
665
|
-
return self._get_dimension(dimension).get_chunk_data_layers(cx, cz)
|
|
666
|
-
|
|
667
|
-
def all_player_ids(self) -> Iterable[str]:
|
|
668
|
-
"""
|
|
669
|
-
Returns a generator of all player ids that are present in the level
|
|
670
|
-
"""
|
|
671
|
-
for f in glob.iglob(
|
|
672
|
-
os.path.join(glob.escape(self.path), "playerdata", "*.dat")
|
|
673
|
-
):
|
|
674
|
-
yield os.path.splitext(os.path.basename(f))[0]
|
|
675
|
-
if self.has_player(LOCAL_PLAYER):
|
|
676
|
-
yield LOCAL_PLAYER
|
|
677
|
-
|
|
678
|
-
def has_player(self, player_id: str) -> bool:
|
|
679
|
-
if player_id == LOCAL_PLAYER:
|
|
680
|
-
return "Player" in self.root_tag.compound.get_compound("Data")
|
|
681
|
-
else:
|
|
682
|
-
return os.path.isfile(
|
|
683
|
-
os.path.join(self.path, "playerdata", f"{player_id}.dat")
|
|
684
|
-
)
|
|
685
|
-
|
|
686
|
-
def _load_player(self, player_id: str) -> Player:
|
|
687
|
-
"""
|
|
688
|
-
Gets the :class:`Player` object that belongs to the specified player id
|
|
689
|
-
|
|
690
|
-
If no parameter is supplied, the data of the local player will be returned
|
|
691
|
-
|
|
692
|
-
:param player_id: The desired player id
|
|
693
|
-
:return: A Player instance
|
|
694
|
-
"""
|
|
695
|
-
player_nbt = self._get_raw_player_data(player_id)
|
|
696
|
-
dimension = player_nbt["Dimension"]
|
|
697
|
-
# TODO: rework this when there is better dimension support.
|
|
698
|
-
if isinstance(dimension, IntTag):
|
|
699
|
-
if -1 <= dimension.py_int <= 1:
|
|
700
|
-
dimension_str = {-1: THE_NETHER, 0: OVERWORLD, 1: THE_END}[
|
|
701
|
-
dimension.py_int
|
|
702
|
-
]
|
|
703
|
-
else:
|
|
704
|
-
dimension_str = f"DIM{dimension}"
|
|
705
|
-
elif isinstance(dimension, StringTag):
|
|
706
|
-
dimension_str = dimension.py_str
|
|
707
|
-
else:
|
|
708
|
-
dimension_str = OVERWORLD
|
|
709
|
-
if dimension_str not in self._dimension_name_map:
|
|
710
|
-
dimension_str = OVERWORLD
|
|
711
|
-
|
|
712
|
-
# get the players position
|
|
713
|
-
pos_data = player_nbt.get("Pos")
|
|
714
|
-
if (
|
|
715
|
-
isinstance(pos_data, ListTag)
|
|
716
|
-
and len(pos_data) == 3
|
|
717
|
-
and pos_data.list_data_type == DoubleTag.tag_id
|
|
718
|
-
):
|
|
719
|
-
position = tuple(map(float, pos_data))
|
|
720
|
-
position = tuple(
|
|
721
|
-
p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
|
|
722
|
-
)
|
|
723
|
-
else:
|
|
724
|
-
position = (0.0, 0.0, 0.0)
|
|
725
|
-
|
|
726
|
-
# get the players rotation
|
|
727
|
-
rot_data = player_nbt.get("Rotation")
|
|
728
|
-
if (
|
|
729
|
-
isinstance(rot_data, ListTag)
|
|
730
|
-
and len(rot_data) == 2
|
|
731
|
-
and rot_data.list_data_type == DoubleTag.tag_id
|
|
732
|
-
):
|
|
733
|
-
rotation = tuple(map(float, rot_data))
|
|
734
|
-
rotation = tuple(
|
|
735
|
-
p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
|
|
736
|
-
)
|
|
737
|
-
else:
|
|
738
|
-
rotation = (0.0, 0.0)
|
|
739
|
-
|
|
740
|
-
return Player(
|
|
741
|
-
player_id,
|
|
742
|
-
dimension_str,
|
|
743
|
-
position,
|
|
744
|
-
rotation,
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
def _get_raw_player_data(self, player_id: str) -> CompoundTag:
|
|
748
|
-
if player_id == LOCAL_PLAYER:
|
|
749
|
-
if "Player" in self.root_tag.compound.get_compound("Data"):
|
|
750
|
-
return self.root_tag.compound.get_compound("Data").get_compound(
|
|
751
|
-
"Player"
|
|
752
|
-
)
|
|
753
|
-
else:
|
|
754
|
-
raise PlayerDoesNotExist("Local player doesn't exist")
|
|
755
|
-
else:
|
|
756
|
-
path = os.path.join(self.path, "playerdata", f"{player_id}.dat")
|
|
757
|
-
if os.path.exists(path):
|
|
758
|
-
return load_nbt(path).compound
|
|
759
|
-
raise PlayerDoesNotExist(f"Player {player_id} does not exist")
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if __name__ == "__main__":
|
|
763
|
-
import sys
|
|
764
|
-
|
|
765
|
-
world_path = sys.argv[1]
|
|
766
|
-
world = AnvilDimensionManager(world_path)
|
|
767
|
-
chunk_ = world.get_chunk_data(0, 0)
|
|
768
|
-
print(chunk_)
|
|
769
|
-
world.put_chunk_data(0, 0, chunk_)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
from typing import (
|
|
6
|
+
Tuple,
|
|
7
|
+
Dict,
|
|
8
|
+
Generator,
|
|
9
|
+
Optional,
|
|
10
|
+
List,
|
|
11
|
+
Union,
|
|
12
|
+
Iterable,
|
|
13
|
+
BinaryIO,
|
|
14
|
+
Any,
|
|
15
|
+
)
|
|
16
|
+
import time
|
|
17
|
+
import glob
|
|
18
|
+
import shutil
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
import portalocker
|
|
23
|
+
|
|
24
|
+
from amulet_nbt import (
|
|
25
|
+
IntTag,
|
|
26
|
+
LongTag,
|
|
27
|
+
DoubleTag,
|
|
28
|
+
StringTag,
|
|
29
|
+
ListTag,
|
|
30
|
+
CompoundTag,
|
|
31
|
+
NamedTag,
|
|
32
|
+
load as load_nbt,
|
|
33
|
+
)
|
|
34
|
+
from amulet.api.player import Player, LOCAL_PLAYER
|
|
35
|
+
from amulet.api.chunk import Chunk
|
|
36
|
+
from amulet.api.selection import SelectionGroup, SelectionBox
|
|
37
|
+
from amulet.api.wrapper import WorldFormatWrapper, DefaultSelection
|
|
38
|
+
from amulet.utils.format_utils import check_all_exist
|
|
39
|
+
from amulet.api.errors import (
|
|
40
|
+
DimensionDoesNotExist,
|
|
41
|
+
ObjectWriteError,
|
|
42
|
+
ChunkLoadError,
|
|
43
|
+
ChunkDoesNotExist,
|
|
44
|
+
PlayerDoesNotExist,
|
|
45
|
+
)
|
|
46
|
+
from amulet.api.data_types import (
|
|
47
|
+
ChunkCoordinates,
|
|
48
|
+
VersionNumberInt,
|
|
49
|
+
PlatformType,
|
|
50
|
+
DimensionCoordinates,
|
|
51
|
+
AnyNDArray,
|
|
52
|
+
Dimension,
|
|
53
|
+
)
|
|
54
|
+
from .dimension import AnvilDimensionManager, ChunkDataType
|
|
55
|
+
from amulet.api import level as api_level
|
|
56
|
+
from amulet.level.interfaces.chunk.anvil.base_anvil_interface import BaseAnvilInterface
|
|
57
|
+
from .data_pack import DataPack, DataPackManager
|
|
58
|
+
|
|
59
|
+
log = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
InternalDimension = str
|
|
62
|
+
OVERWORLD = "minecraft:overworld"
|
|
63
|
+
THE_NETHER = "minecraft:the_nether"
|
|
64
|
+
THE_END = "minecraft:the_end"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AnvilFormat(WorldFormatWrapper[VersionNumberInt]):
|
|
68
|
+
"""
|
|
69
|
+
This FormatWrapper class exists to interface with the Java world format.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, path: str):
|
|
73
|
+
"""
|
|
74
|
+
Construct a new instance of :class:`AnvilFormat`.
|
|
75
|
+
|
|
76
|
+
This should not be used directly. You should instead use :func:`amulet.load_format`.
|
|
77
|
+
|
|
78
|
+
:param path: The file path to the serialised data.
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(path)
|
|
81
|
+
self._platform = "java"
|
|
82
|
+
self._root_tag: NamedTag = NamedTag()
|
|
83
|
+
self._levels: Dict[InternalDimension, AnvilDimensionManager] = {}
|
|
84
|
+
self._dimension_name_map: Dict[Dimension, InternalDimension] = {}
|
|
85
|
+
self._mcc_support: Optional[bool] = None
|
|
86
|
+
self._lock_time: Optional[bytes] = None
|
|
87
|
+
self._lock: Optional[BinaryIO] = None
|
|
88
|
+
self._data_pack: Optional[DataPackManager] = None
|
|
89
|
+
self._shallow_load()
|
|
90
|
+
|
|
91
|
+
def __del__(self):
|
|
92
|
+
self.close()
|
|
93
|
+
|
|
94
|
+
def _shallow_load(self):
|
|
95
|
+
try:
|
|
96
|
+
self._load_level_dat()
|
|
97
|
+
except:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def _load_level_dat(self):
|
|
101
|
+
"""Load the level.dat file and check the image file"""
|
|
102
|
+
if os.path.isfile(os.path.join(self.path, "icon.png")):
|
|
103
|
+
self._world_image_path = os.path.join(self.path, "icon.png")
|
|
104
|
+
else:
|
|
105
|
+
self._world_image_path = self._missing_world_icon
|
|
106
|
+
self.root_tag = load_nbt(os.path.join(self.path, "level.dat"))
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def is_valid(path: str) -> bool:
|
|
110
|
+
if not check_all_exist(path, "level.dat"):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
level_dat_root = load_nbt(os.path.join(path, "level.dat")).compound
|
|
115
|
+
except:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return "Data" in level_dat_root and "FML" not in level_dat_root
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def valid_formats(self) -> Dict[PlatformType, Tuple[bool, bool]]:
|
|
122
|
+
return {"java": (True, True)}
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def version(self) -> VersionNumberInt:
|
|
126
|
+
"""The data version number that the world was last opened in. eg 2578"""
|
|
127
|
+
if self._version is None:
|
|
128
|
+
self._version = self._get_version()
|
|
129
|
+
return self._version
|
|
130
|
+
|
|
131
|
+
def _get_version(self) -> VersionNumberInt:
|
|
132
|
+
return (
|
|
133
|
+
self.root_tag.compound.get_compound("Data", CompoundTag())
|
|
134
|
+
.get_int("DataVersion", IntTag(-1))
|
|
135
|
+
.py_int
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def root_tag(self) -> NamedTag:
|
|
140
|
+
"""The level.dat data for the level."""
|
|
141
|
+
return self._root_tag
|
|
142
|
+
|
|
143
|
+
@root_tag.setter
|
|
144
|
+
def root_tag(self, root_tag: Union[NamedTag, CompoundTag]):
|
|
145
|
+
if isinstance(root_tag, CompoundTag):
|
|
146
|
+
self._root_tag = NamedTag(root_tag)
|
|
147
|
+
elif isinstance(root_tag, NamedTag):
|
|
148
|
+
self._root_tag = root_tag
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError("root_tag must be a CompoundTag or NamedTag")
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def level_name(self) -> str:
|
|
154
|
+
return (
|
|
155
|
+
self.root_tag.compound.get_compound("Data").get_string("LevelName").py_str
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@level_name.setter
|
|
159
|
+
def level_name(self, value: str):
|
|
160
|
+
self.root_tag.compound.setdefault_compound("Data")["LevelName"] = StringTag(
|
|
161
|
+
value
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def last_played(self) -> int:
|
|
166
|
+
return (
|
|
167
|
+
self.root_tag.compound.get_compound("Data")
|
|
168
|
+
.get_long("LastPlayed", LongTag())
|
|
169
|
+
.py_int
|
|
170
|
+
// 1000
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def game_version_string(self) -> str:
|
|
175
|
+
try:
|
|
176
|
+
return f'Java {self.root_tag.compound.get_compound("Data").get_compound("Version").get_string("Name").py_str}'
|
|
177
|
+
except Exception:
|
|
178
|
+
return f"Java Unknown Version"
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def data_pack(self) -> DataPackManager:
|
|
182
|
+
if self._data_pack is None:
|
|
183
|
+
packs = []
|
|
184
|
+
enabled_packs = (
|
|
185
|
+
self.root_tag.compound.get_compound("Data")
|
|
186
|
+
.get_compound("DataPacks", CompoundTag())
|
|
187
|
+
.get_list("Enabled", ListTag())
|
|
188
|
+
)
|
|
189
|
+
for pack in enabled_packs:
|
|
190
|
+
if isinstance(pack, StringTag):
|
|
191
|
+
pack_name: str = pack.py_str
|
|
192
|
+
if pack_name == "vanilla":
|
|
193
|
+
pass
|
|
194
|
+
elif pack_name.startswith("file/"):
|
|
195
|
+
path = os.path.join(self.path, "datapacks", pack_name[5:])
|
|
196
|
+
if DataPack.is_path_valid(path):
|
|
197
|
+
packs.append(DataPack(path))
|
|
198
|
+
self._data_pack = DataPackManager(packs)
|
|
199
|
+
return self._data_pack
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def dimensions(self) -> List[Dimension]:
|
|
203
|
+
return list(self._dimension_name_map.keys())
|
|
204
|
+
|
|
205
|
+
def _register_dimension(
|
|
206
|
+
self,
|
|
207
|
+
relative_dimension_path: InternalDimension,
|
|
208
|
+
dimension_name: Optional[Dimension] = None,
|
|
209
|
+
):
|
|
210
|
+
"""
|
|
211
|
+
Register a new dimension.
|
|
212
|
+
|
|
213
|
+
:param relative_dimension_path: The relative path to the dimension directory from the world root. "" for the world root.
|
|
214
|
+
:param dimension_name: The name of the dimension shown to the user
|
|
215
|
+
"""
|
|
216
|
+
if dimension_name is None:
|
|
217
|
+
dimension_name: Dimension = relative_dimension_path
|
|
218
|
+
|
|
219
|
+
if relative_dimension_path:
|
|
220
|
+
path = os.path.join(self.path, relative_dimension_path)
|
|
221
|
+
else:
|
|
222
|
+
path = self.path
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
relative_dimension_path not in self._levels
|
|
226
|
+
and dimension_name not in self._dimension_name_map
|
|
227
|
+
):
|
|
228
|
+
self._levels[relative_dimension_path] = AnvilDimensionManager(
|
|
229
|
+
path,
|
|
230
|
+
mcc=self._mcc_support,
|
|
231
|
+
layers=("region",) + ("entities",) * (self.version >= 2681),
|
|
232
|
+
)
|
|
233
|
+
self._dimension_name_map[dimension_name] = relative_dimension_path
|
|
234
|
+
self._bounds[dimension_name] = self._get_dimenion_bounds(dimension_name)
|
|
235
|
+
|
|
236
|
+
def _get_dimenion_bounds(self, dimension_type_str: Dimension) -> SelectionGroup:
|
|
237
|
+
if self.version >= 2709: # This number might be smaller
|
|
238
|
+
# If in a version that supports custom height data packs
|
|
239
|
+
dimension_settings = (
|
|
240
|
+
self.root_tag.compound.get_compound("Data", CompoundTag())
|
|
241
|
+
.get_compound("WorldGenSettings", CompoundTag())
|
|
242
|
+
.get_compound("dimensions", CompoundTag())
|
|
243
|
+
.get_compound(dimension_type_str, CompoundTag())
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# "type" can be a reference (string) or inline (compound) dimension-type data.
|
|
247
|
+
dimension_type = dimension_settings.get("type")
|
|
248
|
+
|
|
249
|
+
if isinstance(dimension_type, StringTag):
|
|
250
|
+
# Reference type. Load the dimension data
|
|
251
|
+
dimension_type_str = dimension_type.py_str
|
|
252
|
+
if ":" in dimension_type_str:
|
|
253
|
+
namespace, base_name = dimension_type_str.split(":", 1)
|
|
254
|
+
else:
|
|
255
|
+
namespace = "minecraft"
|
|
256
|
+
base_name = dimension_type_str
|
|
257
|
+
name_tuple = namespace, base_name
|
|
258
|
+
|
|
259
|
+
# First try and load the reference from the data pack and then from defaults
|
|
260
|
+
dimension_path = f"data/{namespace}/dimension_type/{base_name}.json"
|
|
261
|
+
if self.data_pack.has_file(dimension_path):
|
|
262
|
+
with self.data_pack.open(dimension_path) as d:
|
|
263
|
+
try:
|
|
264
|
+
dimension_settings_json = json.load(d)
|
|
265
|
+
except json.JSONDecodeError:
|
|
266
|
+
pass
|
|
267
|
+
else:
|
|
268
|
+
if "min_y" in dimension_settings_json and isinstance(
|
|
269
|
+
dimension_settings_json["min_y"], int
|
|
270
|
+
):
|
|
271
|
+
min_y = dimension_settings_json["min_y"]
|
|
272
|
+
if min_y % 16:
|
|
273
|
+
min_y = 16 * (min_y // 16)
|
|
274
|
+
else:
|
|
275
|
+
min_y = 0
|
|
276
|
+
if "height" in dimension_settings_json and isinstance(
|
|
277
|
+
dimension_settings_json["height"], int
|
|
278
|
+
):
|
|
279
|
+
height = dimension_settings_json["height"]
|
|
280
|
+
if height % 16:
|
|
281
|
+
height = -16 * (-height // 16)
|
|
282
|
+
else:
|
|
283
|
+
height = 256
|
|
284
|
+
|
|
285
|
+
return SelectionGroup(
|
|
286
|
+
SelectionBox(
|
|
287
|
+
(-30_000_000, min_y, -30_000_000),
|
|
288
|
+
(30_000_000, min_y + height, 30_000_000),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
elif name_tuple in {
|
|
293
|
+
("minecraft", "overworld"),
|
|
294
|
+
("minecraft", "overworld_caves"),
|
|
295
|
+
}:
|
|
296
|
+
if self.version >= 2825:
|
|
297
|
+
# If newer than the height change version
|
|
298
|
+
return SelectionGroup(
|
|
299
|
+
SelectionBox(
|
|
300
|
+
(-30_000_000, -64, -30_000_000),
|
|
301
|
+
(30_000_000, 320, 30_000_000),
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
return DefaultSelection
|
|
306
|
+
elif name_tuple in {
|
|
307
|
+
("minecraft", "the_nether"),
|
|
308
|
+
("minecraft", "the_end"),
|
|
309
|
+
}:
|
|
310
|
+
return DefaultSelection
|
|
311
|
+
else:
|
|
312
|
+
log.error(f"Could not find dimension_type {':'.join(name_tuple)}")
|
|
313
|
+
|
|
314
|
+
elif isinstance(dimension_type, CompoundTag):
|
|
315
|
+
# Inline type
|
|
316
|
+
dimension_type_compound = dimension_type
|
|
317
|
+
min_y = (
|
|
318
|
+
dimension_type_compound.get_int("min_y", IntTag()).py_int // 16
|
|
319
|
+
) * 16
|
|
320
|
+
height = (
|
|
321
|
+
-dimension_type_compound.get_int("height", IntTag(256)).py_int // 16
|
|
322
|
+
) * -16
|
|
323
|
+
return SelectionGroup(
|
|
324
|
+
SelectionBox(
|
|
325
|
+
(-30_000_000, min_y, -30_000_000),
|
|
326
|
+
(30_000_000, min_y + height, 30_000_000),
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
log.error(
|
|
331
|
+
f'level_dat["Data"]["WorldGenSettings"]["dimensions"]["{dimension_type_str}"]["type"] was not a StringTag or CompoundTag.'
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Return the default if nothing else returned
|
|
335
|
+
return DefaultSelection
|
|
336
|
+
|
|
337
|
+
def _get_interface(self, raw_chunk_data: Optional[Any] = None) -> "Interface":
|
|
338
|
+
from amulet.level.loader import Interfaces
|
|
339
|
+
|
|
340
|
+
key = self._get_interface_key(raw_chunk_data)
|
|
341
|
+
return Interfaces.get(key)
|
|
342
|
+
|
|
343
|
+
def _get_interface_key(
|
|
344
|
+
self, raw_chunk_data: Optional[ChunkDataType] = None
|
|
345
|
+
) -> Tuple[str, int]:
|
|
346
|
+
if raw_chunk_data is None:
|
|
347
|
+
return self.max_world_version
|
|
348
|
+
else:
|
|
349
|
+
return (
|
|
350
|
+
self.platform,
|
|
351
|
+
raw_chunk_data.get("region", NamedTag())
|
|
352
|
+
.compound.get_int("DataVersion", IntTag(-1))
|
|
353
|
+
.py_int,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _decode(
|
|
357
|
+
self,
|
|
358
|
+
interface: BaseAnvilInterface,
|
|
359
|
+
dimension: Dimension,
|
|
360
|
+
cx: int,
|
|
361
|
+
cz: int,
|
|
362
|
+
raw_chunk_data: ChunkDataType,
|
|
363
|
+
) -> Tuple[Chunk, AnyNDArray]:
|
|
364
|
+
bounds = self.bounds(dimension).bounds
|
|
365
|
+
return interface.decode(cx, cz, raw_chunk_data, (bounds[0][1], bounds[1][1]))
|
|
366
|
+
|
|
367
|
+
def _encode(
|
|
368
|
+
self,
|
|
369
|
+
interface: BaseAnvilInterface,
|
|
370
|
+
chunk: Chunk,
|
|
371
|
+
dimension: Dimension,
|
|
372
|
+
chunk_palette: AnyNDArray,
|
|
373
|
+
) -> ChunkDataType:
|
|
374
|
+
bounds = self.bounds(dimension).bounds
|
|
375
|
+
return interface.encode(
|
|
376
|
+
chunk, chunk_palette, self.max_world_version, (bounds[0][1], bounds[1][1])
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _reload_world(self):
|
|
380
|
+
# reload the level.dat in case it has changed
|
|
381
|
+
self._load_level_dat()
|
|
382
|
+
|
|
383
|
+
# create the session.lock file (this has mostly been lifted from MCEdit)
|
|
384
|
+
try:
|
|
385
|
+
# open the file for writing and reading and lock it
|
|
386
|
+
self._lock = open(os.path.join(self.path, "session.lock"), "wb+")
|
|
387
|
+
portalocker.lock(self._lock, portalocker.LockFlags.EXCLUSIVE)
|
|
388
|
+
|
|
389
|
+
# write the current time to the file
|
|
390
|
+
self._lock_time = struct.pack(">Q", int(time.time() * 1000))
|
|
391
|
+
self._lock.write(self._lock_time)
|
|
392
|
+
|
|
393
|
+
# flush the changes to disk
|
|
394
|
+
self._lock.flush()
|
|
395
|
+
os.fsync(self._lock.fileno())
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
self._lock_time = None
|
|
399
|
+
if self._lock is not None:
|
|
400
|
+
self._lock.close()
|
|
401
|
+
self._lock = None
|
|
402
|
+
|
|
403
|
+
self._is_open = False
|
|
404
|
+
self._has_lock = False
|
|
405
|
+
raise Exception(
|
|
406
|
+
f"Could not access session.lock. The world may be open somewhere else.\n{e}"
|
|
407
|
+
) from e
|
|
408
|
+
|
|
409
|
+
self._is_open = True
|
|
410
|
+
self._has_lock = True
|
|
411
|
+
|
|
412
|
+
# the real number might actually be lower
|
|
413
|
+
self._mcc_support = self.version > 2203
|
|
414
|
+
|
|
415
|
+
self._levels.clear()
|
|
416
|
+
self._bounds.clear()
|
|
417
|
+
|
|
418
|
+
# load all the levels
|
|
419
|
+
self._register_dimension("", OVERWORLD)
|
|
420
|
+
self._register_dimension("DIM-1", THE_NETHER)
|
|
421
|
+
self._register_dimension("DIM1", THE_END)
|
|
422
|
+
|
|
423
|
+
for level_path in glob.glob(os.path.join(glob.escape(self.path), "DIM*")):
|
|
424
|
+
if os.path.isdir(level_path):
|
|
425
|
+
dir_name = os.path.basename(level_path)
|
|
426
|
+
if AnvilDimensionManager.level_regex.fullmatch(dir_name) is None:
|
|
427
|
+
continue
|
|
428
|
+
self._register_dimension(dir_name)
|
|
429
|
+
|
|
430
|
+
for region_path in glob.glob(
|
|
431
|
+
os.path.join(
|
|
432
|
+
glob.escape(self.path), "dimensions", "*", "*", "**", "region"
|
|
433
|
+
),
|
|
434
|
+
recursive=True,
|
|
435
|
+
):
|
|
436
|
+
if not os.path.isdir(region_path):
|
|
437
|
+
continue
|
|
438
|
+
dimension_path = os.path.dirname(region_path)
|
|
439
|
+
rel_dim_path = os.path.relpath(dimension_path, self.path)
|
|
440
|
+
_, dimension, *base_name = rel_dim_path.split(os.sep)
|
|
441
|
+
|
|
442
|
+
dimension_name = f"{dimension}:{'/'.join(base_name)}"
|
|
443
|
+
self._register_dimension(rel_dim_path, dimension_name)
|
|
444
|
+
|
|
445
|
+
def _open(self):
|
|
446
|
+
"""Open the database for reading and writing"""
|
|
447
|
+
self._reload_world()
|
|
448
|
+
|
|
449
|
+
def _create(
|
|
450
|
+
self,
|
|
451
|
+
overwrite: bool,
|
|
452
|
+
bounds: Union[
|
|
453
|
+
SelectionGroup, Dict[Dimension, Optional[SelectionGroup]], None
|
|
454
|
+
] = None,
|
|
455
|
+
**kwargs,
|
|
456
|
+
):
|
|
457
|
+
if os.path.isdir(self.path):
|
|
458
|
+
if overwrite:
|
|
459
|
+
shutil.rmtree(self.path)
|
|
460
|
+
else:
|
|
461
|
+
raise ObjectWriteError(
|
|
462
|
+
f"A world already exists at the path {self.path}"
|
|
463
|
+
)
|
|
464
|
+
self._version = self.translation_manager.get_version(
|
|
465
|
+
self.platform, self.version
|
|
466
|
+
).data_version
|
|
467
|
+
|
|
468
|
+
self.root_tag = root = CompoundTag()
|
|
469
|
+
root["Data"] = data = CompoundTag()
|
|
470
|
+
data["version"] = IntTag(19133)
|
|
471
|
+
data["DataVersion"] = IntTag(self._version)
|
|
472
|
+
data["LastPlayed"] = LongTag(int(time.time() * 1000))
|
|
473
|
+
data["LevelName"] = StringTag("World Created By Amulet")
|
|
474
|
+
|
|
475
|
+
os.makedirs(self.path, exist_ok=True)
|
|
476
|
+
self.root_tag.save_to(os.path.join(self.path, "level.dat"))
|
|
477
|
+
self._reload_world()
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def has_lock(self) -> bool:
|
|
481
|
+
if self._has_lock:
|
|
482
|
+
self._lock.seek(0)
|
|
483
|
+
return self._lock.read(8) == self._lock_time
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
def pre_save_operation(
|
|
487
|
+
self, level: api_level.BaseLevel
|
|
488
|
+
) -> Generator[float, None, bool]:
|
|
489
|
+
changed_chunks = list(level.chunks.changed_chunks())
|
|
490
|
+
height = self._calculate_height(level, changed_chunks)
|
|
491
|
+
try:
|
|
492
|
+
while True:
|
|
493
|
+
yield next(height) / 2
|
|
494
|
+
except StopIteration as e:
|
|
495
|
+
height_changed = e.value
|
|
496
|
+
|
|
497
|
+
# light = self._calculate_light(level, changed_chunks)
|
|
498
|
+
# try:
|
|
499
|
+
# while True:
|
|
500
|
+
# yield next(light) / 2
|
|
501
|
+
# except StopIteration as e:
|
|
502
|
+
# light_changed = e.value
|
|
503
|
+
|
|
504
|
+
return height_changed # or light_changed
|
|
505
|
+
|
|
506
|
+
@staticmethod
|
|
507
|
+
def _calculate_height(
|
|
508
|
+
level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
|
|
509
|
+
) -> Generator[float, None, bool]:
|
|
510
|
+
"""Calculate the height values for chunks."""
|
|
511
|
+
chunk_count = len(chunks)
|
|
512
|
+
# it looks like the game recalculates the height value if not defined.
|
|
513
|
+
# Just delete the stored height values so that they do not get written back.
|
|
514
|
+
# tested as of 1.12.2. This may not be true for older versions.
|
|
515
|
+
changed = False
|
|
516
|
+
for i, (dimension, cx, cz) in enumerate(chunks):
|
|
517
|
+
try:
|
|
518
|
+
chunk = level.get_chunk(cx, cz, dimension)
|
|
519
|
+
except ChunkLoadError:
|
|
520
|
+
pass
|
|
521
|
+
else:
|
|
522
|
+
changed_ = False
|
|
523
|
+
changed_ |= chunk.misc.pop("height_mapC", None) is not None
|
|
524
|
+
changed_ |= chunk.misc.pop("height_map256IA", None) is not None
|
|
525
|
+
if changed_:
|
|
526
|
+
changed = True
|
|
527
|
+
chunk.changed = True
|
|
528
|
+
yield i / chunk_count
|
|
529
|
+
return changed
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def _calculate_light(
|
|
533
|
+
level: api_level.BaseLevel, chunks: List[DimensionCoordinates]
|
|
534
|
+
) -> Generator[float, None, bool]:
|
|
535
|
+
"""Calculate the height values for chunks."""
|
|
536
|
+
# this is needed for before 1.14
|
|
537
|
+
chunk_count = len(chunks)
|
|
538
|
+
changed = False
|
|
539
|
+
if level.level_wrapper.version < 1934:
|
|
540
|
+
# the version may be less than 1934 but is at least 1924
|
|
541
|
+
# calculate the light values
|
|
542
|
+
pass
|
|
543
|
+
# TODO
|
|
544
|
+
else:
|
|
545
|
+
# the game will recalculate the light levels
|
|
546
|
+
for i, (dimension, cx, cz) in enumerate(chunks):
|
|
547
|
+
try:
|
|
548
|
+
chunk = level.get_chunk(cx, cz, dimension)
|
|
549
|
+
except ChunkLoadError:
|
|
550
|
+
pass
|
|
551
|
+
else:
|
|
552
|
+
changed_ = False
|
|
553
|
+
changed_ |= chunk.misc.pop("block_light", None) is not None
|
|
554
|
+
changed_ |= chunk.misc.pop("sky_light", None) is not None
|
|
555
|
+
if changed_:
|
|
556
|
+
changed = True
|
|
557
|
+
chunk.changed = True
|
|
558
|
+
yield i / chunk_count
|
|
559
|
+
return changed
|
|
560
|
+
|
|
561
|
+
def _save(self):
|
|
562
|
+
"""Save the data back to the disk database"""
|
|
563
|
+
os.makedirs(self.path, exist_ok=True)
|
|
564
|
+
self.root_tag.save_to(os.path.join(self.path, "level.dat"))
|
|
565
|
+
# TODO: save other world data
|
|
566
|
+
|
|
567
|
+
def _close(self):
|
|
568
|
+
"""Close the disk database"""
|
|
569
|
+
if self._lock is not None:
|
|
570
|
+
portalocker.unlock(self._lock)
|
|
571
|
+
self._lock.close()
|
|
572
|
+
|
|
573
|
+
def unload(self):
|
|
574
|
+
for level in self._levels.values():
|
|
575
|
+
level.unload()
|
|
576
|
+
|
|
577
|
+
def _has_dimension(self, dimension: Dimension):
|
|
578
|
+
return (
|
|
579
|
+
dimension in self._dimension_name_map
|
|
580
|
+
and self._dimension_name_map[dimension] in self._levels
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def _get_dimension(self, dimension: Dimension):
|
|
584
|
+
self._verify_has_lock()
|
|
585
|
+
if self._has_dimension(dimension):
|
|
586
|
+
return self._levels[self._dimension_name_map[dimension]]
|
|
587
|
+
else:
|
|
588
|
+
raise DimensionDoesNotExist(dimension)
|
|
589
|
+
|
|
590
|
+
def all_chunk_coords(self, dimension: Dimension) -> Iterable[ChunkCoordinates]:
|
|
591
|
+
if self._has_dimension(dimension):
|
|
592
|
+
yield from self._get_dimension(dimension).all_chunk_coords()
|
|
593
|
+
|
|
594
|
+
def has_chunk(self, cx: int, cz: int, dimension: Dimension) -> bool:
|
|
595
|
+
return self._has_dimension(dimension) and self._get_dimension(
|
|
596
|
+
dimension
|
|
597
|
+
).has_chunk(cx, cz)
|
|
598
|
+
|
|
599
|
+
def _delete_chunk(self, cx: int, cz: int, dimension: Dimension):
|
|
600
|
+
"""Delete a chunk from a given dimension"""
|
|
601
|
+
if self._has_dimension(dimension):
|
|
602
|
+
self._get_dimension(dimension).delete_chunk(cx, cz)
|
|
603
|
+
|
|
604
|
+
# TODO: add a new version of this method that handles all the raw data
|
|
605
|
+
def put_raw_chunk_data(
|
|
606
|
+
self, cx: int, cz: int, data: NamedTag, dimension: Dimension
|
|
607
|
+
):
|
|
608
|
+
"""
|
|
609
|
+
Commit the raw chunk data to the FormatWrapper cache.
|
|
610
|
+
|
|
611
|
+
Call :meth:`save` to push all the cache data to the level.
|
|
612
|
+
|
|
613
|
+
:param cx: The x coordinate of the chunk.
|
|
614
|
+
:param cz: The z coordinate of the chunk.
|
|
615
|
+
:param data: The raw data to commit to the level.
|
|
616
|
+
:param dimension: The dimension to load the data from.
|
|
617
|
+
"""
|
|
618
|
+
self._verify_has_lock()
|
|
619
|
+
self._put_raw_chunk_data(cx, cz, {"region": data}, dimension)
|
|
620
|
+
|
|
621
|
+
def _put_raw_chunk_data(
|
|
622
|
+
self, cx: int, cz: int, data: ChunkDataType, dimension: Dimension
|
|
623
|
+
):
|
|
624
|
+
self._get_dimension(dimension).put_chunk_data_layers(cx, cz, data)
|
|
625
|
+
|
|
626
|
+
# TODO: add a new version of this method that handles all the raw data
|
|
627
|
+
def get_raw_chunk_data(self, cx: int, cz: int, dimension: Dimension) -> NamedTag:
|
|
628
|
+
"""
|
|
629
|
+
Return the raw data as loaded from disk.
|
|
630
|
+
|
|
631
|
+
:param cx: The x coordinate of the chunk.
|
|
632
|
+
:param cz: The z coordinate of the chunk.
|
|
633
|
+
:param dimension: The dimension to load the data from.
|
|
634
|
+
:return: The raw chunk data.
|
|
635
|
+
"""
|
|
636
|
+
# TODO: make this return layer data
|
|
637
|
+
return self._safe_load(
|
|
638
|
+
self._legacy_get_raw_chunk_data,
|
|
639
|
+
(cx, cz, dimension),
|
|
640
|
+
"Error loading chunk {} {} {}",
|
|
641
|
+
ChunkLoadError,
|
|
642
|
+
ChunkDoesNotExist,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def _legacy_get_raw_chunk_data(
|
|
646
|
+
self, cx: int, cz: int, dimension: Dimension
|
|
647
|
+
) -> NamedTag:
|
|
648
|
+
layers = self._get_raw_chunk_data(cx, cz, dimension)
|
|
649
|
+
if "region" in layers:
|
|
650
|
+
return layers["region"]
|
|
651
|
+
else:
|
|
652
|
+
raise ChunkDoesNotExist
|
|
653
|
+
|
|
654
|
+
def _get_raw_chunk_data(
|
|
655
|
+
self, cx: int, cz: int, dimension: Dimension
|
|
656
|
+
) -> ChunkDataType:
|
|
657
|
+
"""
|
|
658
|
+
Return the raw data as loaded from disk.
|
|
659
|
+
|
|
660
|
+
:param cx: The x coordinate of the chunk.
|
|
661
|
+
:param cz: The z coordinate of the chunk.
|
|
662
|
+
:param dimension: The dimension to load the data from.
|
|
663
|
+
:return: The raw chunk data.
|
|
664
|
+
"""
|
|
665
|
+
return self._get_dimension(dimension).get_chunk_data_layers(cx, cz)
|
|
666
|
+
|
|
667
|
+
def all_player_ids(self) -> Iterable[str]:
|
|
668
|
+
"""
|
|
669
|
+
Returns a generator of all player ids that are present in the level
|
|
670
|
+
"""
|
|
671
|
+
for f in glob.iglob(
|
|
672
|
+
os.path.join(glob.escape(self.path), "playerdata", "*.dat")
|
|
673
|
+
):
|
|
674
|
+
yield os.path.splitext(os.path.basename(f))[0]
|
|
675
|
+
if self.has_player(LOCAL_PLAYER):
|
|
676
|
+
yield LOCAL_PLAYER
|
|
677
|
+
|
|
678
|
+
def has_player(self, player_id: str) -> bool:
|
|
679
|
+
if player_id == LOCAL_PLAYER:
|
|
680
|
+
return "Player" in self.root_tag.compound.get_compound("Data")
|
|
681
|
+
else:
|
|
682
|
+
return os.path.isfile(
|
|
683
|
+
os.path.join(self.path, "playerdata", f"{player_id}.dat")
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
def _load_player(self, player_id: str) -> Player:
|
|
687
|
+
"""
|
|
688
|
+
Gets the :class:`Player` object that belongs to the specified player id
|
|
689
|
+
|
|
690
|
+
If no parameter is supplied, the data of the local player will be returned
|
|
691
|
+
|
|
692
|
+
:param player_id: The desired player id
|
|
693
|
+
:return: A Player instance
|
|
694
|
+
"""
|
|
695
|
+
player_nbt = self._get_raw_player_data(player_id)
|
|
696
|
+
dimension = player_nbt["Dimension"]
|
|
697
|
+
# TODO: rework this when there is better dimension support.
|
|
698
|
+
if isinstance(dimension, IntTag):
|
|
699
|
+
if -1 <= dimension.py_int <= 1:
|
|
700
|
+
dimension_str = {-1: THE_NETHER, 0: OVERWORLD, 1: THE_END}[
|
|
701
|
+
dimension.py_int
|
|
702
|
+
]
|
|
703
|
+
else:
|
|
704
|
+
dimension_str = f"DIM{dimension}"
|
|
705
|
+
elif isinstance(dimension, StringTag):
|
|
706
|
+
dimension_str = dimension.py_str
|
|
707
|
+
else:
|
|
708
|
+
dimension_str = OVERWORLD
|
|
709
|
+
if dimension_str not in self._dimension_name_map:
|
|
710
|
+
dimension_str = OVERWORLD
|
|
711
|
+
|
|
712
|
+
# get the players position
|
|
713
|
+
pos_data = player_nbt.get("Pos")
|
|
714
|
+
if (
|
|
715
|
+
isinstance(pos_data, ListTag)
|
|
716
|
+
and len(pos_data) == 3
|
|
717
|
+
and pos_data.list_data_type == DoubleTag.tag_id
|
|
718
|
+
):
|
|
719
|
+
position = tuple(map(float, pos_data))
|
|
720
|
+
position = tuple(
|
|
721
|
+
p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in position
|
|
722
|
+
)
|
|
723
|
+
else:
|
|
724
|
+
position = (0.0, 0.0, 0.0)
|
|
725
|
+
|
|
726
|
+
# get the players rotation
|
|
727
|
+
rot_data = player_nbt.get("Rotation")
|
|
728
|
+
if (
|
|
729
|
+
isinstance(rot_data, ListTag)
|
|
730
|
+
and len(rot_data) == 2
|
|
731
|
+
and rot_data.list_data_type == DoubleTag.tag_id
|
|
732
|
+
):
|
|
733
|
+
rotation = tuple(map(float, rot_data))
|
|
734
|
+
rotation = tuple(
|
|
735
|
+
p if -100_000_000 <= p <= 100_000_000 else 0.0 for p in rotation
|
|
736
|
+
)
|
|
737
|
+
else:
|
|
738
|
+
rotation = (0.0, 0.0)
|
|
739
|
+
|
|
740
|
+
return Player(
|
|
741
|
+
player_id,
|
|
742
|
+
dimension_str,
|
|
743
|
+
position,
|
|
744
|
+
rotation,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
def _get_raw_player_data(self, player_id: str) -> CompoundTag:
|
|
748
|
+
if player_id == LOCAL_PLAYER:
|
|
749
|
+
if "Player" in self.root_tag.compound.get_compound("Data"):
|
|
750
|
+
return self.root_tag.compound.get_compound("Data").get_compound(
|
|
751
|
+
"Player"
|
|
752
|
+
)
|
|
753
|
+
else:
|
|
754
|
+
raise PlayerDoesNotExist("Local player doesn't exist")
|
|
755
|
+
else:
|
|
756
|
+
path = os.path.join(self.path, "playerdata", f"{player_id}.dat")
|
|
757
|
+
if os.path.exists(path):
|
|
758
|
+
return load_nbt(path).compound
|
|
759
|
+
raise PlayerDoesNotExist(f"Player {player_id} does not exist")
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
if __name__ == "__main__":
|
|
763
|
+
import sys
|
|
764
|
+
|
|
765
|
+
world_path = sys.argv[1]
|
|
766
|
+
world = AnvilDimensionManager(world_path)
|
|
767
|
+
chunk_ = world.get_chunk_data(0, 0)
|
|
768
|
+
print(chunk_)
|
|
769
|
+
world.put_chunk_data(0, 0, chunk_)
|