amulet-core 2.0a3__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of amulet-core might be problematic. Click here for more details.
- amulet/__init__.cp312-win_amd64.pyd +0 -0
- amulet/__init__.pyi +30 -0
- amulet/__pyinstaller/__init__.py +2 -0
- amulet/__pyinstaller/hook-amulet.py +4 -0
- amulet/_init.py +28 -0
- amulet/_version.py +21 -0
- amulet/biome.cpp +36 -0
- amulet/biome.hpp +43 -0
- amulet/biome.pyi +77 -0
- amulet/block.cpp +435 -0
- amulet/block.hpp +119 -0
- amulet/block.pyi +273 -0
- amulet/block_entity.cpp +12 -0
- amulet/block_entity.hpp +56 -0
- amulet/block_entity.pyi +80 -0
- amulet/chunk.cpp +16 -0
- amulet/chunk.hpp +99 -0
- amulet/chunk.pyi +30 -0
- amulet/chunk_/components/biome.py +155 -0
- amulet/chunk_/components/block_entity.py +117 -0
- amulet/chunk_/components/entity.py +64 -0
- amulet/chunk_/components/height_2d.py +16 -0
- amulet/chunk_components.pyi +95 -0
- amulet/collections.pyi +37 -0
- amulet/data_types.py +29 -0
- amulet/entity.py +180 -0
- amulet/errors.py +63 -0
- amulet/game/__init__.py +7 -0
- amulet/game/_game.py +152 -0
- amulet/game/_universal/__init__.py +1 -0
- amulet/game/_universal/_biome.py +17 -0
- amulet/game/_universal/_block.py +47 -0
- amulet/game/_universal/_version.py +68 -0
- amulet/game/abc/__init__.py +22 -0
- amulet/game/abc/_block_specification.py +150 -0
- amulet/game/abc/biome.py +213 -0
- amulet/game/abc/block.py +331 -0
- amulet/game/abc/game_version_container.py +25 -0
- amulet/game/abc/json_interface.py +27 -0
- amulet/game/abc/version.py +44 -0
- amulet/game/bedrock/__init__.py +1 -0
- amulet/game/bedrock/_biome.py +35 -0
- amulet/game/bedrock/_block.py +42 -0
- amulet/game/bedrock/_version.py +165 -0
- amulet/game/java/__init__.py +2 -0
- amulet/game/java/_biome.py +35 -0
- amulet/game/java/_block.py +60 -0
- amulet/game/java/_version.py +176 -0
- amulet/game/translate/__init__.py +12 -0
- amulet/game/translate/_functions/__init__.py +15 -0
- amulet/game/translate/_functions/_code_functions/__init__.py +0 -0
- amulet/game/translate/_functions/_code_functions/_text.py +553 -0
- amulet/game/translate/_functions/_code_functions/banner_pattern.py +67 -0
- amulet/game/translate/_functions/_code_functions/bedrock_chest_connection.py +152 -0
- amulet/game/translate/_functions/_code_functions/bedrock_moving_block_pos.py +88 -0
- amulet/game/translate/_functions/_code_functions/bedrock_sign.py +152 -0
- amulet/game/translate/_functions/_code_functions/bedrock_skull_rotation.py +16 -0
- amulet/game/translate/_functions/_code_functions/custom_name.py +146 -0
- amulet/game/translate/_functions/_frozen.py +66 -0
- amulet/game/translate/_functions/_state.py +54 -0
- amulet/game/translate/_functions/_typing.py +98 -0
- amulet/game/translate/_functions/abc.py +116 -0
- amulet/game/translate/_functions/carry_nbt.py +160 -0
- amulet/game/translate/_functions/carry_properties.py +80 -0
- amulet/game/translate/_functions/code.py +143 -0
- amulet/game/translate/_functions/map_block_name.py +66 -0
- amulet/game/translate/_functions/map_nbt.py +111 -0
- amulet/game/translate/_functions/map_properties.py +93 -0
- amulet/game/translate/_functions/multiblock.py +112 -0
- amulet/game/translate/_functions/new_block.py +42 -0
- amulet/game/translate/_functions/new_entity.py +43 -0
- amulet/game/translate/_functions/new_nbt.py +206 -0
- amulet/game/translate/_functions/new_properties.py +64 -0
- amulet/game/translate/_functions/sequence.py +51 -0
- amulet/game/translate/_functions/walk_input_nbt.py +331 -0
- amulet/game/translate/_translator.py +433 -0
- amulet/item.py +75 -0
- amulet/level/__init__.pyi +27 -0
- amulet/level/_load.py +100 -0
- amulet/level/abc/__init__.py +12 -0
- amulet/level/abc/_chunk_handle.py +335 -0
- amulet/level/abc/_dimension.py +86 -0
- amulet/level/abc/_history/__init__.py +1 -0
- amulet/level/abc/_history/_cache.py +224 -0
- amulet/level/abc/_history/_history_manager.py +291 -0
- amulet/level/abc/_level/__init__.py +5 -0
- amulet/level/abc/_level/_compactable_level.py +10 -0
- amulet/level/abc/_level/_creatable_level.py +29 -0
- amulet/level/abc/_level/_disk_level.py +17 -0
- amulet/level/abc/_level/_level.py +453 -0
- amulet/level/abc/_level/_loadable_level.py +42 -0
- amulet/level/abc/_player_storage.py +7 -0
- amulet/level/abc/_raw_level.py +187 -0
- amulet/level/abc/_registry.py +40 -0
- amulet/level/bedrock/__init__.py +2 -0
- amulet/level/bedrock/_chunk_handle.py +19 -0
- amulet/level/bedrock/_dimension.py +22 -0
- amulet/level/bedrock/_level.py +187 -0
- amulet/level/bedrock/_raw/__init__.py +5 -0
- amulet/level/bedrock/_raw/_actor_counter.py +53 -0
- amulet/level/bedrock/_raw/_chunk.py +54 -0
- amulet/level/bedrock/_raw/_chunk_decode.py +668 -0
- amulet/level/bedrock/_raw/_chunk_encode.py +602 -0
- amulet/level/bedrock/_raw/_constant.py +9 -0
- amulet/level/bedrock/_raw/_dimension.py +343 -0
- amulet/level/bedrock/_raw/_level.py +463 -0
- amulet/level/bedrock/_raw/_level_dat.py +90 -0
- amulet/level/bedrock/_raw/_typing.py +6 -0
- amulet/level/bedrock/_raw/leveldb_chunk_versions.py +83 -0
- amulet/level/bedrock/chunk/__init__.py +1 -0
- amulet/level/bedrock/chunk/_chunk.py +126 -0
- amulet/level/bedrock/chunk/components/__init__.py +0 -0
- amulet/level/bedrock/chunk/components/chunk_version.py +12 -0
- amulet/level/bedrock/chunk/components/finalised_state.py +13 -0
- amulet/level/bedrock/chunk/components/raw_chunk.py +15 -0
- amulet/level/construction/__init__.py +0 -0
- amulet/level/java/__init__.pyi +21 -0
- amulet/level/java/_chunk_handle.py +17 -0
- amulet/level/java/_chunk_handle.pyi +15 -0
- amulet/level/java/_dimension.py +20 -0
- amulet/level/java/_dimension.pyi +13 -0
- amulet/level/java/_level.py +184 -0
- amulet/level/java/_level.pyi +120 -0
- amulet/level/java/_raw/__init__.pyi +19 -0
- amulet/level/java/_raw/_chunk.pyi +23 -0
- amulet/level/java/_raw/_chunk_decode.py +561 -0
- amulet/level/java/_raw/_chunk_encode.py +463 -0
- amulet/level/java/_raw/_constant.py +9 -0
- amulet/level/java/_raw/_constant.pyi +20 -0
- amulet/level/java/_raw/_data_pack/__init__.py +2 -0
- amulet/level/java/_raw/_data_pack/__init__.pyi +8 -0
- amulet/level/java/_raw/_data_pack/data_pack.py +241 -0
- amulet/level/java/_raw/_data_pack/data_pack.pyi +197 -0
- amulet/level/java/_raw/_data_pack/data_pack_manager.py +77 -0
- amulet/level/java/_raw/_data_pack/data_pack_manager.pyi +75 -0
- amulet/level/java/_raw/_dimension.py +86 -0
- amulet/level/java/_raw/_dimension.pyi +72 -0
- amulet/level/java/_raw/_level.py +507 -0
- amulet/level/java/_raw/_level.pyi +238 -0
- amulet/level/java/_raw/_typing.py +3 -0
- amulet/level/java/_raw/_typing.pyi +5 -0
- amulet/level/java/anvil/__init__.py +2 -0
- amulet/level/java/anvil/__init__.pyi +11 -0
- amulet/level/java/anvil/_dimension.py +170 -0
- amulet/level/java/anvil/_dimension.pyi +109 -0
- amulet/level/java/anvil/_region.py +421 -0
- amulet/level/java/anvil/_region.pyi +197 -0
- amulet/level/java/anvil/_sector_manager.py +223 -0
- amulet/level/java/anvil/_sector_manager.pyi +142 -0
- amulet/level/java/chunk.pyi +81 -0
- amulet/level/java/chunk_/_chunk.py +260 -0
- amulet/level/java/chunk_/components/inhabited_time.py +12 -0
- amulet/level/java/chunk_/components/last_update.py +12 -0
- amulet/level/java/chunk_/components/legacy_version.py +12 -0
- amulet/level/java/chunk_/components/light_populated.py +12 -0
- amulet/level/java/chunk_/components/named_height_2d.py +37 -0
- amulet/level/java/chunk_/components/status.py +11 -0
- amulet/level/java/chunk_/components/terrain_populated.py +12 -0
- amulet/level/java/chunk_components.pyi +22 -0
- amulet/level/java/long_array.pyi +38 -0
- amulet/level/java_forge/__init__.py +0 -0
- amulet/level/mcstructure/__init__.py +0 -0
- amulet/level/nbt/__init__.py +0 -0
- amulet/level/schematic/__init__.py +0 -0
- amulet/level/sponge_schematic/__init__.py +0 -0
- amulet/level/temporary_level/__init__.py +1 -0
- amulet/level/temporary_level/_level.py +16 -0
- amulet/palette/__init__.pyi +8 -0
- amulet/palette/biome_palette.pyi +45 -0
- amulet/palette/block_palette.pyi +45 -0
- amulet/player.py +64 -0
- amulet/py.typed +0 -0
- amulet/selection/__init__.py +2 -0
- amulet/selection/abstract_selection.py +342 -0
- amulet/selection/box.py +852 -0
- amulet/selection/group.py +481 -0
- amulet/utils/__init__.pyi +28 -0
- amulet/utils/call_spec/__init__.py +24 -0
- amulet/utils/call_spec/__init__.pyi +53 -0
- amulet/utils/call_spec/_call_spec.py +262 -0
- amulet/utils/call_spec/_call_spec.pyi +272 -0
- amulet/utils/format_utils.py +41 -0
- amulet/utils/generator.py +18 -0
- amulet/utils/matrix.py +243 -0
- amulet/utils/matrix.pyi +177 -0
- amulet/utils/numpy.pyi +11 -0
- amulet/utils/numpy_helpers.py +19 -0
- amulet/utils/shareable_lock.py +335 -0
- amulet/utils/shareable_lock.pyi +190 -0
- amulet/utils/signal/__init__.py +10 -0
- amulet/utils/signal/__init__.pyi +25 -0
- amulet/utils/signal/_signal.py +228 -0
- amulet/utils/signal/_signal.pyi +84 -0
- amulet/utils/task_manager.py +235 -0
- amulet/utils/task_manager.pyi +168 -0
- amulet/utils/typed_property.py +111 -0
- amulet/utils/typing.py +4 -0
- amulet/utils/typing.pyi +6 -0
- amulet/utils/weakref.py +70 -0
- amulet/utils/weakref.pyi +50 -0
- amulet/utils/world_utils.py +102 -0
- amulet/utils/world_utils.pyi +109 -0
- amulet/version.cpp +136 -0
- amulet/version.hpp +142 -0
- amulet/version.pyi +94 -0
- amulet_core-2.0a3.dist-info/METADATA +103 -0
- amulet_core-2.0a3.dist-info/RECORD +210 -0
- amulet_core-2.0a3.dist-info/WHEEL +5 -0
- amulet_core-2.0a3.dist-info/entry_points.txt +2 -0
- amulet_core-2.0a3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from typing import Optional, TYPE_CHECKING, Generic, TypeVar, Callable, Self
|
|
5
|
+
from collections.abc import Iterator, Iterable
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
|
|
10
|
+
from amulet.utils.shareable_lock import LockNotAcquired
|
|
11
|
+
from amulet.chunk import Chunk, get_null_chunk
|
|
12
|
+
from amulet.data_types import DimensionId
|
|
13
|
+
from amulet.errors import ChunkDoesNotExist, ChunkLoadError
|
|
14
|
+
from amulet.utils.signal import Signal
|
|
15
|
+
|
|
16
|
+
from ._level import LevelFriend, LevelT
|
|
17
|
+
from ._history import HistoryManagerLayer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ._level import Level
|
|
22
|
+
from ._raw_level import RawDimension
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ChunkT = TypeVar("ChunkT", bound=Chunk)
|
|
26
|
+
RawDimensionT = TypeVar("RawDimensionT", bound="RawDimension")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChunkKey(tuple[int, int]):
|
|
30
|
+
def __new__(cls, cx: int, cz: int) -> Self:
|
|
31
|
+
return tuple.__new__(cls, (cx, cz))
|
|
32
|
+
|
|
33
|
+
def __init__(self, cx: int, cz: int) -> None:
|
|
34
|
+
self._bytes: Optional[bytes] = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def cx(self) -> int:
|
|
38
|
+
return self[0]
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def cz(self) -> int:
|
|
42
|
+
return self[1]
|
|
43
|
+
|
|
44
|
+
def __bytes__(self) -> bytes:
|
|
45
|
+
if self._bytes is None:
|
|
46
|
+
self._bytes = b"/".join((str(self[0]).encode(), str(self[1]).encode()))
|
|
47
|
+
return self._bytes
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ChunkHandle(
|
|
51
|
+
LevelFriend[LevelT],
|
|
52
|
+
ABC,
|
|
53
|
+
Generic[LevelT, RawDimensionT, ChunkT],
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
A class which manages chunk data.
|
|
57
|
+
You must acquire the lock for the chunk before reading or writing data.
|
|
58
|
+
Some internal synchronisation is done to catch some threading issues.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
_lock: RLock
|
|
62
|
+
_dimension: DimensionId
|
|
63
|
+
_key: ChunkKey
|
|
64
|
+
_chunk_history: HistoryManagerLayer[ChunkKey]
|
|
65
|
+
_chunk_data_history: HistoryManagerLayer[bytes]
|
|
66
|
+
_raw_dimension: Optional[RawDimensionT]
|
|
67
|
+
|
|
68
|
+
__slots__ = (
|
|
69
|
+
"_lock",
|
|
70
|
+
"_dimension_id",
|
|
71
|
+
"_key",
|
|
72
|
+
"_chunk_history",
|
|
73
|
+
"_chunk_data_history",
|
|
74
|
+
"_raw_dimension",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
level_ref: Callable[[], LevelT | None],
|
|
80
|
+
chunk_history: HistoryManagerLayer[ChunkKey],
|
|
81
|
+
chunk_data_history: HistoryManagerLayer[bytes],
|
|
82
|
+
dimension_id: DimensionId,
|
|
83
|
+
cx: int,
|
|
84
|
+
cz: int,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__(level_ref)
|
|
87
|
+
self._lock = RLock()
|
|
88
|
+
self._dimension_id = dimension_id
|
|
89
|
+
self._key = ChunkKey(cx, cz)
|
|
90
|
+
self._chunk_history = chunk_history
|
|
91
|
+
self._chunk_data_history = chunk_data_history
|
|
92
|
+
self._raw_dimension = None
|
|
93
|
+
|
|
94
|
+
changed = Signal[()]()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def dimension_id(self) -> DimensionId:
|
|
98
|
+
return self._dimension_id
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def cx(self) -> int:
|
|
102
|
+
return self._key.cx
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def cz(self) -> int:
|
|
106
|
+
return self._key.cz
|
|
107
|
+
|
|
108
|
+
def _get_raw_dimension(self) -> RawDimensionT:
|
|
109
|
+
if self._raw_dimension is None:
|
|
110
|
+
self._raw_dimension = self._l.raw.get_dimension(self.dimension_id)
|
|
111
|
+
return self._raw_dimension
|
|
112
|
+
|
|
113
|
+
@contextmanager
|
|
114
|
+
def lock(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
blocking: bool = True,
|
|
118
|
+
timeout: float = -1,
|
|
119
|
+
) -> Iterator[None]:
|
|
120
|
+
"""
|
|
121
|
+
Lock access to the chunk.
|
|
122
|
+
|
|
123
|
+
>>> level: Level
|
|
124
|
+
>>> dimension_name: str
|
|
125
|
+
>>> cx: int
|
|
126
|
+
>>> cz: int
|
|
127
|
+
>>> with level.get_dimension(dimension_name).get_chunk_handle(cx, cz).lock():
|
|
128
|
+
>>> # Do what you need to with the chunk
|
|
129
|
+
>>> # No other threads are able to edit or set the chunk while in this with block.
|
|
130
|
+
|
|
131
|
+
If you want to lock, get and set the chunk data :meth:`edit` is probably a better fit.
|
|
132
|
+
|
|
133
|
+
:param blocking: Should this block until the lock is acquired.
|
|
134
|
+
:param timeout: The amount of time to wait for the lock.
|
|
135
|
+
:raises:
|
|
136
|
+
LockNotAcquired: If the lock could not be acquired.
|
|
137
|
+
"""
|
|
138
|
+
if not self._lock.acquire(blocking, timeout):
|
|
139
|
+
# Thread was not acquired
|
|
140
|
+
raise LockNotAcquired("Lock was not acquired.")
|
|
141
|
+
try:
|
|
142
|
+
yield
|
|
143
|
+
finally:
|
|
144
|
+
self._lock.release()
|
|
145
|
+
|
|
146
|
+
@contextmanager
|
|
147
|
+
def edit(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
components: Iterable[str] | None = None,
|
|
151
|
+
blocking: bool = True,
|
|
152
|
+
timeout: float = -1,
|
|
153
|
+
) -> Iterator[ChunkT | None]:
|
|
154
|
+
"""Lock and edit a chunk.
|
|
155
|
+
|
|
156
|
+
If you only want to access/modify parts of the chunk data you can specify the components you want to load.
|
|
157
|
+
This makes it faster because you don't need to load unneeded parts.
|
|
158
|
+
|
|
159
|
+
>>> level: Level
|
|
160
|
+
>>> dimension_name: str
|
|
161
|
+
>>> cx: int
|
|
162
|
+
>>> cz: int
|
|
163
|
+
>>> with level.get_dimension(dimension_name).get_chunk_handle(cx, cz).edit() as chunk:
|
|
164
|
+
>>> # Edit the chunk data
|
|
165
|
+
>>> # No other threads are able to edit the chunk while in this with block.
|
|
166
|
+
>>> # When the with block exits the edited chunk will be automatically set if no exception occurred.
|
|
167
|
+
|
|
168
|
+
:param components: None to load all components or an iterable of component strings to load.
|
|
169
|
+
:param blocking: Should this block until the lock is acquired.
|
|
170
|
+
:param timeout: The amount of time to wait for the lock.
|
|
171
|
+
:raises:
|
|
172
|
+
LockNotAcquired: If the lock could not be acquired.
|
|
173
|
+
"""
|
|
174
|
+
with self.lock(blocking=blocking, timeout=timeout):
|
|
175
|
+
chunk = self.get(components)
|
|
176
|
+
yield chunk
|
|
177
|
+
# If an exception occurs in user code, this line won't be run.
|
|
178
|
+
self._set(chunk)
|
|
179
|
+
self.changed.emit()
|
|
180
|
+
self._l.changed.emit()
|
|
181
|
+
|
|
182
|
+
def exists(self) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Does the chunk exist. This is a quick way to check if the chunk exists without loading it.
|
|
185
|
+
|
|
186
|
+
This state may change if the lock is not acquired.
|
|
187
|
+
|
|
188
|
+
:return: True if the chunk exists. Calling get on this chunk handle may still throw ChunkLoadError
|
|
189
|
+
"""
|
|
190
|
+
if self._chunk_history.has_resource(self._key):
|
|
191
|
+
return self._chunk_history.resource_exists(self._key)
|
|
192
|
+
else:
|
|
193
|
+
# The history system is not aware of the chunk. Look in the level data
|
|
194
|
+
return self._get_raw_dimension().has_chunk(self.cx, self.cz)
|
|
195
|
+
|
|
196
|
+
def _preload(self) -> None:
|
|
197
|
+
"""Load the chunk data if it has not already been loaded."""
|
|
198
|
+
if not self._chunk_history.has_resource(self._key):
|
|
199
|
+
# The history system is not aware of the chunk. Load from the level data
|
|
200
|
+
chunk: Chunk
|
|
201
|
+
try:
|
|
202
|
+
raw_chunk = self._get_raw_dimension().get_raw_chunk(self.cx, self.cz)
|
|
203
|
+
chunk = self._get_raw_dimension().raw_chunk_to_native_chunk(
|
|
204
|
+
raw_chunk,
|
|
205
|
+
self.cx,
|
|
206
|
+
self.cz,
|
|
207
|
+
)
|
|
208
|
+
except ChunkDoesNotExist:
|
|
209
|
+
self._chunk_history.set_initial_resource(self._key, b"")
|
|
210
|
+
except ChunkLoadError as e:
|
|
211
|
+
self._chunk_history.set_initial_resource(self._key, pickle.dumps(e))
|
|
212
|
+
else:
|
|
213
|
+
self._chunk_history.set_initial_resource(
|
|
214
|
+
self._key, pickle.dumps(chunk.chunk_id)
|
|
215
|
+
)
|
|
216
|
+
for component_id, component_data in chunk.serialise_chunk().items():
|
|
217
|
+
if component_data is None:
|
|
218
|
+
raise RuntimeError(
|
|
219
|
+
"Component must not be None when initialising chunk"
|
|
220
|
+
)
|
|
221
|
+
self._chunk_data_history.set_initial_resource(
|
|
222
|
+
b"/".join((bytes(self._key), component_id.encode())),
|
|
223
|
+
component_data,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _get_null_chunk(self) -> ChunkT:
|
|
227
|
+
"""Get a null chunk instance used for this chunk.
|
|
228
|
+
|
|
229
|
+
:raises:
|
|
230
|
+
ChunkDoesNotExist if the chunk does not exist.
|
|
231
|
+
"""
|
|
232
|
+
data = self._chunk_history.get_resource(self._key)
|
|
233
|
+
if data:
|
|
234
|
+
obj: ChunkLoadError | str = pickle.loads(data)
|
|
235
|
+
if isinstance(obj, ChunkLoadError):
|
|
236
|
+
raise obj
|
|
237
|
+
elif isinstance(obj, str):
|
|
238
|
+
return get_null_chunk(obj) # type: ignore
|
|
239
|
+
else:
|
|
240
|
+
raise RuntimeError
|
|
241
|
+
else:
|
|
242
|
+
raise ChunkDoesNotExist
|
|
243
|
+
|
|
244
|
+
def get_class(self) -> type[ChunkT]:
|
|
245
|
+
"""Get the chunk class used for this chunk.
|
|
246
|
+
|
|
247
|
+
:raises:
|
|
248
|
+
ChunkDoesNotExist if the chunk does not exist.
|
|
249
|
+
"""
|
|
250
|
+
return type(self._get_null_chunk())
|
|
251
|
+
|
|
252
|
+
def get(self, components: Iterable[str] | None = None) -> ChunkT:
|
|
253
|
+
"""Get a unique copy of the chunk data.
|
|
254
|
+
|
|
255
|
+
If you want to edit the chunk, use :meth:`edit` instead.
|
|
256
|
+
|
|
257
|
+
If you only want to access/modify parts of the chunk data you can specify the components you want to load.
|
|
258
|
+
This makes it faster because you don't need to load unneeded parts.
|
|
259
|
+
|
|
260
|
+
:param components: None to load all components or an iterable of component strings to load.
|
|
261
|
+
:return: A unique copy of the chunk data.
|
|
262
|
+
"""
|
|
263
|
+
with self.lock(blocking=False):
|
|
264
|
+
self._preload()
|
|
265
|
+
chunk = self._get_null_chunk()
|
|
266
|
+
if components is None:
|
|
267
|
+
components = chunk.component_ids
|
|
268
|
+
else:
|
|
269
|
+
# Ensure all component ids are valid for this class.
|
|
270
|
+
components = set(components).intersection(chunk.component_ids)
|
|
271
|
+
chunk_components = dict[str, bytes | None]()
|
|
272
|
+
for component_id in components:
|
|
273
|
+
chunk_components[component_id] = self._chunk_data_history.get_resource(
|
|
274
|
+
b"/".join((bytes(self._key), component_id.encode()))
|
|
275
|
+
)
|
|
276
|
+
chunk.reconstruct_chunk(chunk_components)
|
|
277
|
+
return chunk
|
|
278
|
+
|
|
279
|
+
def _set(self, chunk: ChunkT | None) -> None:
|
|
280
|
+
"""Lock must be acquired before calling this"""
|
|
281
|
+
history = self._chunk_history
|
|
282
|
+
if not history.has_resource(self._key):
|
|
283
|
+
if self._l.history_enabled:
|
|
284
|
+
self._preload()
|
|
285
|
+
else:
|
|
286
|
+
history.set_initial_resource(self._key, b"")
|
|
287
|
+
if chunk is None:
|
|
288
|
+
history.set_resource(self._key, b"")
|
|
289
|
+
else:
|
|
290
|
+
self._validate_chunk(chunk)
|
|
291
|
+
try:
|
|
292
|
+
old_chunk_class = self.get_class()
|
|
293
|
+
except ChunkLoadError:
|
|
294
|
+
old_chunk_class = None
|
|
295
|
+
new_chunk_class = type(chunk)
|
|
296
|
+
component_data = chunk.serialise_chunk()
|
|
297
|
+
if old_chunk_class != new_chunk_class and None in component_data.values():
|
|
298
|
+
raise RuntimeError(
|
|
299
|
+
"When changing chunk class all the data must be present."
|
|
300
|
+
)
|
|
301
|
+
history.set_resource(self._key, pickle.dumps(new_chunk_class))
|
|
302
|
+
for component_id, data in component_data.items():
|
|
303
|
+
if data is None:
|
|
304
|
+
continue
|
|
305
|
+
self._chunk_data_history.set_resource(
|
|
306
|
+
b"/".join((bytes(self._key), component_id.encode())),
|
|
307
|
+
data,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
@abstractmethod
|
|
312
|
+
def _validate_chunk(chunk: ChunkT) -> None:
|
|
313
|
+
raise NotImplementedError
|
|
314
|
+
|
|
315
|
+
def set(self, chunk: ChunkT) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Overwrite the chunk data.
|
|
318
|
+
You must acquire the chunk lock before setting.
|
|
319
|
+
If you want to edit the chunk, use :meth:`edit` instead.
|
|
320
|
+
|
|
321
|
+
:param chunk: The chunk data to set.
|
|
322
|
+
:raises:
|
|
323
|
+
LockNotAcquired: If the chunk is already locked by another thread.
|
|
324
|
+
"""
|
|
325
|
+
with self.lock(blocking=False):
|
|
326
|
+
self._set(chunk)
|
|
327
|
+
self.changed.emit()
|
|
328
|
+
self._l.changed.emit()
|
|
329
|
+
|
|
330
|
+
def delete(self) -> None:
|
|
331
|
+
"""Delete the chunk from the level."""
|
|
332
|
+
with self.lock(blocking=False):
|
|
333
|
+
self._set(None)
|
|
334
|
+
self.changed.emit()
|
|
335
|
+
self._l.changed.emit()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Generic, TypeVar, Callable
|
|
3
|
+
from weakref import WeakValueDictionary
|
|
4
|
+
from threading import Lock
|
|
5
|
+
|
|
6
|
+
from amulet.data_types import DimensionId
|
|
7
|
+
from amulet.block import BlockStack
|
|
8
|
+
from amulet.biome import Biome
|
|
9
|
+
from amulet.selection import SelectionGroup
|
|
10
|
+
|
|
11
|
+
from ._level import LevelFriend, LevelT
|
|
12
|
+
from ._history import HistoryManagerLayer
|
|
13
|
+
from ._chunk_handle import ChunkKey, ChunkHandle
|
|
14
|
+
from ._raw_level import RawDimension
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ChunkHandleT = TypeVar("ChunkHandleT", bound=ChunkHandle)
|
|
18
|
+
RawDimensionT = TypeVar("RawDimensionT", bound=RawDimension)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Dimension(LevelFriend[LevelT], ABC, Generic[LevelT, RawDimensionT, ChunkHandleT]):
|
|
22
|
+
_dimension_id: DimensionId
|
|
23
|
+
_chunk_handles: WeakValueDictionary[tuple[int, int], ChunkHandleT]
|
|
24
|
+
_chunk_handle_lock: Lock
|
|
25
|
+
_chunk_history: HistoryManagerLayer[ChunkKey]
|
|
26
|
+
_chunk_data_history: HistoryManagerLayer[bytes]
|
|
27
|
+
_raw: RawDimensionT
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self, level_ref: Callable[[], LevelT | None], dimension_id: DimensionId
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(level_ref)
|
|
33
|
+
self._dimension_id = dimension_id
|
|
34
|
+
self._chunk_handles = WeakValueDictionary()
|
|
35
|
+
self._chunk_handle_lock = Lock()
|
|
36
|
+
self._chunk_history = self._l._o.history_manager.new_layer()
|
|
37
|
+
self._chunk_data_history = self._l._o.history_manager.new_layer()
|
|
38
|
+
self._raw = self._l.raw.get_dimension(self._dimension_id)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def dimension_id(self) -> DimensionId:
|
|
42
|
+
return self._dimension_id
|
|
43
|
+
|
|
44
|
+
def bounds(self) -> SelectionGroup:
|
|
45
|
+
"""The editable region of the dimension."""
|
|
46
|
+
return self._raw.bounds()
|
|
47
|
+
|
|
48
|
+
def default_block(self) -> BlockStack:
|
|
49
|
+
"""The default block for this dimension"""
|
|
50
|
+
return self._raw.default_block()
|
|
51
|
+
|
|
52
|
+
def default_biome(self) -> Biome:
|
|
53
|
+
"""The default biome for this dimension"""
|
|
54
|
+
return self._raw.default_biome()
|
|
55
|
+
|
|
56
|
+
def chunk_coords(self) -> set[tuple[int, int]]:
|
|
57
|
+
"""
|
|
58
|
+
The coordinates of every chunk that exists in this dimension.
|
|
59
|
+
|
|
60
|
+
This is the combination of chunks saved to the level and chunks yet to be saved.
|
|
61
|
+
"""
|
|
62
|
+
chunks: set[tuple[int, int]] = set(self._raw.all_chunk_coords())
|
|
63
|
+
for key, state in self._chunk_history.resources_exist_map().items():
|
|
64
|
+
if state:
|
|
65
|
+
chunks.add((key.cx, key.cz))
|
|
66
|
+
else:
|
|
67
|
+
chunks.discard((key.cx, key.cz))
|
|
68
|
+
return chunks
|
|
69
|
+
|
|
70
|
+
def changed_chunk_coords(self) -> set[tuple[int, int]]:
|
|
71
|
+
"""The coordinates of every chunk in this dimension that have been changed since the last save."""
|
|
72
|
+
return {(key.cx, key.cz) for key in self._chunk_history.changed_resources()}
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def _create_chunk_handle(self, cx: int, cz: int) -> ChunkHandleT:
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def get_chunk_handle(self, cx: int, cz: int) -> ChunkHandleT:
|
|
79
|
+
key = cx, cz
|
|
80
|
+
with self._chunk_handle_lock:
|
|
81
|
+
chunk_handle = self._chunk_handles.get(key)
|
|
82
|
+
if chunk_handle is None:
|
|
83
|
+
chunk_handle = self._chunk_handles[key] = self._create_chunk_handle(
|
|
84
|
+
cx, cz
|
|
85
|
+
)
|
|
86
|
+
return chunk_handle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._history_manager import HistoryManager, HistoryManagerLayer
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from typing import Optional, Callable, cast, IO, Self
|
|
5
|
+
from threading import Lock
|
|
6
|
+
import os
|
|
7
|
+
from weakref import ref, finalize
|
|
8
|
+
import time
|
|
9
|
+
import glob
|
|
10
|
+
import re
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
import portalocker
|
|
14
|
+
from leveldb import LevelDB
|
|
15
|
+
from amulet.utils.weakref import CallableWeakMethod
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TempPattern = re.compile(r"amulettmp.*?-(?P<time>\d+)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _temp_dir() -> str:
|
|
22
|
+
temp_dir = os.environ.get("CACHE_DIR")
|
|
23
|
+
if temp_dir is None:
|
|
24
|
+
raise RuntimeError
|
|
25
|
+
return temp_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _clear_temp_dirs() -> None:
|
|
29
|
+
"""
|
|
30
|
+
Try and delete historic temporary directories.
|
|
31
|
+
If things went very wrong in past sessions temporary directories may still exist.
|
|
32
|
+
"""
|
|
33
|
+
for path in glob.glob(
|
|
34
|
+
os.path.join(glob.escape(tempfile.gettempdir()), "amulettmp*")
|
|
35
|
+
) + glob.glob(
|
|
36
|
+
os.path.join(glob.escape(_temp_dir()), "**", "amulettmp*"), recursive=True
|
|
37
|
+
):
|
|
38
|
+
name = os.path.basename(path)
|
|
39
|
+
match = TempPattern.fullmatch(name)
|
|
40
|
+
if match and int(match.group("time")) < (time.time() - 7 * 24 * 3600):
|
|
41
|
+
lock_path = os.path.join(path, "lock")
|
|
42
|
+
if os.path.exists(lock_path):
|
|
43
|
+
with open(lock_path) as lock:
|
|
44
|
+
# make sure it is not locked by another process
|
|
45
|
+
try:
|
|
46
|
+
portalocker.lock(lock, portalocker.LockFlags.EXCLUSIVE)
|
|
47
|
+
except:
|
|
48
|
+
continue
|
|
49
|
+
else:
|
|
50
|
+
portalocker.unlock(lock)
|
|
51
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_clear_temp_dirs()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TempDir(str):
|
|
58
|
+
"""
|
|
59
|
+
A temporary directory to do with as you wish.
|
|
60
|
+
|
|
61
|
+
>>> t = TempDir()
|
|
62
|
+
>>> path = os.path.join(t, "your_file.txt") # TempDir is a subclass of str
|
|
63
|
+
>>> # make sure all files in the temporary directory are closed before releasing or closing this object.
|
|
64
|
+
>>> # The temporary directory will be deleted when the last reference to `t` is lost or when `t.close()` is called
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
__lock: IO | None
|
|
68
|
+
__finalise: finalize
|
|
69
|
+
|
|
70
|
+
def __new__(cls, group: str) -> Self:
|
|
71
|
+
cache_dir = os.path.join(_temp_dir(), group)
|
|
72
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
73
|
+
return super().__new__(
|
|
74
|
+
cls,
|
|
75
|
+
tempfile.mkdtemp(
|
|
76
|
+
prefix="amulettmp",
|
|
77
|
+
suffix=f"-{time.time():.0f}",
|
|
78
|
+
dir=cache_dir,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __init__(self, group: str) -> None:
|
|
83
|
+
self.__lock = open(os.path.join(self, "lock"), "w")
|
|
84
|
+
portalocker.lock(self.__lock, portalocker.LockFlags.EXCLUSIVE)
|
|
85
|
+
self.__finalise = finalize(self, CallableWeakMethod(self._close))
|
|
86
|
+
|
|
87
|
+
def _close(self) -> None:
|
|
88
|
+
if self.__lock is not None:
|
|
89
|
+
portalocker.unlock(self.__lock)
|
|
90
|
+
self.__lock.close()
|
|
91
|
+
self.__lock = None
|
|
92
|
+
shutil.rmtree(self)
|
|
93
|
+
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
"""Close the lock and delete the directory."""
|
|
96
|
+
self.__finalise()
|
|
97
|
+
|
|
98
|
+
def __del__(self) -> None:
|
|
99
|
+
self.__finalise()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DiskCache:
|
|
103
|
+
"""
|
|
104
|
+
A key, value database with a fast access RAM component and a longer term storage disk component.
|
|
105
|
+
Keys and values are both bytes.
|
|
106
|
+
The disk component is a leveldb database.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, path: str, max_size: int) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Create a new DiskCache
|
|
112
|
+
:param path: The path to save the disk component to.
|
|
113
|
+
:param max_size: The maximum amount of RAM that values can occupy. Key size is assumed negligible.
|
|
114
|
+
When this is overflowed, the least recently used entries are unloaded to the disk storage.
|
|
115
|
+
"""
|
|
116
|
+
self._lock = Lock()
|
|
117
|
+
self._ram: dict[bytes, tuple[bytes, bool]] = {}
|
|
118
|
+
self._path = path
|
|
119
|
+
self._disk = LevelDB(path, create_if_missing=True)
|
|
120
|
+
self._max_size: int = max_size
|
|
121
|
+
self._size: int = 0
|
|
122
|
+
self.__finalise = finalize(self, CallableWeakMethod(self._close))
|
|
123
|
+
|
|
124
|
+
def _close(self) -> None:
|
|
125
|
+
self._disk.close()
|
|
126
|
+
shutil.rmtree(self._path, ignore_errors=True)
|
|
127
|
+
|
|
128
|
+
def __del__(self) -> None:
|
|
129
|
+
self.__finalise()
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def max_size(self) -> int:
|
|
133
|
+
return self._max_size
|
|
134
|
+
|
|
135
|
+
@max_size.setter
|
|
136
|
+
def max_size(self, max_size: int) -> None:
|
|
137
|
+
if not isinstance(max_size, int):
|
|
138
|
+
raise TypeError
|
|
139
|
+
with self._lock:
|
|
140
|
+
self._max_size = max_size
|
|
141
|
+
self._free()
|
|
142
|
+
|
|
143
|
+
def __setitem__(self, key: bytes, value: bytes) -> None:
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._remove(key)
|
|
146
|
+
self._ram[key] = (value, True)
|
|
147
|
+
self._size += len(value)
|
|
148
|
+
self._free()
|
|
149
|
+
|
|
150
|
+
def _remove(self, key: bytes) -> None:
|
|
151
|
+
if key in self._ram:
|
|
152
|
+
data = self._ram.pop(key)[0]
|
|
153
|
+
self._size -= len(data)
|
|
154
|
+
|
|
155
|
+
def __delitem__(self, key: bytes) -> None:
|
|
156
|
+
with self._lock:
|
|
157
|
+
self._remove(key)
|
|
158
|
+
if key in self._disk:
|
|
159
|
+
del self._disk[key]
|
|
160
|
+
|
|
161
|
+
def _free(self) -> None:
|
|
162
|
+
"""Push some values to disk"""
|
|
163
|
+
if self._size > self._max_size:
|
|
164
|
+
keys = iter(self._ram.copy())
|
|
165
|
+
while self._size > self._max_size:
|
|
166
|
+
key = next(keys)
|
|
167
|
+
value, changed = self._ram.pop(key)
|
|
168
|
+
self._size -= len(value)
|
|
169
|
+
if changed:
|
|
170
|
+
self._disk[key] = value
|
|
171
|
+
|
|
172
|
+
def __getitem__(self, key: bytes) -> bytes:
|
|
173
|
+
with self._lock:
|
|
174
|
+
if key in self._ram:
|
|
175
|
+
value = self._ram.pop(key)
|
|
176
|
+
# Push it to the end
|
|
177
|
+
self._ram[key] = value
|
|
178
|
+
return value[0]
|
|
179
|
+
elif key in self._disk:
|
|
180
|
+
data = self._disk[key]
|
|
181
|
+
self._ram[key] = (data, False)
|
|
182
|
+
self._size += len(data)
|
|
183
|
+
self._free()
|
|
184
|
+
return data
|
|
185
|
+
else:
|
|
186
|
+
raise KeyError
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class GlobalDiskCache(DiskCache):
|
|
190
|
+
_instance_ref: Callable[[], Optional[GlobalDiskCache]] = cast(
|
|
191
|
+
Callable[[], Optional["GlobalDiskCache"]], lambda: None
|
|
192
|
+
)
|
|
193
|
+
_cache_size = 100_000_000
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def instance(cls) -> GlobalDiskCache:
|
|
197
|
+
"""
|
|
198
|
+
Get the global disk cache instance.
|
|
199
|
+
The caller must store a strong reference to the returned value otherwise it will be destroyed.
|
|
200
|
+
"""
|
|
201
|
+
instance: Optional[GlobalDiskCache] = cls._instance_ref()
|
|
202
|
+
if instance is None:
|
|
203
|
+
instance = GlobalDiskCache()
|
|
204
|
+
cls._instance_ref = ref(instance)
|
|
205
|
+
return instance
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def cache_size(cls) -> int:
|
|
209
|
+
instance: Optional[GlobalDiskCache] = cls._instance_ref()
|
|
210
|
+
if instance is None:
|
|
211
|
+
return cls._cache_size
|
|
212
|
+
else:
|
|
213
|
+
return instance.max_size
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def set_cache_size(cls, size: int) -> None:
|
|
217
|
+
instance: Optional[GlobalDiskCache] = cls._instance_ref()
|
|
218
|
+
cls._cache_size = size
|
|
219
|
+
if instance is not None:
|
|
220
|
+
instance.max_size = size
|
|
221
|
+
|
|
222
|
+
def __init__(self) -> None:
|
|
223
|
+
self._temp_dir = TempDir("level_data")
|
|
224
|
+
super().__init__(os.path.join(self._temp_dir, "history_db"), 100_000_000)
|