amulet-core 2.0a5__cp311-cp311-macosx_10_9_universal2.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of amulet-core might be problematic. Click here for more details.

Files changed (210) hide show
  1. amulet/__init__.cpython-311-darwin.so +0 -0
  2. amulet/__init__.pyi +30 -0
  3. amulet/__pyinstaller/__init__.py +2 -0
  4. amulet/__pyinstaller/hook-amulet.py +4 -0
  5. amulet/_init.py +28 -0
  6. amulet/_version.py +21 -0
  7. amulet/biome.cpp +36 -0
  8. amulet/biome.hpp +43 -0
  9. amulet/biome.pyi +77 -0
  10. amulet/block.cpp +435 -0
  11. amulet/block.hpp +119 -0
  12. amulet/block.pyi +273 -0
  13. amulet/block_entity.cpp +12 -0
  14. amulet/block_entity.hpp +56 -0
  15. amulet/block_entity.pyi +80 -0
  16. amulet/chunk.cpp +16 -0
  17. amulet/chunk.hpp +99 -0
  18. amulet/chunk.pyi +30 -0
  19. amulet/chunk_/components/biome.py +155 -0
  20. amulet/chunk_/components/block_entity.py +117 -0
  21. amulet/chunk_/components/entity.py +64 -0
  22. amulet/chunk_/components/height_2d.py +16 -0
  23. amulet/chunk_components.pyi +95 -0
  24. amulet/collections.pyi +37 -0
  25. amulet/data_types.py +29 -0
  26. amulet/entity.py +180 -0
  27. amulet/errors.py +63 -0
  28. amulet/game/__init__.py +7 -0
  29. amulet/game/_game.py +152 -0
  30. amulet/game/_universal/__init__.py +1 -0
  31. amulet/game/_universal/_biome.py +17 -0
  32. amulet/game/_universal/_block.py +47 -0
  33. amulet/game/_universal/_version.py +68 -0
  34. amulet/game/abc/__init__.py +22 -0
  35. amulet/game/abc/_block_specification.py +150 -0
  36. amulet/game/abc/biome.py +213 -0
  37. amulet/game/abc/block.py +331 -0
  38. amulet/game/abc/game_version_container.py +25 -0
  39. amulet/game/abc/json_interface.py +27 -0
  40. amulet/game/abc/version.py +44 -0
  41. amulet/game/bedrock/__init__.py +1 -0
  42. amulet/game/bedrock/_biome.py +35 -0
  43. amulet/game/bedrock/_block.py +42 -0
  44. amulet/game/bedrock/_version.py +165 -0
  45. amulet/game/java/__init__.py +2 -0
  46. amulet/game/java/_biome.py +35 -0
  47. amulet/game/java/_block.py +60 -0
  48. amulet/game/java/_version.py +176 -0
  49. amulet/game/translate/__init__.py +12 -0
  50. amulet/game/translate/_functions/__init__.py +15 -0
  51. amulet/game/translate/_functions/_code_functions/__init__.py +0 -0
  52. amulet/game/translate/_functions/_code_functions/_text.py +553 -0
  53. amulet/game/translate/_functions/_code_functions/banner_pattern.py +67 -0
  54. amulet/game/translate/_functions/_code_functions/bedrock_chest_connection.py +152 -0
  55. amulet/game/translate/_functions/_code_functions/bedrock_moving_block_pos.py +88 -0
  56. amulet/game/translate/_functions/_code_functions/bedrock_sign.py +152 -0
  57. amulet/game/translate/_functions/_code_functions/bedrock_skull_rotation.py +16 -0
  58. amulet/game/translate/_functions/_code_functions/custom_name.py +146 -0
  59. amulet/game/translate/_functions/_frozen.py +66 -0
  60. amulet/game/translate/_functions/_state.py +54 -0
  61. amulet/game/translate/_functions/_typing.py +98 -0
  62. amulet/game/translate/_functions/abc.py +116 -0
  63. amulet/game/translate/_functions/carry_nbt.py +160 -0
  64. amulet/game/translate/_functions/carry_properties.py +80 -0
  65. amulet/game/translate/_functions/code.py +143 -0
  66. amulet/game/translate/_functions/map_block_name.py +66 -0
  67. amulet/game/translate/_functions/map_nbt.py +111 -0
  68. amulet/game/translate/_functions/map_properties.py +93 -0
  69. amulet/game/translate/_functions/multiblock.py +112 -0
  70. amulet/game/translate/_functions/new_block.py +42 -0
  71. amulet/game/translate/_functions/new_entity.py +43 -0
  72. amulet/game/translate/_functions/new_nbt.py +206 -0
  73. amulet/game/translate/_functions/new_properties.py +64 -0
  74. amulet/game/translate/_functions/sequence.py +51 -0
  75. amulet/game/translate/_functions/walk_input_nbt.py +331 -0
  76. amulet/game/translate/_translator.py +433 -0
  77. amulet/item.py +75 -0
  78. amulet/level/__init__.pyi +27 -0
  79. amulet/level/_load.py +100 -0
  80. amulet/level/abc/__init__.py +12 -0
  81. amulet/level/abc/_chunk_handle.py +335 -0
  82. amulet/level/abc/_dimension.py +86 -0
  83. amulet/level/abc/_history/__init__.py +1 -0
  84. amulet/level/abc/_history/_cache.py +224 -0
  85. amulet/level/abc/_history/_history_manager.py +291 -0
  86. amulet/level/abc/_level/__init__.py +5 -0
  87. amulet/level/abc/_level/_compactable_level.py +10 -0
  88. amulet/level/abc/_level/_creatable_level.py +29 -0
  89. amulet/level/abc/_level/_disk_level.py +17 -0
  90. amulet/level/abc/_level/_level.py +453 -0
  91. amulet/level/abc/_level/_loadable_level.py +42 -0
  92. amulet/level/abc/_player_storage.py +7 -0
  93. amulet/level/abc/_raw_level.py +187 -0
  94. amulet/level/abc/_registry.py +40 -0
  95. amulet/level/bedrock/__init__.py +2 -0
  96. amulet/level/bedrock/_chunk_handle.py +19 -0
  97. amulet/level/bedrock/_dimension.py +22 -0
  98. amulet/level/bedrock/_level.py +187 -0
  99. amulet/level/bedrock/_raw/__init__.py +5 -0
  100. amulet/level/bedrock/_raw/_actor_counter.py +53 -0
  101. amulet/level/bedrock/_raw/_chunk.py +54 -0
  102. amulet/level/bedrock/_raw/_chunk_decode.py +668 -0
  103. amulet/level/bedrock/_raw/_chunk_encode.py +602 -0
  104. amulet/level/bedrock/_raw/_constant.py +9 -0
  105. amulet/level/bedrock/_raw/_dimension.py +343 -0
  106. amulet/level/bedrock/_raw/_level.py +463 -0
  107. amulet/level/bedrock/_raw/_level_dat.py +90 -0
  108. amulet/level/bedrock/_raw/_typing.py +6 -0
  109. amulet/level/bedrock/_raw/leveldb_chunk_versions.py +83 -0
  110. amulet/level/bedrock/chunk/__init__.py +1 -0
  111. amulet/level/bedrock/chunk/_chunk.py +126 -0
  112. amulet/level/bedrock/chunk/components/__init__.py +0 -0
  113. amulet/level/bedrock/chunk/components/chunk_version.py +12 -0
  114. amulet/level/bedrock/chunk/components/finalised_state.py +13 -0
  115. amulet/level/bedrock/chunk/components/raw_chunk.py +15 -0
  116. amulet/level/construction/__init__.py +0 -0
  117. amulet/level/java/__init__.pyi +21 -0
  118. amulet/level/java/_chunk_handle.py +17 -0
  119. amulet/level/java/_chunk_handle.pyi +15 -0
  120. amulet/level/java/_dimension.py +20 -0
  121. amulet/level/java/_dimension.pyi +13 -0
  122. amulet/level/java/_level.py +184 -0
  123. amulet/level/java/_level.pyi +120 -0
  124. amulet/level/java/_raw/__init__.pyi +19 -0
  125. amulet/level/java/_raw/_chunk.pyi +23 -0
  126. amulet/level/java/_raw/_chunk_decode.py +561 -0
  127. amulet/level/java/_raw/_chunk_encode.py +463 -0
  128. amulet/level/java/_raw/_constant.py +9 -0
  129. amulet/level/java/_raw/_constant.pyi +20 -0
  130. amulet/level/java/_raw/_data_pack/__init__.py +2 -0
  131. amulet/level/java/_raw/_data_pack/__init__.pyi +8 -0
  132. amulet/level/java/_raw/_data_pack/data_pack.py +241 -0
  133. amulet/level/java/_raw/_data_pack/data_pack.pyi +197 -0
  134. amulet/level/java/_raw/_data_pack/data_pack_manager.py +77 -0
  135. amulet/level/java/_raw/_data_pack/data_pack_manager.pyi +75 -0
  136. amulet/level/java/_raw/_dimension.py +86 -0
  137. amulet/level/java/_raw/_dimension.pyi +72 -0
  138. amulet/level/java/_raw/_level.py +507 -0
  139. amulet/level/java/_raw/_level.pyi +238 -0
  140. amulet/level/java/_raw/_typing.py +3 -0
  141. amulet/level/java/_raw/_typing.pyi +5 -0
  142. amulet/level/java/anvil/__init__.py +2 -0
  143. amulet/level/java/anvil/__init__.pyi +11 -0
  144. amulet/level/java/anvil/_dimension.py +170 -0
  145. amulet/level/java/anvil/_dimension.pyi +109 -0
  146. amulet/level/java/anvil/_region.py +421 -0
  147. amulet/level/java/anvil/_region.pyi +197 -0
  148. amulet/level/java/anvil/_sector_manager.py +223 -0
  149. amulet/level/java/anvil/_sector_manager.pyi +142 -0
  150. amulet/level/java/chunk.pyi +81 -0
  151. amulet/level/java/chunk_/_chunk.py +260 -0
  152. amulet/level/java/chunk_/components/inhabited_time.py +12 -0
  153. amulet/level/java/chunk_/components/last_update.py +12 -0
  154. amulet/level/java/chunk_/components/legacy_version.py +12 -0
  155. amulet/level/java/chunk_/components/light_populated.py +12 -0
  156. amulet/level/java/chunk_/components/named_height_2d.py +37 -0
  157. amulet/level/java/chunk_/components/status.py +11 -0
  158. amulet/level/java/chunk_/components/terrain_populated.py +12 -0
  159. amulet/level/java/chunk_components.pyi +22 -0
  160. amulet/level/java/long_array.pyi +38 -0
  161. amulet/level/java_forge/__init__.py +0 -0
  162. amulet/level/mcstructure/__init__.py +0 -0
  163. amulet/level/nbt/__init__.py +0 -0
  164. amulet/level/schematic/__init__.py +0 -0
  165. amulet/level/sponge_schematic/__init__.py +0 -0
  166. amulet/level/temporary_level/__init__.py +1 -0
  167. amulet/level/temporary_level/_level.py +16 -0
  168. amulet/palette/__init__.pyi +8 -0
  169. amulet/palette/biome_palette.pyi +45 -0
  170. amulet/palette/block_palette.pyi +45 -0
  171. amulet/player.py +64 -0
  172. amulet/py.typed +0 -0
  173. amulet/selection/__init__.py +2 -0
  174. amulet/selection/abstract_selection.py +342 -0
  175. amulet/selection/box.py +852 -0
  176. amulet/selection/group.py +481 -0
  177. amulet/utils/__init__.pyi +28 -0
  178. amulet/utils/call_spec/__init__.py +24 -0
  179. amulet/utils/call_spec/__init__.pyi +53 -0
  180. amulet/utils/call_spec/_call_spec.py +262 -0
  181. amulet/utils/call_spec/_call_spec.pyi +272 -0
  182. amulet/utils/format_utils.py +41 -0
  183. amulet/utils/generator.py +18 -0
  184. amulet/utils/matrix.py +243 -0
  185. amulet/utils/matrix.pyi +177 -0
  186. amulet/utils/numpy.pyi +11 -0
  187. amulet/utils/numpy_helpers.py +19 -0
  188. amulet/utils/shareable_lock.py +335 -0
  189. amulet/utils/shareable_lock.pyi +190 -0
  190. amulet/utils/signal/__init__.py +10 -0
  191. amulet/utils/signal/__init__.pyi +25 -0
  192. amulet/utils/signal/_signal.py +228 -0
  193. amulet/utils/signal/_signal.pyi +84 -0
  194. amulet/utils/task_manager.py +235 -0
  195. amulet/utils/task_manager.pyi +168 -0
  196. amulet/utils/typed_property.py +111 -0
  197. amulet/utils/typing.py +4 -0
  198. amulet/utils/typing.pyi +6 -0
  199. amulet/utils/weakref.py +70 -0
  200. amulet/utils/weakref.pyi +50 -0
  201. amulet/utils/world_utils.py +102 -0
  202. amulet/utils/world_utils.pyi +109 -0
  203. amulet/version.cpp +136 -0
  204. amulet/version.hpp +142 -0
  205. amulet/version.pyi +94 -0
  206. amulet_core-2.0a5.dist-info/METADATA +103 -0
  207. amulet_core-2.0a5.dist-info/RECORD +210 -0
  208. amulet_core-2.0a5.dist-info/WHEEL +5 -0
  209. amulet_core-2.0a5.dist-info/entry_points.txt +2 -0
  210. amulet_core-2.0a5.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)